双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机制的限制

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

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

相关推荐
IT_陈寒1 小时前
Redis内存爆了,原来我漏掉了这个致命配置
前端·人工智能·后端
fliter2 小时前
最后一块拼图:用 bitvec 构造 IPv4 包,真正做出自己的 Ping
后端
fliter3 小时前
用 Rust 解析并生成 ICMP 包:checksum、nom 与 cookie-factory
后端
蝎子莱莱爱打怪3 小时前
XZLL-IM干货系列 03|消息 ID 设计:一个 UUID 搞不定的事,我用两个 ID 解决了
后端·面试·开源
fliter3 小时前
从 panic 到 Result:用 Rust 重新整理一个 ping 项目的错误处理
后端
森蓝情丶4 小时前
我给 AI 搭了个法庭:一个前端仔的 LangGraph 实战全记录
前端·后端
JensCS猿4 小时前
从 Spring Boot 回看 SSM 框架:手动挡与自动挡的驾驶哲学
后端
爱勇宝4 小时前
干了近 8 年,一夜之间被裁:AI 时代,程序员最该害怕的不是 AI
前端·后端·程序员
科米米4 小时前
嵌入式日志模块
后端
血小溅4 小时前
三大 AI 编码框架深度对比:GSD vs OpenSpec vs Superpowers
人工智能·后端