关于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
相似问题