网络请求时关于cookie或token失效的解决方案

当一次网络请求(比如说请求购物车的数据,这时是需要验证用户身份的标识的,例如cookie或者token)
想到的三种方法:
1.最开始没用rxjava之前就是用的这种,但是感觉实在累赘。当token失效后重新请求登录接口,当登录成功后通知原先的Activity重新加载数据。这样需要对每个接口都进行token是否失效的判断。

2.使用Intercept(参考这篇文章,但是Okhttpclien3.0删除了ErrorHandler)onErrorResumeNext操作符实现app与服务器间token机制
http://blog.csdn.net/johnny901114/article/details/51533586),在intercept方法中拿到返回的json字符串,然后判断token是否失效,如果失效,那么重新登录,但是这儿需要注意的是因为需要继续往下传递请求,登录接口的请求必须是同步的!(ps:后来朋友想了另一个办法,在intercept中抛出异常,这儿就需要用到第三种方法了)

3.使用retryWhen操作符
(关于retryWhen这篇博客讲的非常好http://www.jianshu.com/p/023a5f60e6d0
最开始我的理解有问题。我的代码是这样的。

ApiClient.getInstance()
    .getUserCourse(cookies, "1", "1")
                .flatMap(new Func1<MyCourse, Observable<MyCourse>>() {
                    @Override
                    public Observable<MyCourse> call(MyCourse myCourse) {
                        if (myCourse.status == 205) {
                            return Observable.error(new Exception("kkkk"));
                        }
                        return Observable.just(myCourse);
                    }
                })
                .retryWhen(new RetryWithDelay(3, 1000))
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<MyCourse>() {
                    @Override
                    public void call(MyCourse response) {
                        fillData(response);
                    }
                }, new Action1<Throwable>() {
                    @Override
                    public void call(Throwable throwable)                    Log.i("===========k",throwable.toString());
                    }
                });

然后我在retryWhen中进行了重新登录获取到了最新的cookie,结果显示没有获取到正确的数据,我猜,难道retryWhen只重试了flatMap?当然不是,我抓包得到的结果是,重新进行了网络请求,但是并没有使用新的cookie,为什么呢,cookies作为一个成员变量,他的值变化了啊!
然后我写了个just的例子测试了下!

str = "aaa";
        Observable.just(str).map(new Func1<String, String>() {
            @Override
            public String call(String s) {
                Log.i("====", "s == " + s);
                if ("aaa".equals(s)) throw new RuntimeException(s);
                return s + "123";
            }
        }).retryWhen(new Func1<Observable<? extends Throwable>, Observable<?>>() {
            @Override
            public Observable<?> call(Observable<? extends Throwable> observable) {
                return observable.zipWith(Observable.range(1, 4), new Func2<Throwable, Integer, Integer>() {
                    @Override
                    public Integer call(Throwable throwable, Integer i) {
                        str = "ggg";
                        return i;
                    }
                }).flatMap(new Func1<Integer, Observable<? extends Long>>() {
                    @Override
                    public Observable<? extends Long> call(Integer retryCount) {
                        return Observable.timer(1, TimeUnit.SECONDS);
                    }
                });
            }
        }).subscribe(new Action1<String>() {
            @Override
            public void call(String s) {
                Log.i("====k", "s = " + s);
            }
        }, new Action1<Throwable>() {
            @Override
            public void call(Throwable throwable) {
                Log.i("====", "throwable = " + throwable.getMessage());
            }
        });

结果是
aaa
aaa
aaa

what?
为啥啊?为什么后面不打印ggg呢?
看这里吧。
关于retryWhen的issue
https://github.com/ReactiveX/RxJava/issues/4840
也就说retryWhen每次重试的都是Source Observable!为了保证使用最新的cookie,使用defer操作符,原理类似于fromCallable.
再来说第二条提到的那个抛出异常的方法。
具体实现就是Intercept和RetryWhen结合,在Intercept中进行token是否失效的判断,如果token失效那么就直接抛出异常,然后在retryWhen中进行重新登录,并给token设置最新的值。这样就避免了同步请求的问题。不过需要注意:

