1. 整体架构
核心流程:
• 无感刷新:通过 Refresh Token 自动获取新的 Access Token。
• 黑名单机制:将需要失效的 Token(如用户主动登出后的旧 Token)存入 Redis,并在验证时检查黑名单。
技术栈:
• JWT(Access Token + Refresh Token)
• Redis(存储黑名单和 Refresh Token)
• 后端框架(如 Node.js/Express、Spring Boot 等)
2. 实现步骤
2.1 服务器端设计
(1) 登录接口
用户登录成功后,生成并返回 Access Token 和 Refresh Token,同时将 Refresh Token 存入 Redis。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| app.post("/login", async (req, res) => { const { username, password } = req.body const user = await validateUser(username, password) if (!user) return res.status(401).json({ error: "Invalid credentials" })
const accessToken = jwt.sign({ userId: user.id }, "ACCESS_TOKEN_SECRET", { expiresIn: "15m", })
const refreshToken = jwt.sign({ userId: user.id }, "REFRESH_TOKEN_SECRET", { expiresIn: "7d", })
await redisClient.set( `refresh_token:${user.id}`, refreshToken, "EX", 7 * 24 * 60 * 60 )
res.json({ accessToken, refreshToken }) })
|
(2) 刷新接口
通过 Refresh Token 获取新的 Access Token,并验证 Redis 中的有效性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| app.post("/refresh-token", async (req, res) => { const { refreshToken } = req.body try { const decoded = jwt.verify(refreshToken, "REFRESH_TOKEN_SECRET") const userId = decoded.userId
const storedRefreshToken = await redisClient.get(`refresh_token:${userId}`) if (refreshToken !== storedRefreshToken) { return res.status(401).json({ error: "Invalid refresh token" }) }
const newAccessToken = jwt.sign({ userId }, "ACCESS_TOKEN_SECRET", { expiresIn: "15m", })
res.json({ accessToken: newAccessToken }) } catch (error) { res.status(401).json({ error: "Refresh token expired or invalid" }) } })
|
(3) 黑名单机制
用户登出或需要撤销 Token 时,将旧 Access Token 加入 Redis 黑名单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| app.post("/logout", async (req, res) => { const { accessToken } = req.body const decoded = jwt.decode(accessToken) const exp = decoded.exp
const ttl = exp - Math.floor(Date.now() / 1000)
if (ttl > 0) { await redisClient.set(`blacklist:${accessToken}`, "revoked", "EX", ttl) }
const userId = decoded.userId await redisClient.del(`refresh_token:${userId}`)
res.json({ message: "Logged out successfully" }) })
|
(4) 中间件:验证 Access Token
在每次请求中验证 Access Token 是否在黑名单中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const authMiddleware = async (req, res, next) => { const authHeader = req.headers.authorization const accessToken = authHeader?.split(" ")[1]
if (!accessToken) { return res.status(401).json({ error: "No token provided" }) }
try { const isBlacklisted = await redisClient.exists(`blacklist:${accessToken}`) if (isBlacklisted) { return res.status(401).json({ error: "Token revoked" }) }
const decoded = jwt.verify(accessToken, "ACCESS_TOKEN_SECRET") req.userId = decoded.userId next() } catch (error) { res.status(401).json({ error: "Invalid or expired token" }) } }
|
2.2 客户端设计
客户端需在 Access Token 过期前自动刷新,并在登出时触发黑名单机制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| axios.interceptors.request.use(async (config) => { const accessToken = localStorage.getItem("accessToken") const refreshToken = localStorage.getItem("refreshToken")
const decoded = jwt.decode(accessToken) const isExpired = decoded.exp * 1000 < Date.now() + 5 * 60 * 1000
if (isExpired) { try { const response = await axios.post("/refresh-token", { refreshToken }) const newAccessToken = response.data.accessToken localStorage.setItem("accessToken", newAccessToken) config.headers.Authorization = `Bearer ${newAccessToken}` } catch (error) { window.location.href = "/login" } }
return config })
|
3. Redis 黑名单优化
- 自动清理:通过设置
EX
参数,让 Redis 自动删除过期的黑名单 Token。
- 内存管理:定期清理无效数据,避免内存占用过多。
- 分片存储:使用
blacklist:${token}
的键名格式,避免单个 Key 过大。
4. 安全性增强
- Refresh Token 存储:
• 使用 httpOnly
Cookie 存储 Refresh Token,防止 XSS 攻击。
• 每次刷新后生成新的 Refresh Token,旧 Token 立即失效(需更新 Redis)。
- Token 轮换:
• 每次刷新 Access Token 时,生成新的 Refresh Token 并替换 Redis 中的旧值。
- 密钥管理:
• 使用强随机密钥(如 ACCESS_TOKEN_SECRET
和 REFRESH_TOKEN_SECRET
)。
• 定期轮换密钥,降低泄露风险。
5. 流程图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| 用户登录 │ ├─ 生成 Access Token(15分钟)和 Refresh Token(7天) │ ├─ 存储 Refresh Token 到 Redis(关联用户ID) │ └─ 返回 Token 给客户端
用户请求受保护接口 │ ├─ 验证 Access Token 是否在黑名单 │ ├─ 若在黑名单 → 拒绝请求 │ └─ 若有效 → 允许访问
Access Token 过期前 │ ├─ 客户端自动调用刷新接口 │ ├─ 验证 Refresh Token(检查 Redis) │ ├─ 生成新 Access Token │ └─ 更新客户端存储
用户登出 │ ├─ 将当前 Access Token 加入 Redis 黑名单 │ └─ 删除 Redis 中的 Refresh Token
|