双Token无感刷新方案

token有效期设置问题

最近在做用户认证模块的后端功能开发,之前就有一个问题困扰了我好久,就是如何设置token的过期时间,前端在申请后端登录接口成功之后,会返回一个token值,存储在用户端本地,用户要访问后端的其他接口必须通过请求头带上这个token值,但是这个token的有效期应该设置为多少?

  1. 如果设置的太短,比如1小时,那么用户一小时之后。再访问其他接口,需要再次重新登录,对用户的体验极差
  2. 如果设置为一个星期,那么在这个时间内
      • 一旦token泄露,攻击者可长期冒充用户身份,直到token过期,服务端无法限制其访问用户数据
      • 虽然可以依赖黑名单机制,但会增加系统复杂度,还要进行系统监测
      • 如果在这段时间恶意用户利用未过期的条款持续调用后端API将会导致资源耗尽或产生巨额费用

所以有没有两者都兼顾的方案呢?

双token无感刷新方案

传统的token方案要么频繁要求用户重新登录,要么面临长期有效的安全风险

但是双token无感刷新机制,通过组合设计,在保证安全性的情况下,实现无感知的认证续期

核心设计

  1. access_token:访问令牌,有效期一般设置为15~30分钟,主要用于对后端请求API的交互
  2. refresh_token:刷新令牌,一般设置为一个星期到一个月,主要用于获取新的access_token

大致的执行流程如下

用户登录之后,后端返回access_tokenrefresh_token响应给前端,前端将两个token存储在用户本地

在用户端发起前端请求,访问后端接口,在请求头中携带上access_token

前端会对access_token的过期时间进行检测,当access_token过期前一分钟,前端通过refresh_token向后端发起请求,后端判断refresh_token是否有效,有效则重新获取新的access_token,返回给前端替换掉之前的access_token存储在用户本地,无效则要求用户重新认证

这样的话对于用户而言token的刷新是无感知的,不会影响用户体验,只有当refresh_token失效之后,才需要用户重新进行登录认证,同时,后端可以通过对用户refresh_token的管理来限制用户对后端接口的请求,大大提高了安全性

有了这个思路,写代码就简单了

ini 复制代码
@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private JwtUtils jwtUtils;

    // token过期时间
    private static final Integer TOKEN_EXPIRE_DAYS =5;
    // token续期时间

    private static final Integer TOKEN_RENEWAL_MINUTE =15;

    @Override
    public boolean verify(String refresh_token) {
        Long uid = jwtUtils.getUidOrNull(refresh_token);
        if (Objects.isNull(uid)) {
            return false;
        }
        String key = RedisKey.getKey(RedisKey.USER_REFRESH_TOKEN,uid);
        String realToken = RedisUtils.getStr(key);
        return Objects.equals(refresh_token, realToken);
    }

    @Override
    public void renewalTokenIfNecessary(String refresh_token) {
        Long uid = jwtUtils.getUidOrNull(refresh_token);
        if (Objects.isNull(uid)) {
            return;
        }
        String refresh_key = RedisKey.getKey(RedisKey.USER_REFRESH_TOKEN, uid);
        long expireSeconds = RedisUtils.getExpire(refresh_key, TimeUnit.SECONDS);
        if (expireSeconds == -2) { // key不存在,refresh_token已过期
            return;
        }
        String access_key = RedisKey.getKey(RedisKey.USER_ACCESS_TOKEN, uid);
        RedisUtils.expire(access_key, TOKEN_RENEWAL_MINUTE, TimeUnit.MINUTES);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @RedissonLock(key = "#uid")
    public LoginTokenResponse login(Long uid) {
        String refresh_key = RedisKey.getKey(RedisKey.USER_REFRESH_TOKEN, uid);
        String access_key = RedisKey.getKey(RedisKey.USER_ACCESS_TOKEN, uid);
        String refresh_token = RedisUtils.getStr(refresh_key);
        String access_token;
        if (StrUtil.isNotBlank(refresh_token)) { //刷新令牌不为空
            access_token = jwtUtils.createToken(uid);
            RedisUtils.set(access_key, access_token, TOKEN_RENEWAL_MINUTE, TimeUnit.MINUTES);
            return LoginTokenResponse.builder()
                    .refresh_token(refresh_token).access_token(access_token)
                    .build();
        }
        refresh_token = jwtUtils.createToken(uid);
        RedisUtils.set(refresh_key, refresh_token, TOKEN_EXPIRE_DAYS, TimeUnit.DAYS);
        access_token = jwtUtils.createToken(uid);
        RedisUtils.set(access_key, access_token, TOKEN_RENEWAL_MINUTE, TimeUnit.MINUTES);
        return LoginTokenResponse.builder()
                .refresh_token(refresh_token).access_token(access_token)
                .build();
    }
}}

注意事项

  1. 安全存储Refresh Token时,优先使用HttpOnly+Secure Cookie而非LocalStorage
  2. 在颁发新Access Token时,重置旧Token的生存周期(滑动过期)而非简单续期
  3. 针对高敏感操作(如支付、改密),建议强制二次认证以突破Token机制的限制

最后记住一句话 "完美的认证方案不存在,但聪明的权衡永远存在。"

如果文章中有不对的地方,欢迎大家指正。

相关推荐
步行cgn44 分钟前
Spring Boot 将配置绑定到第三方对象详解
spring boot·后端·python
码事漫谈9 小时前
多人共用一个 key,缓存命中率会不会因此降低?
后端
考虑考虑10 小时前
docker compose环境变量替换
运维·后端·自动化运维
武雄(小星Ai)11 小时前
飞书API上传26MB文件偶发失败:错误码9499与空响应体排坑实录
后端·api·排坑实录
徐小夕11 小时前
JitWord 4.0 万字分享:从协同工具到AI Word操作系统,聊聊3年产品创业史
前端·vue.js·后端
geovindu12 小时前
CSharp: Observer Pattern
开发语言·后端·观察者模式·设计模式·c#·.netcore·行为模式
Flynt13 小时前
把公司项目迁到 Spring Boot 4.0:编译通过只是开始
java·spring boot·后端
IT_陈寒13 小时前
Redis误用keys命令把生产环境搞崩了,血的教训
前端·人工智能·后端
她的男孩14 小时前
多租户和数据权限怎么共存?扒完拦截器注册链路,我找到 4 个隐蔽的坑
java·后端·架构
用户83562907805114 小时前
使用 Python 设置 Excel 页眉和页脚
后端·python