token认证过滤器实现(配合redis)

该代码实现了一个基于JWT的认证过滤器,通过检查请求头中的token来验证用户。如果token不存在或非法,将抛出异常。合法的token会被解析,从Redis缓存中获取用户信息,并创建AuthenticationToken存储在SecurityContextHolder中,以进行权限控制。
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        //获取token
        String token = request.getHeader("token");
        if(!StringUtils.hasText(token)){
            //放行   继承类 OncePerRequestFilter 的方法 doFilterInternal 放行方法如下
            filterChain.doFilter(request,response);
            //一定要记住有个return 不然上面方法如果返回后到此依然执行到下面的全部代码了
            return;
        }
        //解析token
        String userid;
        try{
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        //从redis中获取用户信息
        String redisKey = "login" + userid;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if(Objects.isNull(loginUser)){
            throw new RuntimeException("用户未登录");
        }

        //todo 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);
        //存入到SecurityContextHolder 接受参数 AuthenticationToken 对象,所有上一步要封装一个AuthenticationToken对象,参数分别是数据 身份 权限
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request,response);

    }
}```
<think>我们正在实现一个过滤器(Filter)用于JWT token的校验和续期。根据引用[2]和[4],常见的做法是在过滤器中校验token的有效性,并在一定条件下进行token的续期(即延长token的有效时间)。 主要步骤: 1. 从请求头中获取token。 2. 校验token的有效性(如签名、过期时间等)。 3. 如果token有效,则判断是否需要续期(比如,当token距离过期时间较近时,或者根据配置的续期策略)。 4. 续期操作:生成一个新的token(或者更新原有token的过期时间)并返回给前端,或者在后端存储中更新token的过期时间(如果token存储在Redis中)。 引用[2]提到单Token方案:设置较短的过期时间(如15分钟),当用户请求时,如果token过期但在允许的重新登录期限内(如72小时),则允许刷新token。但注意,刷新操作通常由前端在收到特定错误码后发起,而过滤器内也可以实现自动刷新(在token即将过期时刷新,而不是等到完全过期)。 引用[4]展示了一个在过滤器中续期token的示例:在验证通过后,启动一个新线程来更新Redistoken的过期时间(即续期)。这样,用户的操作不会因为续期操作而阻塞。 因此,我们可以设计一个过滤器,在每次请求时进行如下操作: 步骤1:从请求头中获取token(例如:Authorization头)。 步骤2:如果请求路径是公开的(如登录、注册等),则直接放行。 步骤3:对于需要认证的请求,校验token: - 如果token无效(签名错误、完全过期等),则返回401错误。 - 如果token有效,但即将过期(比如在5分钟内过期),则进行续期操作。 步骤4:续期操作:更新token的过期时间(在Redis中更新过期时间,或者生成一个新token并返回给前端)。 注意:引用[4]中的做法是更新Redistoken的过期时间(因为他们的token是存储在Redis中的)。而如果使用无状态的JWT(即token自包含,不存储于服务端),则无法直接更新原有的token,而是需要生成一个新的token,并在响应头中返回给前端。前端在后续请求中需要携带这个新的token。 因此,我们需要确定token的存储方式: - 如果使用有状态方式(如将token存储在Redis),则续期就是更新Redistoken的过期时间。 - 如果使用无状态方式(JWT自包含),则续期需要生成一个新的token,并返回给前端。 根据引用[2]和[3],我们这里采用单Token方案,并且使用有状态存储(Redis)来存储token,这样可以方便地续期(只需更新过期时间)而不需要生成新token。但注意,引用[2]中的示例是无状态的JWT,它设置了一个较短的过期时间,并在过期后要求前端刷新。而引用[4]使用了Redis存储,所以续期就是更新Redis的过期时间。 我们这里参考引用[4]的方式,使用Redis存储token,这样在过滤器中可以更新Redis的过期时间。 设计实现: 1. 过滤器类:TokenVerifyFilter 2. 在过滤器中,排除不需要拦截的路径(如登录、注册等)。 3. 从请求头中获取token(例如:Authorization: Bearer <token>)。 4. 解析token,获取用户信息(如用户id),并验证token的有效性(包括检查Redis中是否存在此token,以及token是否被篡改)。 5. 如果token无效,返回401。 6. 如果token有效,则判断是否需要续期(比如,当前token的剩余时间小于某个阈值,比如5分钟)。 7. 如果需要续期,则更新Redis中该token的过期时间(延长一段时间,如30分钟)。注意:为了避免阻塞请求,可以异步执行续期操作(如使用新线程)。 8. 将用户信息存入请求属性中,以便后续使用。 9. 放行请求。 注意:如果使用有状态方式,那么每次请求都需要查询Redis来验证token是否存在。如果使用无状态方式(JWT自包含),则只需要验证签名和过期时间,但无法在过期前续期(因为JWT的内容是固定的,过期时间已经写入,只能重新生成)。 因此,我们采用有状态方式:将JWT存储在Redis中,并设置过期时间。这样,即使JWT本身包含过期时间,我们也可以通过在Redis中设置更短的过期时间来控制。同时,续期操作就是更新Redis的过期时间。 但是,为了安全,我们仍然需要验证JWT的签名,以防止伪造。所以步骤是: - 从请求头获取token。 - 验证token的签名和过期时间(使用JWT库验证)。 - 从Redis中获取该用户对应的token,并比较是否请求的token一致(防止token被重置后,旧token还能使用)。 - 如果Redis中没有,则说明token已失效(比如用户登出)。 具体代码结构(Java)示例: ```java import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TokenVerifyFilter implements Filter { private RedisService redisService; // 假设的Redis服务 private JwtUtil jwtUtil; // 假设的JWT工具类 private ExecutorService executorService = Executors.newSingleThreadExecutor(); // 用于异步续期 @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; // 1. 排除不需要拦截的路径 String path = req.getRequestURI(); if (path.startsWith("/api/auth") || path.startsWith("/public")) { chain.doFilter(request, response); return; } // 2. 获取token String token = req.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } token = token.substring(7); // 去掉Bearer // 3. 验证token的签名和过期时间 Claims claims = null; try { claims = jwtUtil.parseToken(token); } catch (Exception e) { // 验证失败(签名错误、格式错误等) res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } // 4. 从claims中获取用户id String userId = claims.getSubject(); // 5. 检查Redis中是否存在该token(并且当前token一致) String redisToken = redisService.get(Constants.REDIS_JWT_KEY + userId); if (redisToken == null || !redisToken.equals(token)) { // token不存在或已被新token替换(如用户在其他地方登录) res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } // 6. 判断是否需要续期(例如,距离过期时间小于5分钟) long ttl = redisService.getExpire(Constants.REDIS_JWT_KEY + userId); // 获取剩余时间(秒) if (ttl < 5 * 60) { // 小于5分钟 // 异步续期,避免阻塞请求 executorService.submit(() -> { // 重新设置过期时间为30分钟 redisService.expire(Constants.REDIS_JWT_KEY + userId, 30 * 60, TimeUnit.SECONDS); }); } // 7. 将用户信息存入请求属性 request.setAttribute("userId", userId); // 8. 放行 chain.doFilter(request, response); } // 其他方法(init, destroy)省略 } ``` 注意: - 上述代码中,我们使用了一个线程池(单线程)来异步执行续期操作,避免阻塞请求线程。 - Redis中存储token的key为:Constants.REDIS_JWT_KEY + userId,这样每个用户只有一个有效的token(后登录会覆盖之前的)。 - 在验证时,我们不仅验证了JWT的签名和过期时间,还检查了Redis中是否存在相同的token。这可以防止用户登出后token仍然被使用(因为登出会删除Redis中的token),以及防止用户在其他设备登录导致旧token失效。 另外,关于续期策略,我们只在token剩余时间小于5分钟时才续期,并且每次续期30分钟。这样可以避免每次请求都续期,减少不必要的操作。 引用[4]中使用了请求头中的rememberMe字段来决定是使用较长的过期时间(Constants.EXPIRE_TIME)还是默认的过期时间(Constants.DEFAULT_EXPIRE_TIME)。在我们的实现中,可以在生成token时根据rememberMe设置不同的初始过期时间,在续期时也可以根据rememberMe设置不同的续期时间。但注意,在过滤器中如何获取rememberMe?我们可以将其放在请求头中(如引用[4]的做法),然后在续期时读取。 修改:在续期时,我们需要知道该用户登录时是否选择了“记住我”。因此,可以在登录时,将rememberMe信息也存储在Redis中(token一起存储,或者单独存储),或者在生成token时将rememberMe作为claim存入JWT,这样在过滤器中可以解析出来。但是,注意JWT的内容虽然可以解析,但用户可能修改,所以不能信任。因此,我们可以在登录时将rememberMe信息存储在Redis中(token关联)。 另一种做法是,在请求头中携带rememberMe信息(如引用[4]),这样过滤器可以直接读取。但这样需要前端每次请求都发送rememberMe,可能不太安全(因为rememberMe是敏感信息,且不应该由前端随意指定)。所以更安全的做法是,在登录时,将rememberMe信息存储在Redis中(token关联)。 因此,我们可以修改Redis存储结构:不再只存储token字符串,而是存储一个包含token和rememberMe的对象(或者用两个key,一个存token,一个存rememberMe)。但为了简单,我们可以将rememberMe信息作为key的一部分?不太合适。所以,我们可以使用Hash结构: key: Constants.REDIS_JWT_KEY + userId field: token -> 存储token字符串 field: rememberMe -> 存储布尔值 或者,我们也可以将token和rememberMe封装成一个对象,然后序列化为字符串存储。但这样每次都要序列化和反序列化。 这里我们选择使用两个key: tokenKey: Constants.REDIS_JWT_KEY + userId 存储token字符串 rememberKey: Constants.REDIS_REMEMBER_KEY + userId 存储rememberMe(字符串或布尔值) 但这样会增加一次Redis查询。为了效率,我们可以一次获取多个key的值。 另一种方式:在登录生成token后,将token和rememberMe一起存储在一个hash中: key: Constants.REDIS_JWT_KEY + userId field: "token" -> token字符串 field: "rememberMe" -> "true"或"false" 这样,在过滤器中,我们使用hgetall获取这个hash,然后得到token和rememberMe。 修改步骤5和6: // 5. 获取Redis中存储的token和rememberMe String redisKey = Constants.REDIS_JWT_KEY + userId; Map<String, String> tokenInfo = redisService.hgetAll(redisKey); if (tokenInfo == null || !token.equals(tokenInfo.get("token"))) { // token无效 res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } // 6. 判断是否需要续期 long ttl = redisService.ttl(redisKey); // 获取key的剩余时间 if (ttl < 5 * 60) { boolean rememberMe = Boolean.parseBoolean(tokenInfo.get("rememberMe")); long newExpireTime = rememberMe ? Constants.LONG_EXPIRE_TIME : Constants.DEFAULT_EXPIRE_TIME; executorService.submit(() -> { redisService.expire(redisKey, newExpireTime, TimeUnit.SECONDS); }); } 注意:这里我们使用hash存储,所以续期是对整个key(hash)设置过期时间。 但是,这样存储后,我们在登录时就需要使用hset来存储: redisService.hset(redisKey, "token", token); redisService.hset(redisKey, "rememberMe", String.valueOf(rememberMe)); redisService.expire(redisKey, rememberMe ? Constants.LONG_EXPIRE_TIME : Constants.DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS); 这样,在过滤器中,我们只需要一次hgetAll操作即可获取所有信息。 总结:我们实现了一个过滤器,用于校验token和续期。主要步骤包括获取token、解析验证、检查Redis中的token、根据剩余时间和rememberMe设置进行续期。 引用说明: - 引用[2]提供了单Token方案的基本思想,包括设置较短过期时间和刷新机制。 - 引用[4]提供了在过滤器中异步续期的代码示例。 因此,我们的实现参考了这些思路。 相关问题: 1. 如何防止token被盗用? 2. 在分布式系统中,如何实现token的集中管理? 3. 如何实现token的双重验证(如加上设备信息)?</think>### Token校验续期过滤器实现 在前后端分离架构中,通过过滤器实现Token校验和续期是保障系统安全性的关键环节。以下是一个基于Java Servlet Filter的实现方案,结合JWT和Redis存储: #### 1. 核心实现逻辑 ```java import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.concurrent.TimeUnit; public class TokenAuthFilter implements Filter { private final RedisService redisService; // Redis操作服务 private final JwtUtil jwtUtil; // JWT工具类 @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; // 1. 放行登录/公开路径 if (isPublicPath(request)) { chain.doFilter(request, response); return; } // 2. 获取并校验Token String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { sendError(response, 401, "缺少有效Token"); return; } token = token.substring(7); // 去除"Bearer " // 3. 验证Token有效性 Claims claims = jwtUtil.parseToken(token); if (claims == null) { sendError(response, 401, "Token无效或过期"); return; } // 4. 检查Redis存储状态 String userId = claims.getSubject(); String redisKey = "JWT:" + userId; if (!redisService.exists(redisKey)) { sendError(response, 401, "Token已失效"); return; } // 5. 异步Token续期(不阻塞主流程) if (shouldRenewToken(claims)) { renewTokenAsync(request, userId); } // 6. 传递用户信息并放行 request.setAttribute("currentUserId", userId); chain.doFilter(request, response); } private boolean shouldRenewToken(Claims claims) { long expireTime = claims.getExpiration().getTime(); long currentTime = System.currentTimeMillis(); // 过期前5分钟内触发续期 return (expireTime - currentTime) < 5 * 60 * 1000; } private void renewTokenAsync(HttpServletRequest request, String userId) { new Thread(() -> { String rememberMe = request.getHeader("rememberMe"); long expireTime = Boolean.parseBoolean(rememberMe) ? Constants.LONG_EXPIRE : Constants.SHORT_EXPIRE; redisService.expire("JWT:" + userId, expireTime, TimeUnit.SECONDS); }).start(); } } ``` #### 2. 关键组件说明 1. **Token校验流程**: - 从`Authorization`头提取Bearer Token - 使用JWT库验证签名和过期时间$(\text{exp} \geq \text{currentTime})$ - 检查Redis是否存在对应Token(防止登出后Token被复用) 2. **续期触发条件**: - 当Token剩余有效期$t < 5\text{min}$时触发 - 根据`rememberMe`标志设置不同有效期: $$ T_{\text{new}} = \begin{cases} T_{\text{long}} & \text{if rememberMe=true} \\ T_{\text{short}} & \text{otherwise} \end{cases} $$ 3. **异步续期设计**: - 通过`new Thread()`实现非阻塞续期 - Redis续期命令:`EXPIRE key seconds` - 避免主请求线程因续期操作延迟 #### 3. 安全增强措施 1. **双重验证机制**: ```java // 在Redis存储Token时添加客户端指纹 String clientFingerprint = request.getHeader("User-Agent") + request.getRemoteAddr(); String storedToken = redisService.get(redisKey); if (!storedToken.equals(token + "|" + clientFingerprint)) { // 异常设备访问 } ``` 2. **续期频率限制**: ```java // Redis记录续期次数 String renewKey = "RENEW:" + userId; long renewCount = redisService.incr(renewKey); redisService.expire(renewKey, 1, TimeUnit.HOURS); if (renewCount > MAX_RENEW_TIMES) { // 强制重新登录 } ``` #### 4. 部署注意事项 1. **白名单配置**: ```java private boolean isPublicPath(HttpServletRequest request) { String path = request.getRequestURI(); return path.startsWith("/api/login") || path.startsWith("/api/public") || path.equals("/swagger-ui.html"); } ``` 2. **异常处理规范**: ```java private void sendError(HttpServletResponse res, int code, String msg) { res.setStatus(code); res.setContentType("application/json"); res.getWriter().write("{\"code\":" + code + ",\"message\":\"" + msg + "\"}"); } ``` #### 5. 相关配置示例 ```properties # application.properties jwt.secret=your_256bit_secret jwt.short-expire=900 # 15分钟 jwt.long-expire=259200 # 72小时 ``` 该方案实现了: 1. 无状态JWT验证有状态Redis存储结合 2. 基于剩余时间的智能续期触发 3. 异步处理避免请求阻塞 4. 设备指纹增强安全性 5. 续期频率防滥用机制[^2][^4] --- ### 相关问题 1. **如何防止Token被恶意刷新?** 需实现续期频率限制(如1小时内最多续期5次),超过阈值要求重新认证[^2] 2. **分布式环境下如何保证Token一致性?** 采用Redis Cluster存储Token,通过Redisson实现分布式锁进行续期操作 3. **前端如何处理Token自动更新?** 当前端收到401响应时,应调用`/refresh-token`接口,用refresh token换取新access token 4. **如何实现JWT的无感刷新?** 在响应头返回新Token:`response.setHeader("New-Authorization", newToken)`, 前端拦截响应自动更新 5. **Token续期时如何避免并发问题?** 使用Redis的`WATCH/MULTI`事务或分布式锁,确保续期操作的原子性[^4]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值