Skip to content

8.6 Token 使用说明 ​

概述

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

Token 完整生命周期 ​

登录获取 Token ​

text
用户输入账号密码
    │
    ▼
┌─────────────────────────────────────────────────────────────┐
│ 前端: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 ​

text
用户操作(如点击"用户列表")
    │
    ▼
┌─────────────────────────────────────────────────────────────┐
│ 前端:请求拦截器                                            │
│                                                             │
│ 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 过期自动刷新 ​

text
前端请求 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 刷新失败(重新登录) ​

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

两种 Token 的区别 ​

text
┌─────────────────────────────────────────────────────────────┐
│                    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()                         │
│  特点:长期有效,仅用于换取新令牌,不用于接口认证            │
└─────────────────────────────────────────────────────────────┘

请求头格式 ​

text
Authorization: Bearer <access_token>

示例 ​

bash
# 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_nameAuthorization请求头名称
jwt.header_prefixBearer 请求头前缀(注意有空格)

Token 获取 ​

登录获取 ​

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

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

成功响应:

json
{
    "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 密码模式获取 ​

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

grant_type=password&username=admin&password=123456

响应字段 ​

字段类型说明
accessTokenstring访问令牌(用于接口认证)
refreshTokenstring刷新令牌(用于换取新令牌)
tokenTypestring令牌类型(固定 Bearer)
expiresInnumberaccess_token 有效期(秒,默认 7200)
userobject用户基础信息(id、username、realname、avatar)

Token 刷新 ​

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

接口信息 ​

项目说明
URLPOST /api/oauth2/token
Content-Typeapplication/json 或 application/x-www-form-urlencoded

请求示例 ​

bash
# 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...

成功响应 ​

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

失败响应 ​

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

Token 有效性检查 ​

接口信息 ​

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

请求示例 ​

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

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

成功响应(有效) ​

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

失败响应(无效) ​

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

Token 吊销 ​

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

简化实现

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

URL 参数传递(SSO 场景) ​

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

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

前端处理逻辑:

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

前端存储与使用 ​

存储 ​

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

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

请求拦截器(自动携带) ​

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

响应拦截器(自动刷新) ​

typescript
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 ​

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

refresh_token ​

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

错误响应汇总 ​

场景HTTP 状态codemsg
未提供 Token200401未提供认证令牌
Token 过期200401token已过期
Token 签名无效200401token签名无效
Token 类型错误200401无效的认证令牌
无权限200403无访问权限

配置项 ​

配置键环境变量默认值说明
jwt.secretJWT.SECRET内置默认密钥JWT 签名密钥,生产环境务必修改
jwt.access_ttlJWT.ACCESS_TTL7200access_token 有效期(秒)
jwt.refresh_ttlJWT.REFRESH_TTL604800refresh_token 有效期(秒)
jwt.issuer—rxthinkcmf签发者标识
jwt.header_name—Authorization请求头名称
jwt.header_prefix—Bearer 请求头前缀

生产环境安全

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

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

小蚂蚁云团队 · 提供技术支持