1.因为重新登录是异步请求,所以需要对retryWhen中的重试进行限制,即重新请求原先接口需要延迟(用timer操作符)

2.对重新登录次数进行限制
3.最好自定义抛出的异常,这样方便在Subscriber的onError方法或者retryWhen中进行判断是否是token失效,万一后续还有其他问题需要在Intercept中处理呢。

4.最大的弊端是对所有的接口都进行了token是否失效的判断(因为Intercept会是全局的),所以在Intercept中对token是否失效那儿的判断可以自行处理,比如说用户是否登录?这样的判断。

<think>嗯,用户之前问了关于JWT Token续期的问题,现在接着问退出登录如何让Token失效。JWT本身是无状态的,所以传统的基于服务端Session的注销方式不适用。用户可能需要知道几种常见的失效方法。 首先,我需要回忆JWT的特性。因为JWT的有效期通常由其中的过期间(exp)字段控制,服务端不会保存Token的状态,所以单纯的删除客户端Token并不能真正让它失效,除非等到自然过期。但用户可能希望在退出立即让Token失效,这就需要额外的机制。 接下来,可能的解决方案包括:维护一个黑名单、使用短期Token、改变签名密钥、者结合数据库存储状态。其中黑名单是比较常见的做法,但需要考虑存储和性能问题。另外,修改密钥会影响所有已发放的Token,可能不太合适。而短期Token虽然减少了风险,但用户可能需要立即失效的能力。 然后,用户可能的应用场景是什么?如果是高安全要求的系统,可能需要更严格的措施,比如每次注销都记录黑名单,并且每次验证Token检查黑名单。但如果是普通应用,可能使用黑名单加上较短的过期间即可。 还要考虑实现细节,比如使用Redis存储黑名单,设置合理的TTL,避免内存泄漏。同,在Spring Security中如何配置TokenStore,添加黑名单检查的逻辑。例如,在资源服务器的配置中,自定义Token增强器过滤器,在验证Token查询黑名单。 另外,用户可能需要代码示例,比如如何实现黑名单的存储和验证。需要提供具体的Java代码片段,展示如何用Spring Security和Redis来实现。同,要注意处理并发和性能问题,比如使用高效的存储结构,设置自动过期等。 还有,用户可能关心安全性,比如黑名单存储的加密,者如何防止恶意用户频繁注销导致存储膨胀。需要建议设置合理的黑名单过期间,比如稍长于Token的最长有效期,避免长期存储。 最后,需要比较不同方法的优缺点,帮助用户根据实际情况选择。例如,黑名单实现简单但增加存储开销,而短期Token无需存储但可能不够及。用户可能需要根据系统需求和资源情况做出权衡。</think>在JWT无状态特性下实现立即失效,可通过以下方案实现: ### 一、核心实现方案(黑名单机制) ```java // 1. 创建Redis黑名单存储 @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; } // 2. 实现黑名单服务 @Service public class TokenBlacklistService { @Autowired private RedisTemplate<String, Object> redisTemplate; public void addToBlacklist(String token, long expiresIn) { // 计算剩余有效间 long ttl = getRemainingValidity(token) > 0 ? getRemainingValidity(token) : 60; redisTemplate.opsForValue().set("blacklist:" + token, true, ttl, TimeUnit.SECONDS); } private long getRemainingValidity(String token) { Claims claims = Jwts.parser().parseClaimsJwt(token).getBody(); return claims.getExpiration().getTime() - System.currentTimeMillis(); } } // 3. 登出接口实现 @PostMapping("/logout") public ResponseEntity<?> logout(@RequestHeader("Authorization") String authHeader) { String token = authHeader.replace("Bearer ", ""); tokenBlacklistService.addToBlacklist(token); return ResponseEntity.ok().build(); } // 4. 配置全局安全验证 @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .oauth2ResourceServer() .jwt() .jwtAuthenticationConverter(jwtAuthenticationConverter()) .and() .and() .addFilterBefore(new BlacklistFilter(), BearerTokenFilter.class); } private JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter(); converter.setAuthorityPrefix(""); JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter(); jwtConverter.setJwtGrantedAuthoritiesConverter(converter); return jwtConverter; } } // 5. 黑名单过滤器 public class BlacklistFilter extends OncePerRequestFilter { @Autowired private RedisTemplate<String, Object> redisTemplate; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = extractToken(request); if (token != null && redisTemplate.hasKey("blacklist:" + token)) { response.sendError(HttpStatus.UNAUTHORIZED.value(), "Token revoked"); return; } filterChain.doFilter(request, response); } private String extractToken(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } } ``` ### 二、增强型方案对比 | 方案 | 实现复杂度 | 性能影响 | 失效性 | 存储开销 | |--------------------|------------|----------|------------|----------| | 黑名单机制 | ★★☆ | 低 | 实 | 中 | | 短期Token(5分钟) | ★☆☆ | 无 | 延迟 | 无 | | 密钥轮换 | ★★★ | 高 | 实 | 无 | | 数据库状态位 | ★★☆ | 中 | 实 | 高 | 推荐组合方案:**黑名单 + 短有效期(30分钟) + 自动续期** ### 三、安全增强配置 1. Redis安全配置 ```yaml spring: redis: ssl: true password: ${REDIS_PASSWORD} timeout: 5000 lettuce: pool: max-active: 50 max-idle: 10 ``` 2. JWT签名强化 ```java @Bean public JwtDecoder jwtDecoder() { return NimbusJwtDecoder.withSecretKey( new SecretKeySpec(Base64.getDecoder().decode(secret), "HmacSHA512") ).build(); } ``` 3. 防御性代码示例(防重复登出) ```java public void addToBlacklist(String token, long expiresIn) { if (redisTemplate.opsForValue().setIfAbsent("blacklist:" + token, true, expiresIn, TimeUnit.SECONDS)) { log.info("Token blacklisted: {}", token); } else { log.warn("Duplicate logout attempt for token: {}", token); } } ``` ### 四、运维监控建议 1. Prometheus监控指标配置 ```java @Bean public MeterRegistryCustomizer<MeterRegistry> metrics() { return registry -> { registry.gauge("jwt.blacklist.size", redisTemplate.keys("blacklist:*").size()); }; } ``` 2. 黑名单自动清理策略 ```java @Scheduled(fixedRate = 3600000) // 每小清理 public void cleanExpiredEntries() { Set<String> keys = redisTemplate.keys("blacklist:*"); keys.forEach(key -> { if (redisTemplate.getExpire(key) <= 0) { redisTemplate.delete(key); } }); } ``` ### 五、前端配合方案 ```javascript // 前端登出逻辑增强 function logout() { // 1. 清除本地存储 localStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); // 2. 调用后端登出接口 fetch('/api/logout', { method: 'POST', headers: { 'Authorization': `Bearer ${getAccessToken()}` } }); // 3. 强制页面刷新(清除内存中的残留) window.location.reload(true); } // 4. 添加心跳检测(每5分钟检查登录状态) setInterval(() => { fetch('/api/check-token', { headers: { 'Authorization': `Bearer ${getAccessToken()}` } }).catch(() => { localStorage.clear(); window.location.href = '/login'; }); }, 300000); ``` 建议采用Redis黑名单方案配合以下最佳实践: 1. 设置JWT有效期不超过1小 2. 每次请求携带请求ID并校验唯一性 3. 重要操作要求二次认证 4. 监控黑名单增长率(异常登出预警) 5. 使用HTTP Only + Secure的Cookie存储Refresh Token 这种方案在保证安全性的同,兼顾系统性能,适合中大型分布式系统。对于更高安全要求的场景,可结合设备指纹识别IP绑定技术增强校验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值