关于token是否过期的流程问题
来源:3-13 本章总结
杨清川
2022-08-19
@Component
@Slf4j
public class JwtUtil {
/**
* 秘钥
*/
@Value("${emos.jwt.secret}")
private String secret;
/**
* 令牌过期时间(天) 保存在客户端的
*/
@Value("${emos.jwt.expire}")
private int expire;
/**
* 令牌缓存时间(天数) 保存在Redis端的
*/
@Value("${emos.jwt.cache-expire}")
private int cacheExpire;
public String createToken(Integer userId){
//当前日期向后偏移5天,也就是过期时间
DateTime offsetDate = DateUtil.offset(new Date(), DateField.DAY_OF_YEAR, 5);
Algorithm algorithm = Algorithm.HMAC256(secret); //创建加密算法对象
JWTCreator.Builder builder = JWT.create();
String token = builder.withClaim("userId", userId)
.withExpiresAt(offsetDate)
.sign(algorithm);
return token;
}
public Integer getUserId(String token){
DecodedJWT jwt = JWT.decode(token);
Integer userId = jwt.getClaim("userId").asInt();
return userId;
}
public void verifierToken(String token) {
Algorithm algorithm = Algorithm.HMAC256(secret); //创建加密算法对象
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(token);
}
}@Component
@Scope("prototype")//不让这个过滤器全局唯一,否则与ThreadLocal联用会出现异常
public class OAuth2Filter extends AuthenticatingFilter {
@Resource
private ThreadLocalToken threadLocalToken;
@Value("${emos.jwt.cache-expire}")
private int cacheExpire;
@Resource
private RedisTemplate redisTemplate;
@Resource
private JwtUtil jwtUtil;
/**
* 获取token后判断,如果不为空则封装成Shiro认识的对象进行返回
* 为空则直接返回
*
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req = (HttpServletRequest) request;
String token = getRequestToken(req);
if (StrUtil.isBlank(token)) {
return null;
}
return new OAuth2Token(token);
}
/**
* 用于判断那种请求被Shiro处理,哪种不需要
* options请求不需要处理全部放行
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletRequest req = (HttpServletRequest) request;
//是options请求直接放行,不需要Shiro处理
if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
return true;
}
//其余的需要Shiro处理
return false;
}
/**
* 需要被Shiro处理的请求会进入该方法
*
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
//设置响应字符集和类型
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
//解决跨域问题,允许跨域
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
//当判断token需要刷新,那么就要清空ThreadLocalToken类
threadLocalToken.clear();
//从请求头中获取到token字符串
String token = getRequestToken(req);
if (StrUtil.isBlank(token)) {
//token为空返回错误响应
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
resp.getWriter().print("无效的令牌");
//无需执行后续的授权和认证
return false;
}
//验证token是否有效
try {
jwtUtil.verifierToken(token);
} catch (TokenExpiredException e) {//token是否过期异常
//现在判断Redis中的token是否过期
if (redisTemplate.hasKey(token)) {
//redis存在,说明客户端token过期了,服务端并没有过期,要做令牌的续期,生成全新的令牌
Integer userId = jwtUtil.getUserId(token);
token = jwtUtil.createToken(userId);
//放入Redis,放入ThreadLocalToken类
redisTemplate.opsForValue().set(token, userId + "", cacheExpire, TimeUnit.DAYS);
threadLocalToken.setToken(token);
} else {
//客户端令牌的过期了,服务端的也过期了,需要用户重新登录,返回错误响应
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
resp.getWriter().print("令牌已经过期");
//无需执行后续的授权和认证
return false;
}
} catch (JWTDecodeException jwtDecodeException) {//token字符串不合法
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
resp.getWriter().print("无效的令牌");
//无需执行后续的授权和认证
return false;
}
//token令牌无误,继续正常执行
boolean bool = executeLogin(request, response);
//如果是false说明认证或者授权某一个或全部失败
return bool;
}
/**
* 认证用户的方法
*
* @param token
* @param e
* @param request
* @param response
* @return
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
//设置响应字符集和类型
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
//解决跨域问题,允许跨域
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
//返回错误异常信息
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
try {
resp.getWriter().print(e.getMessage());
} catch (IOException ex) {
}
//认证失败返回false
return false;
}
/**
* 判断该过滤器执行完是否还需要继续后续的程序执行
* @param request
* @param response
* @param chain
* @throws ServletException
* @throws IOException
*/
@Override
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
super.doFilterInternal(request, response, chain);
}
/**
* 私有方法,先从header请求头中获取token,如果失败那么再尝试从请求体中获取
*
* @param request
* @return token字符串
*/
private String getRequestToken(HttpServletRequest request) {
String token = request.getHeader("token");
if (StrUtil.isBlank(token)) {
token = request.getParameter("token");
}
return token;
}
}@Component
public class ThreadLocalToken {
private ThreadLocal<String> local = new ThreadLocal<>();
public void setToken(String token) {
local.set(token);
}
public String getToken() {
return local.get();
}
public void clear() {
local.remove();
}
}@Component
@Aspect
public class TokenAspect {
@Resource
private ThreadLocalToken threadLocalToken;
@Pointcut("execution(public * com.yada.emos.wx.controller.*.*(..)))")
public void aspect(){
}
/**
* 使用环绕通知,去存放token的媒介类中查看是否有新的token生成
* 如果有则放入R对象一起返回,没有则不做处理
* @param point
* @return
* @throws Throwable
*/
@Around("aspect()")
public Object around(ProceedingJoinPoint point) throws Throwable {
R r = (R)point.proceed();
String token = threadLocalToken.getToken();
if (token != null) {
r.put("token", token);
threadLocalToken.clear();
}
return r;
}
}以上四个类
目前我理解的逻辑:
正常情况:一个前端请求被OAuth2Filter拦截,过滤器会验证带来的token是否正确,是否过期,均正确那么开始执行控制器方法,返回响应被AOP拦截,查看token媒介类ThreadLocalToken有没有token,因为没有生成新token所以类中是空的,则把R原封不动的返回。
异常:
1.拦截到的token如果是错误的则直接返回异常结束;如果是过期了,则去Redis中查看是否过期,如果服务端Redis没过期则续期,生成新的令牌保存到Redis和ThreadLocalToken媒介类,执行控制器方法返回响应,被AOP拦截,查看token媒介类没有token,生成了新token,则把R多增加一个token返回给前端,清空媒介类中的token。
2.客户端和服务的token全都过期则返回错误,需要重新登录。
一,请问我以上的理解是否正确。二,如果是第一次登录成功,那么生成的token也会经过媒介类和Redis然后向后执行再AOP处添加到响应的R类中,是吗。
写回答
1回答
-
你的理解大致是正确的,无论第一次还是多少次请求,只要是受Shiro保护的URL路径都要验证Token。那些不需要Shiro保护的URL路径就不需要过滤器验证Token,你继续往下看视频,里面有不受Shiro保护的URL路径,例如登陆方法。
012022-08-20
相似问题