Files
rxthinkcmf-wiki/RXThinkCMF_ThinkPhp6_NaiveVue/zh/api-spec/token.md
T
2026-09-29 10:14:04 +08:00

21 KiB
Raw Blame History

8.6 Token 使用说明

:::tip 概述 系统采用 JWT 双令牌认证,access_token 用于接口认证(2小时),refresh_token 用于刷新令牌(7天)。所有需认证的接口必须在请求头中携带 access_token。 :::

Token 完整生命周期

登录获取 Token

用户输入账号密码
    │
    ▼
┌─────────────────────────────────────────────────────────────┐
│ 前端:POST /api/login                                       │
│ { username, password, code, key }                           │
└─────────────────────────┬───────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│ 后端:LoginController → LoginLogic → JwtService             │
│                                                             │
│ 1. CaptchaService::check()     验证码校验                    │
│ 2. User::where('username')     查询用户                      │
│ 3. PasswordService::verify()   密码校验                      │
│ 4. Jwt::createAccessToken()    签发 access_token(2小时)    │
│    Jwt::createRefreshToken()   签发 refresh_token(7天)     │
└─────────────────────────┬───────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│ 响应:                                                      │
│ {                                                           │
│   "accessToken": "eyJ...",      ← 用于接口认证              │
│   "refreshToken": "eyJ...",     ← 用于刷新令牌              │
│   "tokenType": "Bearer",                                   │
│   "expiresIn": 7200,            ← 2小时 = 7200秒           │
│   "user": { "id":1, "username":"admin" }                    │
│ }                                                           │
└─────────────────────────┬───────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│ 前端:存储 Token                                            │
│                                                             │
│ localStorage.setItem('access_token', accessToken)           │
│ localStorage.setItem('refresh_token', refreshToken)         │
└─────────────────────────────────────────────────────────────┘

使用 Token 请求 API

用户操作(如点击"用户列表")
    │
    ▼
┌─────────────────────────────────────────────────────────────┐
│ 前端:请求拦截器                                            │
│                                                             │
│ const token = localStorage.getItem('access_token')          │
│ config.headers['Authorization'] = `Bearer ${token}`         │
└─────────────────────────┬───────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│ GET /api/user/page?pageNo=1&pageSize=10                     │
│ Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 │
└─────────────────────────┬───────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│ 后端:AuthMiddleware                                        │
│                                                             │
│ 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 → 响应                 │
└─────────────────────────┬───────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│ 前端:响应拦截器                                            │
│                                                             │
│ code === 0   → 返回 data(成功)                            │
│ code === 401 → Token 过期,尝试刷新                         │
│ code === 403 → 提示"无访问权限"                             │
│ 其他         → 提示 msg                                     │
└─────────────────────────────────────────────────────────────┘

Token 过期自动刷新

前端请求 API → 后端返回 code=401 "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)                                        │
└─────────────────────────────────────────────────────────────┘

Token 刷新失败(重新登录)

前端请求 API → 后端返回 code=401
    │
    ▼
尝试用 refresh_token 刷新
    │
    ▼
刷新失败(refresh_token 也过期/无效)
    │
    ▼
┌─────────────────────────────────────────────────────────────┐
│ 前端:清除登录态,跳转登录页                                 │
│                                                             │
│ localStorage.removeItem('access_token')                     │
│ localStorage.removeItem('refresh_token')                    │
│ window.location.href = '/login'                             │
└─────────────────────────────────────────────────────────────┘

两种 Token 的区别

┌─────────────────────────────────────────────────────────────┐
│                    access_token                             │
│                                                             │
│  用途:接口认证(每个 API 请求携带)                         │
│  有效期:2 小时(7200 秒)                                   │
│  payload.type:access                                       │
│  传递方式:Authorization: Bearer <token>                    │
│  校验方:AuthMiddleware                                      │
│  特点:短期有效,泄露影响范围有限                            │
├─────────────────────────────────────────────────────────────┤
│                    refresh_token                             │
│                                                             │
│  用途:刷新令牌(仅在 access_token 过期时使用)              │
│  有效期:7 天(604800 秒)                                   │
│  payload.type:refresh                                      │
│  传递方式:POST /api/oauth2/token 请求体                    │
│  校验方:JwtService::refreshToken()                         │
│  特点:长期有效,仅用于换取新令牌,不用于接口认证            │
└─────────────────────────────────────────────────────────────┘

请求头格式

Authorization: Bearer <access_token>

示例

# curl
curl http://localhost:8000/api/user/page \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."

# curl(带分页参数)
curl "http://localhost:8000/api/user/page?pageNo=1&pageSize=10" \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."

请求头配置

配置项 默认值 说明
jwt.header_name Authorization 请求头名称
jwt.header_prefix Bearer 请求头前缀(注意有空格)

Token 获取

登录获取

POST /api/login
Content-Type: application/json

{
    "username": "admin",
    "password": "123456",
    "code": "a3Bx",
    "key": "captcha_key_xxx"
}

