签入版本
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# 8.6 Token 使用说明
|
||||
|
||||
:::tip 概述
|
||||
系统采用 JWT 双令牌认证,access_token 用于接口认证(2小时),refresh_token 用于刷新令牌(7天)。所有需认证的接口必须在请求头中携带 access_token。
|
||||
系统采用 JWT 双令牌认证,access_token 用于接口认证(2 小时),refresh_token 用于刷新令牌(7 天)。refresh_token 存储于 Redis(jti 有状态),支持**一次性刷新、登出吊销、踢下线**。所有需认证的接口必须在请求头中携带 access_token。
|
||||
:::
|
||||
|
||||
## Token 完整生命周期
|
||||
@@ -19,33 +19,35 @@
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 后端:LoginController → LoginLogic → JwtService │
|
||||
│ 后端:AuthController → JwtService │
|
||||
│ │
|
||||
│ 1. CaptchaService::check() 验证码校验 │
|
||||
│ 2. User::where('username') 查询用户 │
|
||||
│ 3. PasswordService::verify() 密码校验 │
|
||||
│ 4. Jwt::createAccessToken() 签发 access_token(2小时) │
|
||||
│ Jwt::createRefreshToken() 签发 refresh_token(7天) │
|
||||
│ 1. LoginRequest 参数格式校验 │
|
||||
│ 2. CaptchaService::verify() 验证码校验(key/code) │
|
||||
│ 3. User::where('username') 查询用户 │
|
||||
│ 4. PasswordService::verify() 密码校验(双重加盐) │
|
||||
│ 5. createTokenPair() 签发双令牌 + refresh 存 Redis │
|
||||
│ (同一 jti,Redis 键 refresh:{uid}:{jti},TTL 7 天) │
|
||||
└─────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 响应: │
|
||||
│ 响应(登录/刷新接口 raw 返回,保持 snake_case): │
|
||||
│ { │
|
||||
│ "accessToken": "eyJ...", ← 用于接口认证 │
|
||||
│ "refreshToken": "eyJ...", ← 用于刷新令牌 │
|
||||
│ "tokenType": "Bearer", │
|
||||
│ "expiresIn": 7200, ← 2小时 = 7200秒 │
|
||||
│ "user": { "id":1, "username":"admin" } │
|
||||
│ "access_token": "eyJ...", ← 用于接口认证 │
|
||||
│ "refresh_token": "eyJ...", ← 用于刷新令牌 │
|
||||
│ "token_type": "Bearer", │
|
||||
│ "expires_in": 7200, ← 2小时 = 7200秒 │
|
||||
│ "user": { "id":1, "username":"admin", ... } │
|
||||
│ } │
|
||||
│ (AuthController 调用 success(..., true) 跳过驼峰转换, │
|
||||
│ 与前端 store 的 data.access_token 读取方式保持一致) │
|
||||
└─────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 前端:存储 Token │
|
||||
│ 前端:存储 Token(仅存 access_token) │
|
||||
│ │
|
||||
│ localStorage.setItem('access_token', accessToken) │
|
||||
│ localStorage.setItem('refresh_token', refreshToken) │
|
||||
│ storage.set('ACCESS-TOKEN', data.access_token) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -57,116 +59,75 @@
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 前端:请求拦截器 │
|
||||
│ │
|
||||
│ const token = localStorage.getItem('access_token') │
|
||||
│ const token = storage.get('ACCESS-TOKEN') │
|
||||
│ config.headers['Authorization'] = `Bearer ${token}` │
|
||||
└─────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ GET /api/user/page?pageNo=1&pageSize=10 │
|
||||
│ Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 │
|
||||
└─────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 后端:AuthMiddleware │
|
||||
│ 后端:Authenticate 中间件(auth.jwt) │
|
||||
│ │
|
||||
│ 1. Jwt::getTokenFromHeader() 从请求头提取 Token │
|
||||
│ │ 空 → 返回 401 "未提供认证令牌" │
|
||||
│ ▼ │
|
||||
│ 2. Jwt::decode($token) 解码并校验签名+有效期 │
|
||||
│ │ 过期 → 返回 401 "token已过期" │
|
||||
│ │ 签名无效 → 返回 401 "token签名无效" │
|
||||
│ ▼ │
|
||||
│ 3. $decoded->type === 'access' 校验 Token 类型 │
|
||||
│ │ 非 access → 返回 401 "无效的认证令牌" │
|
||||
│ ▼ │
|
||||
│ 4. $request->userInfo = $decoded 注入用户信息 │
|
||||
│ ▼ │
|
||||
│ 5. checkPermission() 权限校验 │
|
||||
│ │ 无权限 → 返回 403 "无访问权限" │
|
||||
│ ▼ │
|
||||
│ 6. 放行 → Controller → Logic → Model → 响应 │
|
||||
│ 1. 提取 Authorization 头(带 Bearer 前缀则剥离,裸 token 亦可)│
|
||||
│ │ 头缺失 → HTTP 200 "未提供认证令牌" │
|
||||
│ 2. JwtService::parseToken()(校验签名+有效期+type=access) │
|
||||
│ │ 过期/签名错/类型错 → HTTP 200 │
|
||||
│ 3. 注入 attributes['userInfo'] │
|
||||
│ 4. 反射 #[Permission] → checkPermission() │
|
||||
│ │ 无权限 → HTTP 403 "无访问权限:{权限码}" │
|
||||
│ 5. 放行 → Controller → Service → Model → 响应 │
|
||||
└─────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 前端:响应拦截器 │
|
||||
│ │
|
||||
│ code === 0 → 返回 data(成功) │
|
||||
│ code === 401 → Token 过期,尝试刷新 │
|
||||
│ code === 403 → 提示"无访问权限" │
|
||||
│ code/status 401 → 弹窗"登录身份已失效" → 清存储 → 跳登录页 │
|
||||
│ code/status 403 → 提示"无访问权限" │
|
||||
│ 其他 → 提示 msg │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Token 过期自动刷新
|
||||
### Token 刷新(后端能力)
|
||||
|
||||
access_token 过期后,客户端可用 refresh_token 换取新令牌对:
|
||||
|
||||
```text
|
||||
前端请求 API → 后端返回 code=401 "token已过期"
|
||||
POST /api/refresh
|
||||
{ "refresh_token": "eyJ..." } ← 主入口(公开接口)
|
||||
(或 POST /api/oauth2/token,grant_type=refresh_token)
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 前端:响应拦截器检测到 401 │
|
||||
│ │
|
||||
│ if (code === 401 && !config._retry) { │
|
||||
│ // 尝试用 refresh_token 刷新 │
|
||||
│ } │
|
||||
└─────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 前端:POST /api/oauth2/token │
|
||||
│ { grant_type: "refresh_token", refresh_token: "eyJ..." } │
|
||||
└─────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 后端:JwtService::refreshToken() │
|
||||
│ │
|
||||
│ 1. Jwt::decode(refreshToken) 解码 refresh_token │
|
||||
│ │ 无效/过期 → 返回错误 │
|
||||
│ ▼ │
|
||||
│ 2. $decoded->type === 'refresh' 校验必须为 refresh 类型 │
|
||||
│ │ 非 refresh → 返回错误 │
|
||||
│ ▼ │
|
||||
│ 3. User::find($decoded->uid) 校验用户仍存在且状态正常 │
|
||||
│ │ 不存在/禁用 → 返回错误 │
|
||||
│ ▼ │
|
||||
│ 4. 签发新 access_token + 新 refresh_token │
|
||||
└─────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 前端:更新存储 + 重试原请求 │
|
||||
│ │
|
||||
│ localStorage.setItem('access_token', newAccessToken) │
|
||||
│ localStorage.setItem('refresh_token', newRefreshToken) │
|
||||
│ │
|
||||
│ // 用新 Token 重试原失败请求 │
|
||||
│ config.headers['Authorization'] = `Bearer ${newAccessToken}`│
|
||||
│ return axios(config) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
JwtService::refreshToken()
|
||||
│ 1. 解析 refresh_token → 失败 → "无效的refresh_token"
|
||||
│ 2. type 必须为 refresh
|
||||
│ 3. Lua 原子校验并删除 Redis refresh:{uid}:{jti}(一次性)
|
||||
│ 已用过/已吊销 → "refresh_token已失效"
|
||||
│ 4. 用户状态校验 → "用户不存在或已被禁用"
|
||||
│ 5. createTokenPair() → 新 jti 签发新令牌对
|
||||
▼
|
||||
返回新令牌对(access_token / refresh_token 均为新值)
|
||||
```
|
||||
|
||||
### Token 刷新失败(重新登录)
|
||||
:::warning 一次性刷新(防重放)
|
||||
旧 refresh_token 刷新后立即从 Redis 删除——**必须保存返回的新 refresh_token**,重放旧令牌会得到 `refresh_token已失效`。
|
||||
:::
|
||||
|
||||
:::info 管理后台前端的现状
|
||||
`ui/` 管理后台当前**未内置自动续签**:access_token 过期(401)时直接弹窗提示重新登录(见下文「响应拦截器」)。`POST /api/refresh` 面向第三方接入客户端与后续前端升级使用。
|
||||
:::
|
||||
|
||||
### Token 失效(重新登录)
|
||||
|
||||
```text
|
||||
前端请求 API → 后端返回 code=401
|
||||
│
|
||||
▼
|
||||
尝试用 refresh_token 刷新
|
||||
│
|
||||
▼
|
||||
刷新失败(refresh_token 也过期/无效)
|
||||
access_token 过期 / refresh_token 刷新失败(过期/已吊销/网络异常)
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 前端:清除登录态,跳转登录页 │
|
||||
│ │
|
||||
│ localStorage.removeItem('access_token') │
|
||||
│ localStorage.removeItem('refresh_token') │
|
||||
│ storage.clear() │
|
||||
│ window.location.href = '/login' │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
@@ -176,22 +137,20 @@
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ access_token │
|
||||
│ │
|
||||
│ 用途:接口认证(每个 API 请求携带) │
|
||||
│ 有效期:2 小时(7200 秒) │
|
||||
│ payload.type:access │
|
||||
│ 有效期:2 小时(JWT_ACCESS_TTL=7200 秒) │
|
||||
│ payload:type=access,含 jti │
|
||||
│ 传递方式:Authorization: Bearer <token> │
|
||||
│ 校验方:AuthMiddleware │
|
||||
│ 特点:短期有效,泄露影响范围有限 │
|
||||
│ 校验方:Authenticate 中间件 │
|
||||
│ 特点:短期有效、无状态,泄露影响范围有限 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ refresh_token │
|
||||
│ │
|
||||
│ 用途:刷新令牌(仅在 access_token 过期时使用) │
|
||||
│ 有效期:7 天(604800 秒) │
|
||||
│ payload.type:refresh │
|
||||
│ 传递方式:POST /api/oauth2/token 请求体 │
|
||||
│ 校验方:JwtService::refreshToken() │
|
||||
│ 特点:长期有效,仅用于换取新令牌,不用于接口认证 │
|
||||
│ 有效期:7 天(JWT_REFRESH_TTL=604800 秒) │
|
||||
│ payload:type=refresh,含与 access 相同的 jti │
|
||||
│ 传递方式:POST /api/refresh 请求体 │
|
||||
│ 校验方:JwtService::refreshToken()(Redis jti 一次性校验) │
|
||||
│ 特点:有状态(Redis)、可吊销、支持踢下线 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -217,12 +176,11 @@ curl "http://localhost:8000/api/user/page?pageNo=1&pageSize=10" \
|
||||
|
||||
| 配置项 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `jwt.header_name` | `Authorization` | 请求头名称 |
|
||||
| `jwt.header_prefix` | `Bearer ` | 请求头前缀(注意有空格) |
|
||||
| `jwt.header_prefix` | `Bearer ` | 请求头前缀(注意有尾部空格,`config/jwt.php`) |
|
||||
|
||||
## Token 获取
|
||||
|
||||
### 登录获取
|
||||
### 登录获取(主入口)
|
||||
|
||||
```bash
|
||||
POST /api/login
|
||||
@@ -244,15 +202,15 @@ Content-Type: application/json
|
||||
"ok": true,
|
||||
"msg": "登录成功",
|
||||
"data": {
|
||||
"accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
||||
"refreshToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
||||
"tokenType": "Bearer",
|
||||
"expiresIn": 7200,
|
||||
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
||||
"refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 7200,
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "admin",
|
||||
"realname": "管理员",
|
||||
"avatar": "/uploads/user/avatar/avatar.jpg"
|
||||
"avatar": "user/avatar/20260923/100429_e8FAKf9r.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,36 +229,38 @@ grant_type=password&username=admin&password=123456
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `accessToken` | string | 访问令牌(用于接口认证) |
|
||||
| `refreshToken` | string | 刷新令牌(用于换取新令牌) |
|
||||
| `tokenType` | string | 令牌类型(固定 `Bearer`) |
|
||||
| `expiresIn` | number | access_token 有效期(秒,默认 7200) |
|
||||
| `access_token` | string | 访问令牌(用于接口认证) |
|
||||
| `refresh_token` | string | 刷新令牌(用于换取新令牌) |
|
||||
| `token_type` | string | 令牌类型(固定 `Bearer`) |
|
||||
| `expires_in` | number | access_token 有效期(秒,默认 7200) |
|
||||
| `user` | object | 用户基础信息(id、username、realname、avatar) |
|
||||
|
||||
:::tip 登录/刷新接口为 snake_case 特例
|
||||
一般接口响应经驼峰自动转换(`create_time` → `createTime`);登录、刷新等令牌接口由 `AuthController` 以 `success(..., true)` raw 返回,键名保持 snake_case,与前端 `data.access_token` 读取方式一致。
|
||||
:::
|
||||
|
||||
## Token 刷新
|
||||
|
||||
access_token 过期后(响应 code=401),使用 refresh_token 换取新令牌对:
|
||||
access_token 过期后(code=401),使用 refresh_token 换取新令牌对:
|
||||
|
||||
### 接口信息
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| URL | `POST /api/oauth2/token` |
|
||||
| URL(主入口) | `POST /api/refresh`(公开接口) |
|
||||
| URL(OAuth2 兼容) | `POST /api/oauth2/token`(grant_type=refresh_token) |
|
||||
| Content-Type | `application/json` 或 `application/x-www-form-urlencoded` |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```bash
|
||||
# JSON 格式
|
||||
POST /api/oauth2/token
|
||||
# 推荐:/api/refresh(JSON)
|
||||
POST /api/refresh
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
|
||||
}
|
||||
{ "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." }
|
||||
|
||||
# 表单格式
|
||||
# OAuth2 兼容(表单)
|
||||
POST /api/oauth2/token
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
@@ -313,12 +273,12 @@ grant_type=refresh_token&refresh_token=eyJ0eXAiOi...
|
||||
{
|
||||
"code": 0,
|
||||
"ok": true,
|
||||
"msg": "success",
|
||||
"msg": "刷新成功",
|
||||
"data": {
|
||||
"accessToken": "eyJ...(新access_token)",
|
||||
"refreshToken": "eyJ...(新refresh_token)",
|
||||
"tokenType": "Bearer",
|
||||
"expiresIn": 7200
|
||||
"access_token": "eyJ...(新access_token)",
|
||||
"refresh_token": "eyJ...(新refresh_token)",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 7200
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -327,8 +287,9 @@ grant_type=refresh_token&refresh_token=eyJ0eXAiOi...
|
||||
|
||||
| 场景 | code | msg |
|
||||
|------|------|-----|
|
||||
| 缺少 refresh_token | 1 | 缺少refresh_token参数 |
|
||||
| refresh_token 无效/过期 | 1 | 无效的refresh_token |
|
||||
| 缺少 refresh_token | 1 | 缺少refresh_token |
|
||||
| refresh_token 无效/类型错 | 1 | 无效的refresh_token |
|
||||
| 旧令牌已用过(重放)/ 已吊销 | 1 | refresh_token已失效 |
|
||||
| 用户不存在/禁用 | 1 | 用户不存在或已被禁用 |
|
||||
|
||||
## Token 有效性检查
|
||||
@@ -338,7 +299,7 @@ grant_type=refresh_token&refresh_token=eyJ0eXAiOi...
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| URL | `GET /api/oauth2/check/token` |
|
||||
| 权限 | 无需认证 |
|
||||
| 权限 | 无需认证(公开接口) |
|
||||
|
||||
### 请求示例
|
||||
|
||||
@@ -375,31 +336,33 @@ GET /api/oauth2/check/token?token=<access_token>
|
||||
"msg": "操作成功",
|
||||
"data": {
|
||||
"valid": false,
|
||||
"msg": "token已过期"
|
||||
"msg": "Expired token"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Token 吊销
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| URL | `DELETE /api/oauth2/remove/{token}` |
|
||||
| 接口 | 方法 | 认证 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `/api/logout` | POST | 认证组 | 登出:吊销当前 refresh_token(`revokeToken` 删除 Redis jti) |
|
||||
| `/api/oauth2/remove/{token}` | DELETE | 认证组 | 吊销指定令牌 |
|
||||
|
||||
```bash
|
||||
DELETE /api/oauth2/remove/eyJ0eXAiOi...
|
||||
# 登出(携带 access_token)
|
||||
POST /api/logout
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
|
||||
:::warning 简化实现
|
||||
当前 Token 吊销为简化实现(直接返回 true),JWT 本身无状态,服务端无法直接使其失效。生产环境建议结合 Redis 黑名单机制实现真正的吊销。
|
||||
:::tip 真实吊销(Redis 有状态)
|
||||
本项目 refresh_token 以 `refresh:{uid}:{jti}` 存于 Redis——登出即删除该键,旧 access_token 到期后**无法续签**;`JwtService::revokeAllTokens($userId)` 可一次性吊销用户全部令牌(踢下线、改密全端失效场景)。
|
||||
:::
|
||||
|
||||
## URL 参数传递(SSO 场景)
|
||||
|
||||
路由守卫支持通过 URL 参数传递 Token,适用于 SSO 单点登录场景:
|
||||
前端路由守卫支持通过 URL 参数传递 Token,适用于 SSO 单点登录场景:
|
||||
|
||||
```html
|
||||
```text
|
||||
http://localhost:8001/?token=<access_token>
|
||||
```
|
||||
|
||||
@@ -414,64 +377,53 @@ http://localhost:8001/?token=<access_token>
|
||||
|
||||
## 前端存储与使用
|
||||
|
||||
### 存储
|
||||
### 存储(摘自 `ui/src/store/modules/user.ts` 实际代码)
|
||||
|
||||
```typescript
|
||||
// 登录后存储
|
||||
localStorage.setItem('access_token', data.accessToken);
|
||||
localStorage.setItem('refresh_token', data.refreshToken);
|
||||
// 登录成功后存储(仅 access_token)
|
||||
const ex = 7 * 24 * 60 * 60 * 1000;
|
||||
storage.set(ACCESS_TOKEN, data.access_token, ex); // 键名 'ACCESS-TOKEN'
|
||||
|
||||
// 登出时清除
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
// 登出时清除(userStore.logout)
|
||||
storage.remove(ACCESS_TOKEN);
|
||||
storage.remove(CURRENT_USER);
|
||||
storage.clear();
|
||||
```
|
||||
|
||||
### 请求拦截器(自动携带)
|
||||
### 请求拦截器(自动携带,摘自 `ui/src/utils/http/axios/index.ts`)
|
||||
|
||||
```typescript
|
||||
// src/utils/http/axios/index.ts
|
||||
axios.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
const token = storage.get('ACCESS-TOKEN', '');
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
```
|
||||
|
||||
### 响应拦截器(自动刷新)
|
||||
### 响应拦截器(401 处理,摘自 `ui/src/utils/http/axios/index.ts`)
|
||||
|
||||
```typescript
|
||||
axios.interceptors.response.use(
|
||||
response => response,
|
||||
async error => {
|
||||
const { config, response } = error;
|
||||
// code === 401(ResultEnum.TIMEOUT)
|
||||
case ResultEnum.TIMEOUT:
|
||||
if (router.currentRoute.value?.name === PageEnum.BASE_LOGIN_NAME) return;
|
||||
ElMessageBox.confirm('登录身份已失效,请重新登录!', '提示', {
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
storage.clear();
|
||||
window.location.href = PageEnum.BASE_LOGIN; // '/login'
|
||||
})
|
||||
.catch(() => {});
|
||||
break;
|
||||
```
|
||||
|
||||
if (response?.status === 401 && !config._retry) {
|
||||
config._retry = true;
|
||||
### SSO 场景(URL 参数传递 Token)
|
||||
|
||||
const refreshToken = localStorage.getItem('refresh_token');
|
||||
if (refreshToken) {
|
||||
try {
|
||||
// 尝试刷新 Token
|
||||
const res = await refreshTokenApi(refreshToken);
|
||||
localStorage.setItem('access_token', res.data.accessToken);
|
||||
localStorage.setItem('refresh_token', res.data.refreshToken);
|
||||
路由守卫检测 `?token=` 参数并写入存储(摘自 `ui/src/router/router-guards.ts` 实际代码):
|
||||
|
||||
// 用新 Token 重试原请求
|
||||
config.headers['Authorization'] = `Bearer ${res.data.accessToken}`;
|
||||
return axios(config);
|
||||
} catch (e) {
|
||||
// 刷新失败,跳转登录
|
||||
localStorage.clear();
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
```typescript
|
||||
if (to.query && 'token' in to.query) {
|
||||
const tokenVal = String(to.query.token);
|
||||
storage.set(ACCESS_TOKEN, tokenVal);
|
||||
userStore.setToken(tokenVal);
|
||||
}
|
||||
```
|
||||
|
||||
## Token Payload 结构
|
||||
@@ -483,6 +435,7 @@ axios.interceptors.response.use(
|
||||
"uid": 1,
|
||||
"username": "admin",
|
||||
"type": "access",
|
||||
"jti": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"iss": "rxthinkcmf",
|
||||
"iat": 1727000000,
|
||||
"exp": 1727007200
|
||||
@@ -496,36 +449,44 @@ axios.interceptors.response.use(
|
||||
"uid": 1,
|
||||
"username": "admin",
|
||||
"type": "refresh",
|
||||
"jti": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"iss": "rxthinkcmf",
|
||||
"iat": 1727000000,
|
||||
"exp": 1727604800
|
||||
}
|
||||
```
|
||||
|
||||
(同一登录的两个令牌共享 `jti`,Redis 键 `refresh:{uid}:{jti}`。)
|
||||
|
||||
## 错误响应汇总
|
||||
|
||||
| 场景 | HTTP 状态 | code | msg |
|
||||
|------|----------|------|-----|
|
||||
| 未提供 Token | 200 | 401 | 未提供认证令牌 |
|
||||
| Token 过期 | 200 | 401 | token已过期 |
|
||||
| Token 签名无效 | 200 | 401 | token签名无效 |
|
||||
| Token 类型错误 | 200 | 401 | 无效的认证令牌 |
|
||||
| 无权限 | 200 | 403 | 无访问权限 |
|
||||
| Token 过期 | 200 | 401 | Expired token |
|
||||
| Token 签名无效 | 200 | 401 | Signature verification failed |
|
||||
| Token 类型错误(refresh 当 access 用) | 200 | 401 | 无效的access_token |
|
||||
| 无权限 | **403** | 403 | 无访问权限:{权限码} |
|
||||
| refresh_token 无效/类型错 | 200 | 1 | 无效的refresh_token |
|
||||
| refresh_token 已用/已吊销 | 200 | 1 | refresh_token已失效 |
|
||||
|
||||
## 配置项
|
||||
|
||||
| 配置键 | 环境变量 | 默认值 | 说明 |
|
||||
| 配置键(`config/jwt.php`) | 环境变量 | 默认值 | 说明 |
|
||||
|--------|---------|--------|------|
|
||||
| `jwt.secret` | `JWT.SECRET` | 内置默认密钥 | JWT 签名密钥,**生产环境务必修改** |
|
||||
| `jwt.access_ttl` | `JWT.ACCESS_TTL` | `7200` | access_token 有效期(秒) |
|
||||
| `jwt.refresh_ttl` | `JWT.REFRESH_TTL` | `604800` | refresh_token 有效期(秒) |
|
||||
| `jwt.issuer` | — | `rxthinkcmf` | 签发者标识 |
|
||||
| `jwt.header_name` | — | `Authorization` | 请求头名称 |
|
||||
| `jwt.secret` | `JWT_SECRET` | (必填) | JWT 签名密钥,**必须 ≥ 32 字符,否则拒绝启动** |
|
||||
| `jwt.access_ttl` | `JWT_ACCESS_TTL` | `7200` | access_token 有效期(秒) |
|
||||
| `jwt.refresh_ttl` | `JWT_REFRESH_TTL` | `604800` | refresh_token 有效期(秒) |
|
||||
| `jwt.perms_ttl` | `JWT_PERMS_TTL` | `7200` | 权限缓存 TTL(秒) |
|
||||
| `jwt.issuer` | `JWT_ISSUER` | `rxthinkcmf` | 签发者标识 |
|
||||
| `jwt.header_prefix` | — | `Bearer ` | 请求头前缀 |
|
||||
|
||||
:::warning 生产环境安全
|
||||
生产环境务必通过 `.env` 设置 `JWT.SECRET` 为强随机密钥:
|
||||
生产环境务必通过 `.env` 设置 `JWT_SECRET` 为 ≥32 字符的强随机密钥(**不满足应用拒绝启动**):
|
||||
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
# 或
|
||||
php -r "echo bin2hex(random_bytes(32));"
|
||||
```
|
||||
:::
|
||||
|
||||
Reference in New Issue
Block a user