【Java后端技术亮点】双令牌模式(Access Token + Refresh Token),彻底搞懂认证机制
2026/5/31 13:03:52 网站建设 项目流程

写在前面:做过前后端分离项目的同学应该都遇到过这个问题——Token过期了怎么办?是让用户重新登录?还是静默刷新?重新登录用户体验差,静默刷新又担心安全性。今天聊聊双令牌模式,这是Sa-Token、Spring Security OAuth2等框架都在用的经典方案。

文章目录

    • 一、为什么需要双令牌?
      • 1.1 单Token方案的痛点
      • 1.2 双Token如何解决?
    • 二、双令牌的核心实现
      • 2.1 登录时返回双Token
      • 2.2 Token刷新接口
      • 2.3 Token失效(踢人/改密)
    • 三、前端如何配合?
      • 3.1 Token存储策略
      • 3.2 Token过期自动刷新(拦截器)
    • 四、预判问题与解答
      • Q1:Refresh Token泄露了怎么办?
      • Q2:双Token比单Token多了几次请求,性能会不会有问题?
      • Q3:JWT还是Session?双Token适合哪种?
      • Q4:Access Token要不要存Redis?
      • Q5:刷新时要不要返回新的Refresh Token?
    • 五、与Sa-Token的对比
    • 六、面试高频考点
      • 面试官问:请介绍一下双令牌模式的设计思路
      • 面试官问:Refresh Token泄露怎么办?
      • 面试官问:双Token和单Token各有什么优缺点?
      • 面试官问:Access Token和Refresh Token存储位置有什么讲究?
    • 七、总结
    • 参考资料

一、为什么需要双令牌?

1.1 单Token方案的痛点

踩坑提醒:很多新手项目只用一个Token,有效期设7天。看起来用户体验好,但一旦Token泄露,攻击者有7天时间冒用用户身份!

单Token方案的两个极端:

有效期问题
短(30分钟)用户频繁登录,体验差
长(7天)Token泄露风险大,无法主动失效

实际场景:你做的App,用户登录后7天内免登录。某天用户手机丢了,捡到手机的人打开App,直接就是登录状态。用户改密码?没用,Token还在有效期内。

1.2 双Token如何解决?

核心思路:用两个Token分工

  • Access Token:有效期短(30分钟),用于接口访问
  • Refresh Token:有效期长(7天),用于刷新Access Token
用户登录 ↓ 返回 Access Token(30分钟) + Refresh Token(7天) ↓ 前端用 Access Token 调接口 ↓ Access Token 过期(30分钟后) ↓ 前端用 Refresh Token 换新的 Access Token ↓ 用户无感知,继续使用

经验之谈:Access Token泄露只影响30分钟,Refresh Token泄露可以主动失效(存数据库,删了就失效)。这就是"风险隔离"的设计思想。


二、双令牌的核心实现

2.1 登录时返回双Token