成功响应:

{
    "code": 0,
    "ok": true,
    "msg": "登录成功",
    "data": {
        "accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
        "refreshToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
        "tokenType": "Bearer",
        "expiresIn": 7200,
        "user": {
            "id": 1,
            "username": "admin",
            "realname": "管理员",
            "avatar": "/uploads/user/avatar/avatar.jpg"
        }
    }
}

OAuth2 密码模式获取

POST /api/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=admin&password=123456

响应字段

字段 类型 说明
accessToken string 访问令牌(用于接口认证)
refreshToken string 刷新令牌(用于换取新令牌)
tokenType string 令牌类型(固定 Bearer)
expiresIn number access_token 有效期(秒,默认 7200)
user object 用户基础信息(id、username、realname、avatar)

Token 刷新

access_token 过期后(响应 code=401),使用 refresh_token 换取新令牌对:

接口信息

项目 说明
URL POST /api/oauth2/token
Content-Type application/json 或 application/x-www-form-urlencoded

请求示例

# JSON 格式
POST /api/oauth2/token
Content-Type: application/json

{
    "grant_type": "refresh_token",
    "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
}

# 表单格式
POST /api/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=eyJ0eXAiOi...

成功响应

{
    "code": 0,
    "ok": true,
    "msg": "success",
    "data": {
        "accessToken": "eyJ...(新access_token)",
        "refreshToken": "eyJ...(新refresh_token)",
        "tokenType": "Bearer",
        "expiresIn": 7200
    }
}

失败响应

场景 code msg
缺少 refresh_token 1 缺少refresh_token参数
refresh_token 无效/过期 1 无效的refresh_token
用户不存在/禁用 1 用户不存在或已被禁用

Token 有效性检查

接口信息

项目 说明
URL GET /api/oauth2/check/token
权限 无需认证

请求示例

# 通过请求头传递
GET /api/oauth2/check/token
Authorization: Bearer <access_token>

# 通过 URL 参数传递
GET /api/oauth2/check/token?token=<access_token>

成功响应(有效)

{
    "code": 0,
    "ok": true,
    "msg": "操作成功",
    "data": {
        "valid": true,
        "uid": 1,
        "exp": 1727007200
    }
}

失败响应(无效)

{
    "code": 0,
    "ok": true,
    "msg": "操作成功",
    "data": {
        "valid": false,
        "msg": "token已过期"
    }
}

Token 吊销

项目 说明
URL DELETE /api/oauth2/remove/{token}
DELETE /api/oauth2/remove/eyJ0eXAiOi...
Authorization: Bearer <access_token>

:::warning 简化实现 当前 Token 吊销为简化实现(直接返回 true),JWT 本身无状态,服务端无法直接使其失效。生产环境建议结合 Redis 黑名单机制实现真正的吊销。 :::

URL 参数传递(SSO 场景)

路由守卫支持通过 URL 参数传递 Token,适用于 SSO 单点登录场景:

http://localhost:8001/?token=<access_token>

前端处理逻辑:

1. 路由守卫检测 URL 中的 token 参数
2. 将 token 存入 localStorage
3. 清除 URL 中的 token 参数(防止泄露)
4. 后续请求自动携带该 token

前端存储与使用

存储

// 登录后存储
localStorage.setItem('access_token', data.accessToken);
localStorage.setItem('refresh_token', data.refreshToken);

// 登出时清除
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');

请求拦截器(自动携带)

// 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;
});

响应拦截器(自动刷新)

axios.interceptors.response.use(
    response => response,
    async error => {
        const { config, response } = error;

        if (response?.status === 401 && !config._retry) {
            config._retry = true;

            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 重试原请求
                    config.headers['Authorization'] = `Bearer ${res.data.accessToken}`;
                    return axios(config);
                } catch (e) {
                    // 刷新失败,跳转登录
                    localStorage.clear();
                    window.location.href = '/login';
                }
            }
        }

        return Promise.reject(error);
    }
);

Token Payload 结构

access_token

{
    "uid": 1,
    "username": "admin",
    "type": "access",
    "iss": "rxthinkcmf",
    "iat": 1727000000,
    "exp": 1727007200
}

refresh_token

{
    "uid": 1,
    "username": "admin",
    "type": "refresh",
    "iss": "rxthinkcmf",
    "iat": 1727000000,
    "exp": 1727604800
}

错误响应汇总

场景 HTTP 状态 code msg
未提供 Token 200 401 未提供认证令牌
Token 过期 200 401 token已过期
Token 签名无效 200 401 token签名无效
Token 类型错误 200 401 无效的认证令牌
无权限 200 403 无访问权限

配置项

配置键 环境变量 默认值 说明
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.header_prefix — Bearer 请求头前缀

:::warning 生产环境安全 生产环境务必通过 .env 设置 JWT.SECRET 为强随机密钥:

php -r "echo bin2hex(random_bytes(32));"

:::