项目采用 是JWT + Redis的混合方案实现认证授权,下面来详细看看流程
一、token的创建
关于token的创建和保存我认为应该从以下几点出发,首先是怎么创建的,然后是何时创建的,。
怎么创建的
token是由三部分拼接而成的,具体的操作方式是由我们程序员自己决定的,在我们的项目里有一个核心的JwtUtil放在travel-common-core这个项目里,根据尾缀可以判断放在工具的软件包里,在这个工具里可以看到最核心的拼接方式
public static String createToken(Map<String, Object> claims)
{
String token = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512secret).compact();
return token;
}
何时创建的
答案是:在用户登录的时候创建的(注意:不是注册时候创建),我们可以在travel-auth的controller中看到
// 用户登录
LoginUser userInfo = null;
if (form.getType().equals(TokenConstants.ADMIN)) {
userInfo = sysLoginService.login(form.getUsername(), form.getPassword());
} else {
userInfo = pcLoginService.login(form.getUsername(), form.getPassword());
}
// 获取登录token
return R.ok(tokenService.createToken(userInfo));
有一个tokenService中的createToken方法,可以看到与jwtUtil中的方法名是一模一样,那到底有何区别呢,我们来看看看这个createToken方法
String token = IdUtils.fastUUID();
Long userId = loginUser.getUserid();
String userName = loginUser.getUsername();
loginUser.setToken(token);
loginUser.setUserid(userId);
loginUser.setUsername(userName);
loginUser.setIpaddr(IpUtils.getIpAddr());
refreshToken(loginUser);
// Jwt存储信息
Map<String, Object> claimsMap = new HashMap<String, Object>();
claimsMap.put(SecurityConstants.USER_KEY, token);
claimsMap.put(SecurityConstants.DETAILS_USER_ID, userId);
claimsMap.put(SecurityConstants.DETAILS_USERNAME, userName);
// 接口返回信息
Map<String, Object> rspMap = new HashMap<String, Object>();
rspMap.put("access_token", JwtUtils.createToken(claimsMap));
rspMap.put("expires_in", expireTime);
return rspMap;
;第一段代码的目的是为了把封装好的 loginUser对象传给这个refreshToken方法,这里的token并非真正的token,他其实是登录用户的唯一标识,第二段代码new了一个claimMap,前面我们也讲过这个clain就是载荷的意思,那么很明显这个,这第二段代码是用来存储jwt信息的,第三段new了一个rspMap,很明显就是response这个单词,也就是响应给前端的数据,往里面放了token和expireTime(过期时间),很明显,这个createToken的方法是面向登录这个业务的,而另一个则是一个纯粹的生成token的工具类。
刚才的方法体中的refreshToken方法非常重要,意思是刷新token,我们按ctrl跟进
{
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 根据uuid将loginUser缓存
String userKey = getTokenKey(loginUser.getToken());
redisService.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}
设置了过期时间。userKey,还调用了 redisService.setCacheObject,这里无疑就是登录信息(包括用户的全部信息)存到reids中,这也是项目中的高明之处,JWT 只存储轻量级标识信息(如 user_key、user_id、username),而完整的用户信息(权限、角色等)存储在 Redis 中,这样做的好处有很多:
- 支持服务端主动注销(删除 Redis 缓存即可)
- Token 体积小,传输效率高
- 权限数据可实时变更,无需重新签发 Token,(token一经签发无法作废)
好了,到这里,token的创建流程已经明确了,继续来看看token接下来的走向
网关过滤器
在travel-gateway的filter包中,可以看到过滤器对应的方法,我们来找对应token的部分
if (!request.getURI().getPath().startsWith("/admin/")) {
String token = getToken(request);
if (StringUtils.isNotEmpty(token)) {
return checkToken(token, exchange, mutate, chain);
}
return chain.filter(exchange);
}
String token = getToken(request);
if (StringUtils.isEmpty(token)) {
return unauthorizedResponse(exchange, "令牌不能为空");
}
return checkToken(token, exchange, mutate, chain);
这里首先对是否带/admin的路径进行了判断,如果是管理的员的话,没有token也放行,剩下的由业务进行判断,如果不是管理员的话,必须有令牌,不然会报错
业务服务拦截器
String token = SecurityUtils.getToken();
if (StringUtils.isNotEmpty(token))
{
LoginUser loginUser = AuthUtil.getLoginUser(token);
if (StringUtils.isNotNull(loginUser))
{
AuthUtil.verifyLoginUserExpire(loginUser);
SecurityContextHolder.set(SecurityConstants.LOGIN_USER, loginUser);
}
}
return true;
首先获取token(注意:这里的token指的是UserKey,用来在redis查用户数据的),如果用户数据不为空进行 AuthUtil.verifyLoginUserExpire(loginUser),校验是否快过期,做续活逻辑(每次登录都会延长token的存活时间),把登录用户放到本地线程上下文(ThreadLocal 封装)。后面 Controller / Service 通过 SecurityUtils.getLoginUser() 就可以拿到登录用户,拿 userId 做业务。到这里token的使命就基本完成了,可以看到并没有对是否能访问controller的方法进行限制,这里就要靠注解了,最后token传进来的数据会随着业务的调用游走整个项目,这样jwt的完整的工作流程就结束了
整个流程
前端带 token → 网关 GlobalFilter → 执行 checkToken()
校验 JWT、查 Redis
token 合法:删掉原来的 JWT,把 userId、username、userkey 放进请求头,放行
http 请求转发到后端业务微服务
后端微服务的拦截器 (HandlerInterceptor) 拿到 header 里面的用户 id
把用户信息存入 ThreadLocal,后续 Controller、Service 直接取用
到达 Controller