JWT认证中HttpOnly Cookie存双令牌,刷新令牌作用存疑
核心解决方案:用Cookie属性控制令牌携带场景
你遇到的核心问题是没利用Cookie的Path属性区分两个令牌的使用场景,导致每次请求都同时携带access和refresh token,完全浪费了双令牌模式的安全优势。通过合理配置Cookie属性,可以实现正常请求仅发送access token,仅刷新操作才携带refresh token,同时保证两个令牌的安全性。
具体实现步骤
1. 后端(Express)签发令牌的Cookie配置
用户登录验证通过后,分别设置两个HttpOnly Cookie:
- Access Token Cookie:
- 配置:
HttpOnly: true, Secure: true, SameSite: 'Strict', Path: '/', MaxAge: 15 * 60 * 1000(15分钟有效期) - 作用:所有正常接口请求自动携带,用于身份验证
- 配置:
- Refresh Token Cookie:
- 配置:
HttpOnly: true, Secure: true, SameSite: 'Strict', Path: '/refresh', MaxAge: 7 * 24 * 60 * 60 * 1000(7天有效期) - 额外处理:后端将refresh token的哈希值(用bcrypt等算法加密)存储到数据库/Redis,关联用户ID;前端只持有明文refresh token的Cookie,但无法通过JS读取
- 配置:
2. 前端(Angular)逻辑处理
- 正常接口请求:无需手动处理令牌,浏览器会自动携带access token Cookie
- 过期处理:当接口返回401状态码(access token过期),调用
/refresh接口,此时浏览器会自动携带refresh token Cookie(因为请求路径匹配/refresh) - 刷新失败:若
/refresh返回401(refresh token过期或无效),则引导用户重新登录
3. 后端接口验证逻辑
- 普通受保护接口:解析请求中的access token Cookie,验证签名和有效期,通过则放行
/refresh接口:- 解析请求中的refresh token Cookie
- 从数据库/Redis中取出对应用户的refresh token哈希值
- 对比明文refresh token与哈希值,验证通过后签发新的access token(更新对应Cookie)
- 若验证失败,返回401,触发前端登录流程
你的疑问解答
“有人建议将refresh token存在后端,但前端如何发起刷新请求?”
这里的“存在后端”指的是后端存储refresh token的哈希值(而非前端不持有令牌),前端依然通过HttpOnly Cookie持有明文refresh token,但由于设置了Path=/refresh,只有调用刷新接口时才会被携带。前端发起刷新请求只需调用/refresh即可,无需手动传递令牌,浏览器会自动处理。“双令牌都存HttpOnly Cookie,每次请求都携带,和只用一个令牌有何区别?”
完全没有优势,甚至风险更高。双令牌模式的核心是通过短生命周期的access token降低泄露影响,而refresh token仅在必要时使用。如果每次请求都携带两个令牌,一旦被中间人捕获,攻击者同时拿到两个令牌,相当于拥有了长期访问权限,和使用一个长生命周期令牌的风险一致,完全浪费了双令牌的设计意义。“access token存普通Cookie、refresh token存HttpOnly Cookie,虽能分开使用,但access token易受JS注入攻击。”
这个判断完全正确,普通Cookie(非HttpOnly)可被前端JS读取,XSS攻击时会直接被窃取,进而冒充用户访问所有接口。因此access token必须存HttpOnly Cookie,彻底杜绝JS读取的可能。
内容的提问来源于stack exchange,提问作者Kolopox

