Revel 中实现 JWT 无状态登录需手动集成:登录成功后通过 Set-Cookie(HttpOnly/Secure/SameSite)安全返回 refresh_token,JSON 响应体仅返回短期 access_token;/auth/refresh 接口须校验 refresh_token、签发新 access_token 并滚动更新 refresh_token,前端调用时需携带 credentials: 'include' 且服务端配置正确 CORS 与预检支持。

Revel 框架本身不内置 JWT 或刷新令牌(refresh_token)机制,它默认依赖传统的 session-cookie 模式。要在 Revel 中实现类似现代 Web 应用的“无状态登录 + 自动刷新令牌”流程,必须手动集成 JWT,并严格区分 access_token 和 refresh_token 的生命周期与存储策略。
Revel 中如何返回 access_token 和 refresh_token
登录成功后,后端不应只返回一个 token,而应明确返回一对凭证:
-
access_token:短期有效(如 15–30 分钟),用于后续 API 请求头:Authorization: Bearer <token> -
refresh_token:长期有效(如 7 天),但**绝不能通过 JSON 响应体明文返回给前端 JavaScript**——否则极易被 XSS 窃取 - 正确做法是:在登录成功响应中,用
Set-Cookie头写入refresh_token,并带上HttpOnly、Secure、SameSite=Strict标志 - 示例 Revel 控制器代码片段(Go):
func (c App) Login() revel.Result { // ... 验证用户名密码 accessToken := generateJWT(userID, 30*time.Minute) refreshToken := generateJWT(userID, 7*24*time.Hour) // 写入 HttpOnly Cookie c.Response.Out.Header().Set("Set-Cookie", "refresh_token="+refreshToken+ "; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh") return c.RenderJSON(map[string]string{ "access_token": accessToken, "expires_in": "1800", }) }
为什么不能把 refresh_token 存 localStorage/sessionStorage
因为任何可被 JavaScript 读取的存储位置(localStorage、sessionStorage、甚至 document.cookie 字符串)都对 XSS 攻击完全开放。一旦页面存在任意一处未过滤的用户输入(比如评论、富文本、URL 参数渲染),攻击者就能执行脚本并盗走 refresh_token——等于永久获得用户身份。
- Revel 后端无法阻止前端乱存;但你可以通过文档和拦截器强制约束
- 前端必须只从
HttpOnlyCookie 中自动携带refresh_token到/auth/refresh接口,且该接口不允许 CORS 共享凭据以外的来源 - Revel 的
Intercept可用于校验请求是否来自合法域名:c.Request.Referer或Origin头
Revel 实现 /auth/refresh 接口的关键点
这个接口是刷新逻辑的核心,它必须完成三件事:校验 refresh_token、签发新 access_token、(可选)滚动更新 refresh_token。
- 从
c.Request.Cookies中提取refresh_token,而非从请求体或 query - 验证 JWT 签名、过期时间、用户绑定关系(建议在 token payload 中嵌入
user_id和fingerprint) - 若使用“滚动刷新”模式(更安全),需在服务端记录
refresh_token的使用状态(如 Redis 中存rt:<hash>并设 TTL),每次刷新后作废旧 token - 不要在响应体中返回新的
refresh_token——除非你同时更新了Set-Cookie头;否则前端无法安全接管 - 示例处理逻辑(伪代码):
func (c App) Refresh() revel.Result { cookie, err := c.Request.Cookie("refresh_token") if err != nil { return c.RenderError("no refresh token") } claims := parseAndValidateJWT(cookie.Value) if !isValidRefreshToken(claims) { return c.RenderError("invalid or revoked") } newAccessToken := generateJWT(claims.UserID, 30*time.Minute) // 滚动刷新:生成新 refresh_token,作废旧的 newRefreshToken := generateJWT(claims.UserID, 7*24*time.Hour) revokeOldRefreshToken(claims.JTI) // JTI 是唯一标识 c.Response.Out.Header().Set("Set-Cookie", "refresh_token="+newRefreshToken+"; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh") return c.RenderJSON(map[string]string{ "access_token": newAccessToken, "expires_in": "1800", }) }
前端调用 refresh 接口时的常见失败原因
即使后端逻辑写对了,前端仍可能因配置疏漏导致刷新失败,最典型的几个坑:
- fetch 或 axios 请求没带
credentials: 'include',浏览器不会发送HttpOnlyCookie -
/auth/refresh接口未开启 CORS,且未设置Access-Control-Allow-Origin为具体前端域名(不能用*配合 credentials) - Revel 默认不自动处理 OPTIONS 预检;需在
app.Init中显式注册预检路由或使用中间件透传 - 前端在 401 响应后发起刷新请求,但没阻塞后续请求队列,导致多个并发请求同时触发刷新 → 多次作废同一个
refresh_token→ 后续全部失败 - 时间不同步:Revel 服务器时间和前端设备时间偏差超过 token 过期窗口,会导致签名验证直接失败(JWT 的
exp和nbf是绝对时间戳)
真正难的不是生成两个 token,而是让 refresh_token 始终处于“不可见、不可读、不可跨域滥用”的状态。Revel 没有开箱即用的方案,每一步都要自己补全防护链:Cookie 属性、CORS 策略、JWT 载荷设计、服务端黑名单、前端拦截器重试控制——漏掉任意一环,安全性就归零。


