publicclassAuthController{@PostMapping("/login")publicResultlogin(@RequestBodyLoginDTOdto){// 1. 校验用户名密码Useruser=userService.checkLogin(dto.getUsername(),dto.getPassword());// 2. 生成 Access Token(短有效期)StringaccessToken=JwtUtil.createToken(user.getId(),user.getUsername(),30*60*1000L// 30分钟);// 3. 生成 Refresh Token(长有效期)StringrefreshToken=JwtUtil.createToken(user.getId(),"refresh",7*24*60*60*1000L// 7天);// 4. Refresh Token 存数据库(支持主动失效)tokenService.saveRefreshToken(user.getId(),refreshToken,7*24*60*60);// 5. 返回双TokenreturnResult.success(Map.of("accessToken",accessToken,"refreshToken",refreshToken,"accessExpire",30*60,"refreshExpire",7*24*60*60));}}

关键点说明

  • Access Token不需要存数据库,纯内存验证(JWT自包含用户信息)
  • Refresh Token必须存数据库,否则无法主动失效
  • 两个Token用不同的标识区分(payload里加个type字段)

2.2 Token刷新接口

@PostMapping("/refresh")publicResultrefresh(@RequestBodyRefreshDTOdto){StringrefreshToken=dto.getRefreshToken();// 1. 校验 Refresh Token 格式和签名if(!JwtUtil.verify(refreshToken)){thrownewBizException("Refresh Token无效");}// 2. 校验 Refresh Token 是否在数据库中(可能已被删除)LonguserId=JwtUtil.getUserId(refreshToken);if(!tokenService.existsRefreshToken(userId,refreshToken)){thrownewBizException("Refresh Token已失效,请重新登录");}// 3. 校验 Token 类型(防止用 Access Token 来刷新)Stringtype=JwtUtil.getType(refreshToken);if(!"refresh".equals(type)){thrownewBizException("Token类型错误");}// 4. 生成新的 Access TokenUseruser=userService.getById(userId);StringnewAccessToken=JwtUtil.createToken(user.getId(),user.getUsername(),30*60*1000L);// 5. Refresh Token 轮转(可选:每次刷新生成新的Refresh Token)StringnewRefreshToken=JwtUtil.createToken(userId,"refresh",7*24*60*60*1000L);tokenService.updateRefreshToken(userId,refreshToken,newRefreshToken);returnResult.success(Map.of("accessToken",newAccessToken,"refreshToken",newRefreshToken,"accessExpire",30*60));}

踩坑提醒:一定要校验Token类型!否则用户拿Access Token也能刷新,那双Token的意义就没了。

2.3 Token失效(踢人/改密)

@PostMapping("/logout")publicResultlogout(){LonguserId=SecurityUtil.getUserId();// 删除数据库中的 Refresh TokentokenService.deleteRefreshToken(userId);// Access Token 不需要删,等它自然过期就行returnResult.success("退出成功");}@PostMapping("/changePassword")publicResultchangePassword(@RequestBodyChangePwdDTOdto){LonguserId=SecurityUtil.getUserId();userService.changePassword(userId,dto.getNewPassword());// 改密后踢掉所有设备tokenService.deleteAllRefreshToken(userId);returnResult.success("密码修改成功,请重新登录");}

关键点:改密码后必须删除所有Refresh Token,否则用户改了密码,旧Token还能继续刷新。


三、前端如何配合?

3.1 Token存储策略

存储位置Access TokenRefresh Token
localStorage❌ 不安全❌ 不安全
sessionStorage✅ 相对安全❌ 关闭浏览器就没了
内存变量✅ 最安全✅ 最安全
Cookie(HttpOnly)✅ 防XSS✅ 防XSS

经验之谈:我推荐的做法是Access Token放内存(Vue的data/React的state),Refresh Token放Cookie(HttpOnly + Secure)。这样XSS拿不到Token,CSRF也难以利用(因为CSRF拿不到Cookie内容)。

3.2 Token过期自动刷新(拦截器)

// axios 拦截器示例axios.interceptors.response.use(response=>response,asyncerror=>{const{response,config}=error;// Access Token 过期(401)if(response?.status===401&&!config._retry){config._retry=true;// 防止无限重试try{// 用 Refresh Token 换新的 Access Tokenconst{data}=awaitaxios.post('/auth/refresh',{refreshToken:getRefreshToken()});// 更新本地 TokensetAccessToken(data.accessToken);if(data.refreshToken){setRefreshToken(data.refreshToken);}// 重试原请求config.headers.Authorization=`Bearer${data.accessToken}`;returnaxios.request(config);}catch(e){// Refresh Token 也失效了,跳转登录页router.push('/login');returnPromise.reject(e);}}returnPromise.reject(error);});

四、预判问题与解答

Q1:Refresh Token泄露了怎么办?

问题场景:黑客拿到了用户的Refresh Token,能不能一直刷新Access Token?

解答

  1. Refresh Token轮转:每次刷新生成新的Refresh Token,旧的立即失效。这样黑客拿到的Token可能已经被轮转掉了。
  2. 设备绑定:Refresh Token绑定设备指纹(IP+UA+DeviceId),刷新时校验设备信息。
  3. 单设备登录:一个用户只允许一个Refresh Token,新登录踢掉旧Token。
  4. 异常检测:刷新频率异常(如1分钟刷新10次)、异地刷新,触发告警或自动失效。
// 设备绑定示例@PostMapping("/refresh")publicResultrefresh(@RequestBodyRefreshDTOdto,HttpServletRequestrequest){StringdeviceFingerprint=generateFingerprint(request);if(!tokenService.checkDevice(userId,refreshToken,deviceFingerprint)){tokenService.deleteRefreshToken(userId);// 异常,踢掉thrownewBizException("设备异常,请重新登录");}// ...}

Q2:双Token比单Token多了几次请求,性能会不会有问题?

解答

  • 刷新请求只在Access Token过期时发生(30分钟一次)
  • 刷新请求很轻量(JWT验证 + 数据库查询)
  • 相比用户重新登录输入密码,刷新请求几乎无感知

性能对比

方案7天内的请求次数用户感知
单Token(7天有效)0次额外请求无感知,但不安全
单Token(30分钟有效)用户手动登录约16次每次都要输入密码
双Token自动刷新约16次无感知

Q3:JWT还是Session?双Token适合哪种?

解答:双Token模式更适合JWT方案

方案Access TokenRefresh Token
JWT存JWT(自包含信息)存数据库(支持失效)
Session存SessionId存数据库

Session方案本身就可以在服务端失效,不需要双Token。双Token的价值主要体现在JWT的无状态特性上。

Q4:Access Token要不要存Redis?

解答不建议存Redis,原因:

  1. JWT自包含用户信息,存Redis就失去了无状态的优势
  2. 每次请求都要查Redis,性能下降
  3. Access Token有效期短(30分钟),泄露风险可控

例外情况

  • 需要实时踢人(删Redis立即生效)
  • 需要控制并发登录数

Q5:刷新时要不要返回新的Refresh Token?

解答建议返回,这叫"Token轮转"。

策略优点缺点
不轮转实现简单Refresh Token泄露后7天有效
轮转泄露风险低(旧Token立即失效)实现稍复杂

经验之谈:我建议轮转。实现复杂度增加不多,安全性提升明显。


五、与Sa-Token的对比

Sa-Token框架内置了双Token模式,看看它是怎么做的:

// Sa-Token 配置@ConfigurationpublicclassSaTokenConfig{@BeanpublicStpInterfacestpInterface(){returnnewStpInterfaceImpl();}}// 登录@PostMapping("/login")publicResultlogin(@RequestBodyLoginDTOdto){// Sa-Token 一行代码搞定登录StpUtil.login(userId);// 获取 TokenStringtokenValue=StpUtil.getTokenValue();returnResult.success(Map.of("token",tokenValue));}// Sa-Token 自动支持 Token 续期// 配置文件设置:// token-style=uuid// token-active-timeout=1800 (30分钟活跃超时)// token-timeout=604800 (7天绝对超时)

Sa-Token的"活跃超时"和"绝对超时"本质上就是双Token的思想:

  • 活跃超时 = Access Token有效期
  • 绝对超时 = Refresh Token有效期

六、面试高频考点

面试官问:请介绍一下双令牌模式的设计思路

参考答案

双令牌模式是解决"安全性"和"用户体验"矛盾的经典方案。

核心思路是用两个Token分工:

  • Access Token负责接口访问,有效期短(30分钟),泄露风险可控
  • Refresh Token负责刷新Access Token,有效期长(7天),存数据库支持主动失效

工作流程:

  1. 登录时返回双Token
  2. 前端用Access Token调接口
  3. Access Token过期后,前端用Refresh Token静默刷新
  4. 用户无感知,除非Refresh Token也失效(才需要重新登录)

关键设计点:

  • Refresh Token存数据库,支持踢人、改密失效
  • Token类型校验,防止用Access Token刷新
  • Token轮转,每次刷新生成新Refresh Token

面试官问:Refresh Token泄露怎么办?

参考答案

多层防护:

  1. Token轮转:每次刷新生成新Token,旧Token立即失效
  2. 设备绑定:绑定IP+UA+设备指纹,异常设备拒绝刷新
  3. 单设备登录:新登录踢掉旧Token
  4. 异常检测:刷新频率异常、异地刷新触发告警
  5. 有效期控制:Refresh Token有效期不要太长(建议7天以内)

面试官问:双Token和单Token各有什么优缺点?

参考答案

方案优点缺点
单Token(短)安全性好用户频繁登录,体验差
单Token(长)用户体验好泄露风险大,无法主动失效
双Token兼顾安全和体验实现稍复杂,多几次刷新请求

实际项目中,前后端分离、移动端App推荐双Token;传统Web应用用Session就够了。


面试官问:Access Token和Refresh Token存储位置有什么讲究?

参考答案

Access Token

  • 放内存(Vue/React状态),XSS拿不到
  • 不放localStorage(XSS可读取)

Refresh Token

  • 放HttpOnly Cookie,XSS拿不到,CSRF难以利用
  • 或放数据库,前端只存TokenId

为什么不放localStorage

  • XSS攻击可以读取localStorage
  • 一旦被XSS,Token全泄露

七、总结

双令牌模式的核心价值:

  1. 风险隔离:Access Token泄露只影响30分钟
  2. 用户体验:Token过期无感知刷新
  3. 可控失效:Refresh Token存数据库,支持踢人/改密
  4. Token轮转:每次刷新生成新Token,进一步降低泄露风险

一句话总结:双Token把"长期有效"的风险转移到了可主动失效的Refresh Token上,实现了安全性和用户体验的平衡。


参考资料

  1. Sa-Token官方文档 - Token有效期详解
  2. RFC 6749 - OAuth 2.0 Authorization Framework
  3. JWT最佳实践 - RFC 8725

互动话题:你在项目中用的是单Token还是双Token?有没有遇到过Token泄露的问题?Refresh Token你是存数据库还是Redis?欢迎在评论区分享你的实践经验!

如果这篇文章对你有帮助,欢迎点赞、收藏!关注我,后续会继续分享更多Java后端技术亮点 👇


本文为【Java后端技术亮点】系列第1篇,持续更新中…

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询