# 8.6 Token 使用说明 :::tip 概述 系统采用 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 │ │ 校验方:AuthMiddleware │ │ 特点:短期有效,泄露影响范围有限 │ ├─────────────────────────────────────────────────────────────┤ │ refresh_token │ │ │ │ 用途:刷新令牌(仅在 access_token 过期时使用) │ │ 有效期:7 天(604800 秒) │ │ payload.type:refresh │ │ 传递方式:POST /api/oauth2/token 请求体 │ │ 校验方:JwtService::refreshToken() │ │ 特点:长期有效,仅用于换取新令牌,不用于接口认证 │ └─────────────────────────────────────────────────────────────┘ ``` ## 请求头格式 ```text Authorization: Bearer ``` ### 示例 ```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_name` | `Authorization` | 请求头名称 | | `jwt.header_prefix` | `Bearer ` | 请求头前缀(注意有空格) | ## 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 ``` ### 响应字段 | 字段 | 类型 | 说明 | |------|------|------| | `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` | ### 请求示例 ```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 } } ``` ### 失败响应 | 场景 | code | msg | |------|------|-----| | 缺少 refresh_token | 1 | 缺少refresh_token参数 | | refresh_token 无效/过期 | 1 | 无效的refresh_token | | 用户不存在/禁用 | 1 | 用户不存在或已被禁用 | ## Token 有效性检查 ### 接口信息 | 项目 | 说明 | |------|------| | URL | `GET /api/oauth2/check/token` | | 权限 | 无需认证 | ### 请求示例 ```bash # 通过请求头传递 GET /api/oauth2/check/token Authorization: Bearer # 通过 URL 参数传递 GET /api/oauth2/check/token?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 吊销 | 项目 | 说明 | |------|------| | URL | `DELETE /api/oauth2/remove/{token}` | ```bash DELETE /api/oauth2/remove/eyJ0eXAiOi... Authorization: Bearer ``` :::warning 简化实现 当前 Token 吊销为简化实现(直接返回 true),JWT 本身无状态,服务端无法直接使其失效。生产环境建议结合 Redis 黑名单机制实现真正的吊销。 ::: ## URL 参数传递(SSO 场景) 路由守卫支持通过 URL 参数传递 Token,适用于 SSO 单点登录场景: ```html http://localhost:8001/?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 状态 | 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` 为强随机密钥: ```bash php -r "echo bin2hex(random_bytes(32));" ``` :::