平时会使用MD5来做授权认证,不管是Session还是JWT,但是如果被别人知道了salt是很危险的,因此这里来介绍一个强哈希方法来进行加密,代表的框架就是SpringSecurity提供的BCryptPasswordEncoder类。
此外本篇文章还会主要介绍一下JWT的加密认证授权方式,是因为无状态的登录相对于有状态的登录对于服务器的压力会更小,并且Redis本身不用去存储这个Session。
BCryptPasswordEncoder强哈希方法,每次加密的结果都是不同的。
介绍一下基于Token进行身份验证的流程:
- 客户端使用用户名和密码请求登录
- 服务端收到请求,验证用户名和密码
- 验证成功之后服务端签发一个Token,再将Token发送给客户端
- 客户端收到Token将其存储,如放在Cookie
- 客户端每次向服务端请求资源的时候需要带着服务端签发的Token
- 服务端收到请求,然后去验证客户端请求里面带着token,验证成功就向客户端返回请求到的数据
简单说一下Token机制相对于Cookie机制的好处:
- 支持跨域访问
- 无状态
- 更适用CDN:可以通过内容分发网络请求服务端的所有资料,服务端只需要提供API即可
- 去耦
- 更加适用于移动应用
- CSRF
- 性能:一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算的Token验证和解析要费时得多
- 不需要为登录页面做特殊处理
JWT组成
- 头部(Header)
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等,也可以被表示成一个json对象
- 载荷(playload)
载荷就是存放有效信息的地方,有效信息包含三个部分:
(1)、标准中注册的声明(建议但不强制使用)
(2)、公共的声明:可以添加任何信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可以解密
(3)、私有的声明
- 签证(signature)
签证信息由三部分组成:header(base64后的)、payload(base64后的)、secret
项目实战
(1)、加密认证
导入maven依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
SpringSecurity安全配置文件
/**
* 安全配置类
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* authorizeRequests():security全注解配置的开端,表示开始说明需要的权限。第一部分:拦截的路径 第二部分:访问该路径需要的权限
* antMatchers:拦截什么路径 permitAll:任何权限都可以访问,放行所有
* anyRequest任何的请求,authenticated认证之后才可以访问
* and().csrf().disable()固定写法,使csrf失效
*/
http.authorizeRequests()
.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
}
}
这样就配置完成了最基本的拦截器,说一下BCryptPasswordEncoder加密和MD5不一样,使用MD5时候都是直接getPassword()然后MD5和数据库去对比,但是这个加密每次的结果都是不一样的,但是有一个API可以帮助完成加密之后 的认证。
(2)、基于Java的JJWT实现JWT
同样还是先导入maven依赖
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
</dependencies>
首先先来看看其实如何进行加密的,先来写一个小的demo
创建create_jwt进行加密
/**
* 创建JWT测试类
*/
public class create_jwt {
public static void main(String[] args) {
JwtBuilder jwtBuilder = Jwts.builder()
.setId("521")
.setIssuedAt(new Date()).setSubject("周晓晨")
.signWith(SignatureAlgorithm.HS256, "zxczxc")
.setExpiration(new Date(new Date().getTime() + 60000))
.claim("role", "admin");
System.out.println(jwtBuilder.compact());
}
}
parse_jwt进行解密:
/**
* 解析JWT测试类
*/
public class parse_jwt {
public static void main(String[] args) {
try {
Claims claims = Jwts.parser().setSigningKey("zxczxc")
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI1MjEiLCJpYXQiOjE1NzMzNzc3ODIsInN1YiI6IuWRqOaZk-aZqCIsImV4cCI6MTU3MzM3Nzg0Mn0.9rI4VBzyvvIaFZ8cghACQm4zryetitBgE-C5HZLryOI")
.getBody();
System.out.println("用户id:" + claims.getId());
System.out.println("用户名:" + claims.getSubject());
System.out.println("登录时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(claims.getIssuedAt()));
System.out.println("过期时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(claims.getExpiration()));
System.out.println("登录角色:" + claims.get("role"));
}catch (Exception e){
throw new RuntimeException("JWT TOKEN已经过期");
}
}
}
创建jwt因为设置了过期时间,因此一分钟之后密钥生效。
微服务鉴权
1、首先封装一个创建JWT和解密的工具类
/**
* JWT工具类
*/
@ConfigurationProperties("jwt.config")
public class JwtUtil {
private String key ;
private long ttl ; //一个小时 过期时间
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public long getTtl() {
return ttl;
}
public void setTtl(long ttl) {
this.ttl = ttl;
}
/**
* 生成JWT
*/
public String createJWT(String id, String subject, String roles) {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
JwtBuilder builder = Jwts.builder().setId(id)
.setSubject(subject)
.setIssuedAt(now)
.signWith(SignatureAlgorithm.HS256, key)
.claim("roles", roles);
if (ttl > 0) {
builder.setExpiration(new Date(nowMillis + ttl));
}
return builder.compact();
}
/**
* 解析JWT
*/
public Claims parseJWT(String jwtStr){
return Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(jwtStr)
.getBody();
}
}
2、基于JWT的授权
下面会展示一个Admin(管理员)账户认证与授权的案例,因为admin和user级别实现起来相同,user有很多角色可以去指定,admin只有admin角色。
admin角色是无法手动注册的,需要老大去给admin添加角色的。接口也是完成了对于password的加密操作,加密的算法引用的是SpringSecurity的BCryptPasswordEncoder加密方法,这个在上面也介绍过了。
接下来进行了admin账户的认证接口的编写,让客户端输入的明文去与mysql的密文通过SpringSecurity的api去对比,如果合格了,就会将role和token传过来,当然token里也可以通过上面提到的claim进行自定义传值。
首先是admin添加用户的方法:
/**
* 增加
*/
public void add(Admin admin) {
admin.setId( idWorker.nextId()+"" );
//密码加密
admin.setPassword(bCryptPasswordEncoder.encode(admin.getPassword()));
adminDao.save(admin);
添加几个用户
admin登录:
/**
* Admin Login
*/
public Admin login(Admin admin) {
//根据用户名查询对象
Admin admin_login = adminDao.findByLoginname(admin.getLoginname());
//数据库中的密码和用户输入的密码是一致的
if (admin_login != null && bCryptPasswordEncoder.matches(admin.getPassword(), admin_login.getPassword())) {
return admin_login;
}
return null;
}
测试结果如下:
admin登录成功,token中包含了各种的信息(包含角色),可以添加到HTTP请求头中,进行接口是否可以访问的认证。
假设我们写一个接口必须是admin角色才可以去执行:
Service逻辑:首先我们得获取到请求头,并且针对Http header进行过滤获取到token,对token去执行BCryptPasswordEncoder里面的解密操作,拿出role去进行判断。
/**
* 删除 必须有admin角色才能删除
*/
public void deleteById(String id) {
String header = request.getHeader("Authorization");
if (StringUtils.isEmpty(header)) {
throw new RuntimeException("请先登录");
}
if (!header.startsWith("Bearer ")) {
throw new RuntimeException("权限不足");
}
//得到token
String token = header.substring(7);
try {
Claims claims = jwtUtil.parseJWT(token);
String role = claims.get("role").toString();
if (role == null || role.equals("admin")) {
throw new RuntimeException("权限不足");
}
} catch (Exception e) {
throw new RuntimeException("权限不足");
}
userDao.deleteById(id);
}
3、基于JWT接口授权优化
deleteById接口的实现方法是,从http请求中找到Authorization的key过滤掉Bearer 最后得到token,通过token再parseJwt,得到claim。
事实上SpringMVC可以做到就是Interceptor。但是在SpringBoot中该如何让他进行激活那就是将拦截器进行注册。
事实上SpringBoot是有这种注册机制的,也是通过WebMvcConfigurationSupport去实现的。
首先对这种SpringBoot的注册机制进行配置做一个基本的拦截。
@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/**/login/**");
}
}
编写拦截器
/**
* 拦截器
*/
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("经过了拦截器");
//无论如何都要放行,具体能不能操作还是在具体的过程中去判断。拦截器只是负责将头请求头中包含token的令牌进行一个解析
String header = request.getHeader("Authorization");
if (header != null && !"".equals(header)) {
//请求头包含Authorization头信息,对其进行解析
if (header.startsWith("Broker ")) {
//获取token
String token = header.substring(7);
try {
Claims claims = jwtUtil.parseJWT(token);
String roles = (String) claims.get("roles");
if (roles != null && roles.equals("admin")) {
request.setAttribute("claims_admin", token);
}
if (roles != null && roles.equals("user")) {
request.setAttribute("claims_user", token);
}
}catch (Exception e){
throw new RuntimeException("令牌不正确");
}
}
}
return true;
}
}