双 Token
什么是 Token
Token(令牌)是在网络通信中用于验证身份和授权访问的一种机制。它是一串随机生成的字符串,用于标识用户的身份信息和权限。服务器可以根据令牌对用户进行身份鉴别,确保请求的合法性。令牌可以包含用户的权限信息,服务器可以根据令牌中的权限信息进行授权访问控制。令牌可以具有加密和签名等安全机制,保证信息的机密性和完整性。这样即使令牌被截获,也不能被篡改或者伪造。
为什么需要双 Token
假如 token 没有过期时间或者过期很长,那么显然 token 被劫持还是不安全的,token 就失去了意义。
所以这时候大家肯定都想:那么把 token 过期时间设置的短一点就行啦?
是的,一般来讲 accessToken 的过期时间应该要短一点,但是这时候对于用户来讲就麻烦了。
因为 token 过期就意味着要重新登录,想象下你正浏览的好好的,突然让你掉线了并且要求你重新登录,心里肯定是想骂人的。
什么时候需要用户重新登录
主要有三种情况:
- 用户长时间无操作,也可以定义为不活跃用户,就会被自动踢下,自动重定向到登录页面,超时时间可以自定义设置
- token 失效,通常是双 token 都失效后,会要求重新登录获取新的双 token
- 当检测到有风险的时候,可以要求重新登录,获取 token
双 Token 机制详解
因此这时候就可以使用双 token 的设计,当两个 token 都过期了再要求用户重新登录。对于 refreshToken,它只用来获取 accessToken,不会频繁被用于请求;对于 accessToken,它过期时间非常短,即使被拦截了解密也需要时间,而 token 本身也很快过期,因此这样的设计更加安全。
Token 类型对比
| 特性 |
accessToken |
refreshToken |
| 用途 |
访问受保护资源 |
刷新 accessToken |
| 过期时间 |
短(15-30分钟) |
长(7-30天) |
| 使用频率 |
频繁(每次请求) |
低频(仅在 accessToken 过期时) |
| 存储位置 |
内存/localStorage |
HttpOnly Cookie |
| 泄露风险 |
高(频繁传输) |
低(仅特定接口使用) |
双 Token 工作流程
1 2 3 4 5 6 7 8 9 10 11
| 用户登录 → 服务器返回 accessToken + refreshToken ↓ 携带 accessToken 请求接口 ↓ accessToken 有效 → 正常返回数据 ↓ accessToken 过期 → 携带 refreshToken 请求刷新接口 ↓ refreshToken 有效 → 返回新的 accessToken ↓ refreshToken 过期 → 跳转登录页,要求重新登录
|
响应拦截器处理 Token 过期
推荐在响应拦截器中拦截,后端判断 token 返回过期后,调用刷新 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 25 26
| http.interceptors.request.use( (config) => { config.headers['token'] = Vue.cookie.get('token') config.headers['language'] = localStorage.getItem('language') return config }, (error) => { return Promise.reject(error) }, )
http.interceptors.response.use( (response) => { if (response.data && response.data.code === 401) { clearLoginInfo() router.push({ name: 'login' }) } return response }, (error) => { return Promise.reject(error) }, )
|
完善的双 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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| let isRefreshing = false let requests = []
http.interceptors.response.use( (response) => { const { code } = response.data
if (code === 401) { if (!isRefreshing) { isRefreshing = true
return http .post('/auth/refresh', { refreshToken: getRefreshToken(), }) .then((res) => { const { accessToken, refreshToken } = res.data setAccessToken(accessToken) setRefreshToken(refreshToken)
requests.forEach((cb) => cb(accessToken)) requests = []
return http(response.config) }) .catch((err) => { clearLoginInfo() router.push({ name: 'login' }) return Promise.reject(err) }) .finally(() => { isRefreshing = false }) }
return new Promise((resolve) => { requests.push((token) => { response.config.headers['token'] = token resolve(http(response.config)) }) }) }
return response }, (error) => { return Promise.reject(error) }, )
|
基于 OAuth2 的单点登录及鉴权
OAuth2 简介
OAuth2 是一个授权框架,允许第三方应用在不暴露用户密码的情况下,获得用户资源的访问权限。它通过令牌(Token)机制实现授权,是目前最流行的单点登录(SSO)解决方案之一。
OAuth2 四种授权模式
1. 授权码模式(Authorization Code)
最安全、最常用的模式,适用于有后端的 Web 应用:
1 2 3 4 5 6 7
| 用户 → 第三方应用 → 授权服务器 ↓ 用户授权 → 返回授权码(code) ↓ 第三方应用 → 用授权码换取 access_token ↓ 获得 access_token → 访问用户资源
|
2. 隐式模式(Implicit)
简化版授权码模式,适用于纯前端应用(如 SPA):
1 2 3 4 5
| 用户 → 前端应用 → 授权服务器 ↓ 用户授权 → 直接返回 access_token(无授权码环节) ↓ 前端应用 → 使用 access_token 访问资源
|
3. 密码模式(Resource Owner Password Credentials)
适用于高度信任的第三方应用:
1 2 3 4 5
| 用户 → 提供用户名密码 → 第三方应用 ↓ 第三方应用 → 用用户名密码向授权服务器换取 access_token ↓ 获得 access_token → 访问用户资源
|
4. 客户端凭证模式(Client Credentials)
适用于机器对机器(M2M)的授权:
1 2 3 4 5
| 第三方应用 → 直接向授权服务器申请 token ↓ 提供 client_id 和 client_secret ↓ 获得 access_token → 访问资源
|
单点登录(SSO)原理
单点登录允许用户在多个应用系统中,只需登录一次就可以访问所有相互信任的应用系统。
SSO 工作流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 用户 → 访问应用A → 未登录 → 重定向到SSO认证中心 ↓ 用户输入账号密码 → SSO认证中心验证 ↓ 验证通过 → 返回 Ticket 给应用A ↓ 应用A → 携带 Ticket 向SSO验证 → 获取用户信息 ↓ 用户登录应用A → 访问应用B → 未登录 → 重定向到SSO认证中心 ↓ SSO检测到已登录 → 直接返回 Ticket 给应用B ↓ 应用B → 携带 Ticket 向SSO验证 → 获取用户信息 ↓ 用户无需再次登录 → 访问应用B成功
|
JWT(JSON Web Token)
JWT 是 OAuth2 中常用的令牌格式,由三部分组成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Header.Payload.Signature
Header: { "alg": "HS256", "typ": "JWT" }
Payload: { "sub": "1234567890", "name": "John Doe", "iat": 1516239022, "exp": 1516242622 }
Signature: HMACSHA256(base64(header) + "." + base64(payload), secret)
|
JWT vs Session
| 特性 |
JWT |
Session |
| 存储位置 |
客户端 |
服务器 |
| 扩展性 |
无状态,易于分布式 |
有状态,需要共享存储 |
| 性能 |
无需查询数据库 |
需要查询数据库 |
| 安全性 |
可被篡改(需签名) |
依赖服务器安全 |
| 跨域 |
天然支持 |
需要特殊配置 |
OAuth2 安全最佳实践
- 使用 HTTPS:所有 OAuth2 通信都应使用 HTTPS,防止令牌在传输过程中被窃取
- 短期令牌:access_token 应设置较短的过期时间(如15分钟)
- 刷新令牌轮换:每次使用 refresh_token 时,都应返回一个新的 refresh_token
- 令牌绑定:将令牌与特定客户端或设备绑定,防止令牌被盗用
- 最小权限原则:只申请必要的权限范围(scope)
- 安全存储:refresh_token 应存储在 HttpOnly Cookie 中,防止 XSS 攻击