若依项目学习笔记05——JWT
1. 什么是JWT
关于JWT,在上一篇博文中有一个学习链接,大家看一下;本学习系列对涉及到的技术不会过多去讲解,这里只对JWT进行大致介绍,大家自行去学习吧;
JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案,通过客户端保存数据,而服务器根本不保存会话数据,每个请求都被发送回服务器;JWT由三段信息构成的,将这三段信息文本用 . 链接一起就构成了Jwt字符串。这三段分别为头部、载荷和签证,就像这样==> eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
1.1 JWT组成
- header 头部
{
'typ': 'JWT',
'alg': 'HS256'
}
由两个部分组成,分别是声明类型(JWT)和声明加密的算法(一般为HMAC SHA256),上面是加密前
1.2 playload 载荷
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
保存用户信息,分别包括:
- 标准中注册的声明(建议但不强制)
iss: jwt签发者
sub: 主题
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 生效时间
iat: 签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
- 公共的声明
可以添加任意的信息,一般添加用户的相关信息或其他业务需要的必要信息;不建议添加敏感信息,因为该部分在客户端可解密
- 私有的声明
是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,则着该部分信息可以归类为明文信息
1.3 Signature 签证
将前面两个部分(加密后)使用头部声明的加密方式进行secret组合加密,就得到三段加密后,用 . 进行分割的jwt字符串
注意! secret和jwt的签发生成都是在服务器端,secret是用于进行jwt的签发和jwt的验证,≈你服务端的私钥,千万不可泄漏!。不然客户端知道这个secret,则客户端可以自我签发jwt
2. JWT实现
在登录验证SysLoginController的最后,会生成token(即jwt,会传到前端,前端根据这个jwt来和后端进行交互),这里调用的是token验证处理framework.web.service.TokenService,我们先来看她的 createToken()
方法,相关讲解在注释中给出,大家自行点进各方法进行查看
2.1 创建token
public String createToken(LoginUser loginUser)
{
//设置token唯一标识
String token = IdUtils.fastUUID();
loginUser.setToken(token);
//设置浏览器相关信息,如IP地址,系统版本等等
setUserAgent(loginUser);
//设置缓存,如有效期及时长,组装唯一标识:Constants.LOGIN_TOKEN_KEY + uuid
refreshToken(loginUser);
//给前端返回jwt的值
Map<String, Object> claims = new HashMap<>();
//这里的token是标识
claims.put(Constants.LOGIN_USER_KEY, token);
//创建,见下
return createToken(claims);
}
最后是返回创建一个token,使用的是 createToken()
方法,我们点进去looklook
private String createToken(Map<String, Object> claims)
{
//这里所调用的是Jwts的api,我们已经在pom中导入了,用于token的生成和解析
String token = Jwts.builder()
.setClaims(claims)
//设置加密方法,密钥
.signWith(SignatureAlgorithm.HS512, secret).compact();
return token;
}
这个方法所返回的字符串就是前端所拿到的token,我们可以运行项目看一下(大家也可以去redis中去看)
2.2 获取token
我们找到 getLoginUser()
,
public LoginUser getLoginUser(HttpServletRequest request)
{
// 获取请求携带的令牌,用getToken从header(已在本类和配置文件中定义,)中获取,判断是否为空并且是否有以 Bearer 为前缀,然后替换(我们只要jwt,不用前缀)
String token = getToken(request);
if (StringUtils.isNotEmpty(token))
{
//获取token,secret已在本类和配置文件定义,secret尽量不可重复和无规律
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
//根据key从缓存中获取
LoginUser user = redisCache.getCacheObject(userKey);
return user;
}
return null;
}
2.2 其他方法
这部分就不详细讲了,大致讲一下其作用
-
setLoginUser()
: 设置用户信息,比如我们更新了用户信息,密码等,就会刷新token -
delLoginUser(S)
: 删除用户身份信息,根据token删除缓存中的用户数据 -
verifyToken()
: 验证令牌有效期,相差不足20分钟,自动刷新缓存,这样就不会说,操作这个系统的人还在使用,结果到了一定实现就掉线了 -
refreshToken()
: 刷新令牌有效期,令牌有效期expireTime已在本类和配置文件定义了
3. JWT过滤器
我们回到最初的起点,回到 SecurityConfig,我们添加了jwt过滤器
...
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;//token认证过滤器
...
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
...
我们进到自定义的 JwtAuthenticationTokenFilter
当中,
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter//继承OncePerRequestFilter,确保一次请求只会通过一次
{
@Autowired
private TokenService tokenService;//这个就是上面刚刚说的jwt实现啦
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException
{
//获取loginUser,具体操作上面已讲
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
{
//验证,刷新令牌有效期
tokenService.verifyToken(loginUser);
//通用设置,如loginUser,权限等
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
//设置request信息
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//将上面的信息放置到上下文当中
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}
如果所需信息没有设置进上下文,直接进入过滤器处理 chain.doFilter(request, response);
的话,会出现异常,我们可以进入security总过滤器类 FilterSecurityInterceptor.invoke 方法,其中有一个预检查方法 beforeInvocation()
,会验证角色或菜单是否有配置对应权限,或者URL是否合法等等;如果有异常会进入错误异常过滤器 ExceptionTranslationFilter
,如果异常不为空,会进入 handleSpringSecurityException
异常,然后调用 sendStartAuthentication()
方法,其中, commence()
方法被我们在 AuthenticationEntryPointImpl
重写了
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
{
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
throws IOException
{
//设置错误code和错误描述
int code = HttpStatus.UNAUTHORIZED;
String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
//通过流,发送给前端
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
}
}