关于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回答

神思者

2022-08-20

你的理解大致是正确的,无论第一次还是多少次请求,只要是受Shiro保护的URL路径都要验证Token。那些不需要Shiro保护的URL路径就不需要过滤器验证Token,你继续往下看视频,里面有不受Shiro保护的URL路径,例如登陆方法。

0
1
杨清川
非常感谢!
2022-08-20
共1条回复

SpringBoot 在线协同办公小程序开发 全栈式项目实战

未来趋势型业务 + 前后端综合技术栈 + 惊艳的细节打磨

1798 学习 · 1915 问题

查看课程