Become a sponsor

概述
系统采用 JWT 双令牌认证,access_token 用于接口认证(2小时),refresh_token 用于刷新令牌(7天)。所有需认证的接口必须在请求头中携带 access_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) │
└─────────────────────────────────────────────────────────────┘用户操作(如点击"用户列表")
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 前端:请求拦截器 │
│ │
│ 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 │
└─────────────────────────────────────────────────────────────┘前端请求 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) │
└─────────────────────────────────────────────────────────────┘前端请求 API → 后端返回 code=401
│
▼
尝试用 refresh_token 刷新
│
▼
刷新失败(refresh_token 也过期/无效)
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 前端:清除登录态,跳转登录页 │
│ │
│ localStorage.removeItem('access_token') │
│ localStorage.removeItem('refresh_token') │
│ window.location.href = '/login' │
└─────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────┐
│ 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 | 请求头前缀(注意有空格) |
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"
}
}
}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) |
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 | 用户不存在或已被禁用 |
| 项目 | 说明 |
|---|---|
| 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已过期"
}
}| 项目 | 说明 |
|---|---|
| URL | DELETE /api/oauth2/remove/{token} |
DELETE /api/oauth2/remove/eyJ0eXAiOi...
Authorization: Bearer <access_token>简化实现
当前 Token 吊销为简化实现(直接返回 true),JWT 本身无状态,服务端无法直接使其失效。生产环境建议结合 Redis 黑名单机制实现真正的吊销。
路由守卫支持通过 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);
}
);{
"uid": 1,
"username": "admin",
"type": "access",
"iss": "rxthinkcmf",
"iat": 1727000000,
"exp": 1727007200
}{
"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 | 请求头前缀 |
生产环境安全
生产环境务必通过 .env 设置 JWT.SECRET 为强随机密钥:
php -r "echo bin2hex(random_bytes(32));"