@[TOC](内容总览)
我与双Token的第一次邂逅,是在手搓自己的个人博客时遇到的。
我仍然很清晰地记得,我是如何实现的:
```
我记得挺清楚,大致就是 短token与刷新token都放在了 请求头中,
如下:
```
## 旧的双Token实现方法
### Token类型
- **Access Token**: 短期有效token,用于API访问验证
- **Refresh Token**: 长期有效token,用于刷新Access Token
### 传输方式
```http
x-access-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
x-refresh-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
### 核心流程
1. **登录**: 返回双token到响应体
2. **API调用**: 前端在请求头携带Access Token
3. **Token刷新**: Access Token过期时,使用Refresh Token获取新的双token
4. **多点登录控制**: 新登录时将旧Refresh Token加入黑名单
### 没有意识到的风险
```
我仍然记得,当时兴致匆匆的,以为自己是个天才。
限于当时的学识,我从来没有考虑过:xss攻击。
现在回想起来真是让当时的自己感到脸红。
```
#### 什么是XSS
`XSS(跨站脚本攻击)` 是一种常见的网络安全漏洞,攻击者通过在网页中注入恶意脚本,在用户浏览器中执行这些脚本来窃取信息或进行其他恶意操作。并且他主要有两种。
1. 存储型 XSS
```
- 恶意脚本被永久存储在服务器上(如数据库)
- 当其他用户访问受影响页面时自动执行
- 危害:盗取用户会话、cookie、个人信息
```
2. 反射型 XSS
```
- 恶意脚本通过 URL 参数等方式传递
- 服务器直接返回包含恶意脚本的页面
- 需要诱使用户点击恶意链接
```
`简单来说,你可以将其看作一个,他会偷走你数据的小偷。`
## 转机
后来在参加过几个项目后,我就开始为双Token的缺陷寻找解决方案。
最开始,我想到的方法是:
```
Access + Refresh 均放 Cookie
当cookie中的HttpOnly设置为true时,脚本就偷不走我想保护的数据了。
因为此时两者(Access、Refresh)均不受 XSS(因为HttpOnly=true)
```
### CSRF的风险
```
当我以为,我已经找到了万全之策时,
殊不知,我只是从一个坑跳进了另一个坑。
```

#### 什么是CSRF
你可以想象一下,接下来这个场景:
```
- 你登录了网上银行(保持登录状态)
- 然后你访问了一个恶意网站
- 这个恶意网站悄悄向银行发送转账请求
- 因为浏览器会自动带上你的登录cookie,银行认为这是你的合法操作
- 结果:你的钱被转走了
```
#### 常见的攻击步骤:
1. **用户登录**:用户登录正规网站,获得认证cookie
2. **保持会话**:用户没有登出,会话仍然有效
3. **访问恶意网站**:用户点击了攻击者发的链接或访问恶意网站
4. **伪造请求**:恶意网站自动向正规网站发送请求(携带用户的cookie)
5. **执行操作**:正规网站认为是用户的自愿操作
#### CSRF 与 XSS 的区别:
| 特性 | CSRF | XSS |
|------|------|-----|
| **攻击目标** | 利用用户的登录状态 | 窃取用户数据/会话 |
| **攻击方式** | 伪造请求 | 注入恶意脚本 |
| **依赖条件** | 用户已登录目标网站 | 网站存在输入漏洞 |
| **防护重点** | 请求来源验证 | 输入输出过滤 |
`大概意思是说:CSRF是个骗子,而XSS是个小偷`
## 新的解决方案
`世上没有十全十美`,而我现在采用的方式是:**Refresh 用 HttpOnly Cookie** +** Access 用 Header**
### 现方案:Refresh 用 HttpOnly Cookie + Access 用 Header
- 客户端约定:
- 请求头携带:`x-access-token: <ACCESS_TOKEN>`
- 刷新时:浏览器自动携带 `x-refresh-token` Cookie(HttpOnly)
- 后端行为:
- 登录设置 `x-refresh-token`(HttpOnly, Path=/, Domain=当前域,`secure` 随 HTTPS)
- 刷新接口只需读取 Cookie,不接收 `x-refresh-token` 头
- 多点登录/黑名单:Redis 记录旧 Refresh,登录时旧值入黑名单并写入新值
- 优点:
- XSS 面显著降低:Refresh Token 不再可读
- CSRF 可控:结合 SameSite、Origin 校验、Token 验签
- 前端更简:无需存/传 Refresh Token
- 注意点:
- 跨域需开启凭证:前端 `withCredentials: true`;后端 CORS 需 `Access-Control-Allow-Credentials: true` 且明确 `Allow-Origin`
通过以上的折中的方案,可以既能享受到双token带来的便利,又能尽量降低损失。
如果你还有更好的建议,欢迎留言,评论( •̀ ω •́ )✧
## 拓展:
### 跨域是什么
浏览器把“同源”定义为三要素都相同:**协议 + 域名(IP) + 端口**。
只要有一个不同,就叫跨域,会触发 CORS 限制。
例子:
- `http://192.168.1.10:3000` → `http://192.168.1.10:8002`:端口不同,跨域
- `http://localhost:3000` → `http://127.0.0.1:3000`:域名不同,跨域
- `http://` → `https://`:协议不同,跨域
### Cookie 常用字段说明(作用)
- **name / value**:Cookie 的名字和值
- **domain**:限定哪个域名可以收到这个 Cookie
- **path**:限定哪个路径可以收到这个 Cookie(如 `/` 代表全站)
- **expires / maxAge**:过期时间(maxAge 是秒;expires 是具体时间点)
- **secure**:只允许在 HTTPS 连接下发送
- **httpOnly**:JS 无法读取(document.cookie 看不到),防 XSS
- **sameSite**:控制跨站请求是否携带 Cookie
- **Lax**:默认,跨站的普通请求不带,安全和可用平衡
- **Strict**:最严格,跨站一律不带
- **None**:跨站也带,但必须配合 `secure=true`(HTTPS)
```go
// 判断 host 是否是 IP 地址;IP 访问下不要设置 domain
if net.ParseIP(host) != nil {
c.SetCookie(name, value, maxAge, "/", "", c.Request.TLS != nil, false)
return
}
// 域名访问:设置 domain 为主机名
// 设置 SameSite 为 Lax(默认),如需 Strict 需显式调用 c.SetSameSite(http.SameSiteStrictMode)
// 这里保持默认 Lax 以平衡安全性与体验
c.SetCookie(name, value, maxAge, "/", host, c.Request.TLS != nil, false)
```