签入版本
This commit is contained in:
@@ -8,22 +8,21 @@
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 认证授权全景 │
|
||||
│ 认证授权全景 │
|
||||
│ │
|
||||
│ ① 登录 POST /api/login │
|
||||
│ 用户名+密码+验证码 → 校验 → 签发 access_token + refresh_token │
|
||||
│ ① 登录 POST /api/login(公开接口) │
|
||||
│ 用户名+密码+验证码(key/code) → 校验 → 签发双令牌 + 写登录日志 │
|
||||
│ │
|
||||
│ ② 请求 API GET/POST /api/xxx │
|
||||
│ Bearer token → AuthMiddleware → 权限校验 → 注入 userInfo │
|
||||
│ ② 请求 API GET/POST /api/xxx(auth.jwt 组内) │
|
||||
│ Bearer token → Authenticate → 权限校验 → 注入 userInfo │
|
||||
│ │
|
||||
│ ③ Token 过期 响应 code=401 │
|
||||
│ refresh_token → POST /api/oauth2/token → 新令牌对 │
|
||||
│ ③ Token 过期 响应 code=401(HTTP 200) │
|
||||
│ refresh_token → POST /api/refresh → 新令牌对(旧 jti 一次性作废) │
|
||||
│ │
|
||||
│ ④ 租户隔离 TenantMiddleware │
|
||||
│ 解析租户 → 注入 tenantId → Logic 层自动过滤 │
|
||||
│ ④ 租户隔离 ResolveTenant │
|
||||
│ 解析租户 → 注入 tenantId → Service 层自动过滤 │
|
||||
│ │
|
||||
│ ⑤ 登出 前端清除 localStorage │
|
||||
│ (JWT 无状态,服务端无需处理) │
|
||||
│ ⑤ 登出 POST /api/logout → revokeToken(refresh 即时失效) │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -33,58 +32,52 @@
|
||||
|
||||
```text
|
||||
POST /api/login
|
||||
{ "username": "admin", "password": "123456", "code": "a3Bx", "key": "xxx" }
|
||||
{ "username": "admin", "password": "123456", "key": "captcha-uuid", "code": "a3Bx" }
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ LoginController::login() │
|
||||
│ AuthController::login(LoginRequest $request) │
|
||||
│ │
|
||||
│ 1. getParams() 获取请求参数 │
|
||||
│ 2. validate($params, LoginValidate::class . '.login') │
|
||||
│ │ 失败 → {"code":422, "msg":"参数验证失败"} │
|
||||
│ 1. LoginRequest 类型注入 → rules() 格式校验 │
|
||||
│ │ 失败 → {"code":1, "msg":"参数验证失败"} │
|
||||
│ ▼ │
|
||||
│ 3. LoginLogic::login($username, $password, $code, $key) │
|
||||
└─────────────────────────┬───────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ LoginLogic::login() │
|
||||
│ │
|
||||
│ 1. CaptchaService::check($code, $key) │
|
||||
│ │ 失败 → throw "验证码错误" │
|
||||
│ 2. 读取 key / code 参数(与 captcha 响应字段命名一致) │
|
||||
│ CaptchaService::verify($key, $code) │
|
||||
│ │ 仅当 config('captcha.length') > 0 时启用 │
|
||||
│ │ 失败 → fail("请输入验证码" / "验证码错误") │
|
||||
│ ▼ │
|
||||
│ 2. JwtService::login($username, $password) │
|
||||
│ 3. JwtService::login($username, $password) │
|
||||
└─────────────────────────┬───────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ JwtService::login() │
|
||||
│ │
|
||||
│ 1. User::withoutGlobalScope(['soft_delete']) │
|
||||
│ ->where('username', $username)->find() │
|
||||
│ │ 不存在 → throw "用户名或密码错误" │
|
||||
│ 1. User::where('username', $username)->first() │
|
||||
│ │ 不存在 → BizException("用户名或密码错误") │
|
||||
│ ▼ │
|
||||
│ 2. $user->status != 1 │
|
||||
│ │ 禁用 → throw "账号已被禁用" │
|
||||
│ 2. $user->status 禁用 │
|
||||
│ │ → BizException("账号已被禁用") │
|
||||
│ ▼ │
|
||||
│ 3. PasswordService::verify($password, $hashed, $salt) │
|
||||
│ │ 不匹配 → throw "用户名或密码错误" │
|
||||
│ │ 不匹配 → BizException("用户名或密码错误") │
|
||||
│ ▼ │
|
||||
│ 4. Jwt::createAccessToken($uid, $username) │
|
||||
│ Jwt::createRefreshToken($uid, $username) │
|
||||
│ 4. createTokenPair($user) │
|
||||
│ │ 生成 jti(UUID) → 签发 access + refresh │
|
||||
│ │ Redis::setex("refresh:{uid}:{jti}", 7天, token) │
|
||||
│ │ loadPermissions() → 缓存 perms:{uid} │
|
||||
│ ▼ │
|
||||
│ 5. 返回 { access_token, refresh_token, user } │
|
||||
│ 5. 返回 { access_token, refresh_token, token_type, │
|
||||
│ expires_in, user } │
|
||||
└─────────────────────────┬───────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ LoginController 续 │
|
||||
│ AuthController 续 │
|
||||
│ │
|
||||
│ 成功 → LoginLogic::recordLoginLog(成功) │
|
||||
│ → $this->success($result, '登录成功', true) │
|
||||
│ │
|
||||
│ 失败 → LoginLogic::recordLoginLog(失败) │
|
||||
│ → $this->fail($e->getMessage()) │
|
||||
│ 成功 → writeLoginLog(成功) → success($result,'登录成功') │
|
||||
│ 失败 → writeLoginLog(失败) → fail($e->getMessage()) │
|
||||
│ (登录日志成功/失败均记录,含 IP 归属地、浏览器) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -99,8 +92,8 @@ Content-Type: application/json
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "123456",
|
||||
"code": "a3Bx",
|
||||
"key": "captcha_xxx"
|
||||
"key": "5c62cae9-3bdb-4356-bc4a-c4c61b7c3bea",
|
||||
"code": "a3Bx"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -112,29 +105,34 @@ 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::tip 登录/刷新接口 raw 返回
|
||||
服务端原始数组键为 `access_token` 等 snake_case;登录、刷新接口由 `AuthController` 以 `success(..., true)` raw 返回,**跳过**驼峰转换,与前端 `data.access_token` 读取方式一致(一般接口仍经驼峰转换,如 `create_time` → `createTime`)。
|
||||
:::
|
||||
|
||||
**失败响应:**
|
||||
|
||||
| 场景 | code | msg |
|
||||
|------|------|-----|
|
||||
| 参数格式错误 | 1 | 参数验证失败(data 为字段明细) |
|
||||
| 验证码缺失 | 1 | 请输入验证码 |
|
||||
| 验证码错误 | 1 | 验证码错误 |
|
||||
| 用户不存在 | 1 | 用户名或密码错误 |
|
||||
| 密码错误 | 1 | 用户名或密码错误 |
|
||||
| 账号禁用 | 1 | 账号已被禁用 |
|
||||
| 参数缺失 | 422 | 用户名不能为空 / 密码不能为空 |
|
||||
|
||||
## 二、接口认证流程
|
||||
|
||||
@@ -146,102 +144,83 @@ Authorization: Bearer eyJ...
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ CorsMiddleware(全局) │
|
||||
│ OPTIONS 预检 → 返回 204 + CORS 头 │
|
||||
│ 普通请求 → 设置跨域头,放行 │
|
||||
│ ProxyMiddleware(全局 api 组) │
|
||||
│ 受信代理校验 → 还原真实 REMOTE_ADDR │
|
||||
└─────────────────────────┬───────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ AuthMiddleware │
|
||||
│ │
|
||||
│ 1. 检查排除列表(login/captcha/oauth2 → 直接放行) │
|
||||
│ 2. Jwt::getTokenFromHeader() │
|
||||
│ │ 空 → {"code":401, "msg":"未提供认证令牌"} │
|
||||
│ ▼ │
|
||||
│ 3. Jwt::decode($token) │
|
||||
│ │ 过期 → {"code":401, "msg":"token已过期"} │
|
||||
│ │ 签名无效 → {"code":401, "msg":"token签名无效"} │
|
||||
│ ▼ │
|
||||
│ 4. $decoded->type === 'access' │
|
||||
│ │ 非 access → {"code":401, "msg":"无效的认证令牌"} │
|
||||
│ ▼ │
|
||||
│ 5. $request->userInfo = $decoded │
|
||||
│ │ 注入 uid、username 等到请求对象 │
|
||||
│ ▼ │
|
||||
│ 6. 读取 #[Permission] 注解 │
|
||||
│ │ uid=1 → 超级管理员,跳过校验 │
|
||||
│ │ 查询用户权限列表 → 匹配权限码 │
|
||||
│ │ 无权限 → {"code":403, "msg":"无访问权限"} │
|
||||
│ ▼ │
|
||||
│ 7. 放行 │
|
||||
│ Cors(全局 api 组) │
|
||||
│ OPTIONS 预检 → 204 + CORS 头;普通请求 → 追加 CORS 头 │
|
||||
└─────────────────────────┬───────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ TenantMiddleware │
|
||||
│ │
|
||||
│ 1. 从 JWT payload 获取 uid │
|
||||
│ 2. 查询用户所属租户 │
|
||||
│ 3. $request->tenantId = 租户ID │
|
||||
│ 4. $request->tenantInfo = 租户信息 │
|
||||
│ │ 租户禁用/过期 → {"code":403, "msg":"租户异常"} │
|
||||
│ ▼ │
|
||||
│ 5. 放行 │
|
||||
│ ForceJsonResponse(全局 api 组) │
|
||||
│ Accept 强制 application/json │
|
||||
└─────────────────────────┬───────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ DemoMiddleware │
|
||||
│ Authenticate(auth.jwt,路由组) │
|
||||
│ │
|
||||
│ 1. 检查 DEMO 配置(env('app_demo')) │
|
||||
│ 2. 非演示模式 → 放行 │
|
||||
│ 3. 演示模式 + 写操作(POST/PUT/DELETE) │
|
||||
│ │ 未标注 #[DemoAllow] → {"code":403, "msg":"演示环境"} │
|
||||
│ ▼ │
|
||||
│ 4. 放行 │
|
||||
│ 1. 提取 Bearer token(公开接口在组外,不经过此处) │
|
||||
│ │ 缺失 → HTTP 200 {"code":401, "msg":"未提供认证令牌"}│
|
||||
│ 2. JwtService::parseToken()(type 必须为 access) │
|
||||
│ │ 过期/签名错 → HTTP 200 │
|
||||
│ 3. 注入 attributes['userInfo'] │
|
||||
│ 4. 反射 #[Permission] 注解 → checkPermission() │
|
||||
│ │ uid=1 跳过 → Redis perms:{uid} → Cache → 查库 │
|
||||
│ │ 无权限 → HTTP 403 {"code":403, "msg":"无访问权限…"} │
|
||||
└─────────────────────────┬───────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ LogMiddleware │
|
||||
│ ResolveTenant(tenant,路由组) │
|
||||
│ │
|
||||
│ 1. 读取 #[Log] 注解 │
|
||||
│ 2. 记录操作日志(异步/同步) │
|
||||
│ 3. 放行 │
|
||||
│ 1. 无 userInfo → 放行(未认证请求不注入租户) │
|
||||
│ 2. uid=1 → tenantId=0, isSuperAdmin=true │
|
||||
│ 3. 非超管 → 查库 tenant_id │
|
||||
│ │ 用户不存在 → HTTP 200 │
|
||||
│ │ 租户不存在/禁用/过期 → HTTP 403 │
|
||||
│ 4. 注入 attributes['tenantId' / 'tenantInfo'] │
|
||||
└─────────────────────────┬───────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ UserController::page() │
|
||||
│ │
|
||||
│ return parent::_index($this->logic); │
|
||||
│ → Logic::pageList() → Model → Database │
|
||||
│ LogOperation(log.operation,路由组) │
|
||||
│ handle:收集上下文、读 #[OperationLog]、判断是否记录 │
|
||||
│ terminate:响应后写入操作日志(不阻塞业务) │
|
||||
└─────────────────────────┬───────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ UserController::page() │
|
||||
│ return $this->_page($this->service); │
|
||||
│ → Service::pageList() → Model → Database │
|
||||
└─────────────────────────┬───────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
JSON 响应返回
|
||||
```
|
||||
|
||||
(各中间件源码级解析见 [4.2 请求处理链路](/zh/architecture/request-flow.html))
|
||||
|
||||
### 权限校验详解
|
||||
|
||||
```text
|
||||
AuthMiddleware::checkPermission($uid, $permissionCode)
|
||||
Authenticate::checkPermission($uid, $permissionCode)
|
||||
│
|
||||
▼
|
||||
1. $uid === 1(超级管理员)
|
||||
│ 是 → return true(跳过校验)
|
||||
▼
|
||||
2. UserRole::where('user_id', $uid)->column('role_id')
|
||||
│ 无角色 → return false
|
||||
2. Redis::get("perms:{$uid}") ← 一级缓存
|
||||
│ 命中 → in_array('*:*:*') 或 in_array(code) → 通过
|
||||
│ Redis 不可用 → 降级
|
||||
▼
|
||||
3. RoleMenu → Menu::whereIn('role_id', $roleIds)
|
||||
.where('type', 1) // type=1 为权限节点
|
||||
.column('permission')
|
||||
3. Cache::remember("perms_fallback:{$uid}", 300) ← 二级缓存(5 分钟)
|
||||
│ 命中 → 匹配判断
|
||||
▼
|
||||
4. in_array($permissionCode, $permissions)
|
||||
4. loadPermissionsFromDb($uid) ← 三级:查库并回填缓存
|
||||
UserRole(用户角色)→ RoleMenu join Menu
|
||||
→ 条件:menu 未删除、type=1、permission 非空
|
||||
│ in_array($permissionCode, $permissions)
|
||||
│ 包含 → return true(放行)
|
||||
│ 不包含 → return false(返回 403)
|
||||
│ 不包含 → return false(HTTP 403)
|
||||
```
|
||||
|
||||
**权限码格式:** `sys:{模块名}:{操作}`
|
||||
@@ -252,73 +231,57 @@ sys:user:update 修改用户
|
||||
sys:user:delete 删除用户
|
||||
sys:user:page 用户分页
|
||||
sys:article:add 添加文章
|
||||
sys:article:list 文章列表
|
||||
sys:article:detail 文章详情
|
||||
```
|
||||
|
||||
菜单/角色菜单数据变更时 `MenuObserver` / `RoleMenuObserver` 自动清除相关缓存,权限调整即时生效。
|
||||
|
||||
## 三、Token 刷新流程
|
||||
|
||||
### 流程图
|
||||
|
||||
```text
|
||||
前端检测到 code=401
|
||||
客户端持有 refresh_token,需要换取新令牌对
|
||||
(第三方接入调用;管理后台前端当前未内置自动续签,见 8.6)
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 前端响应拦截器 │
|
||||
│ │
|
||||
│ 1. 从 localStorage 读取 refresh_token │
|
||||
│ 2. POST /api/oauth2/token │
|
||||
│ { grant_type: "refresh_token", refresh_token: "..." } │
|
||||
│ 客户端请求 │
|
||||
│ POST /api/refresh { refresh_token: "..." } │
|
||||
└─────────────────────────┬───────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ LoginController::token() │
|
||||
│ │
|
||||
│ 1. 判断 grant_type === 'refresh_token' │
|
||||
│ 2. LoginLogic::refreshToken($refreshToken) │
|
||||
│ AuthController::refresh() │
|
||||
│ 1. 读取 refresh_token(缺失 → fail("缺少refresh_token")│
|
||||
│ 2. JwtService::refreshToken($refreshToken) │
|
||||
└─────────────────────────┬───────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ JwtService::refreshToken() │
|
||||
│ │
|
||||
│ 1. Jwt::decode($refreshToken) │
|
||||
│ │ 无效 → throw "无效的refresh_token" │
|
||||
│ ▼ │
|
||||
│ 2. $decoded->type === 'refresh' │
|
||||
│ │ 非 refresh → throw "无效的refresh_token" │
|
||||
│ ▼ │
|
||||
│ 3. User::find($decoded->uid) │
|
||||
│ │ 不存在或禁用 → throw "用户不存在或已被禁用" │
|
||||
│ ▼ │
|
||||
│ 4. 签发新 access_token + 新 refresh_token │
|
||||
│ ▼ │
|
||||
│ 5. 返回新令牌对 │
|
||||
│ 1. 解析令牌 → 失败 → BizException("无效的refresh_token") │
|
||||
│ 2. type === 'refresh' → 否则同上 │
|
||||
│ 3. Lua 原子脚本:GETDEL refresh:{uid}:{jti} │
|
||||
│ │ 不存在(已用过/已吊销)→ "refresh_token已失效" │
|
||||
│ 4. User 状态校验 → "用户不存在或已被禁用" │
|
||||
│ 5. createTokenPair() → 新 jti 签发新令牌对写入 Redis │
|
||||
└─────────────────────────┬───────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 前端响应拦截器(续) │
|
||||
│ │
|
||||
│ 1. 更新 localStorage 中的 access_token │
|
||||
│ 2. 更新 localStorage 中的 refresh_token │
|
||||
│ 3. 用新 access_token 重试原失败请求 │
|
||||
│ 客户端处理结果 │
|
||||
│ 1. 保存返回的新 access_token / refresh_token(均为新值) │
|
||||
│ 2. 后续请求携带新 access_token │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 刷新接口详情
|
||||
|
||||
**请求:**
|
||||
**请求(主入口):**
|
||||
|
||||
```bash
|
||||
POST /api/oauth2/token
|
||||
POST /api/refresh
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
|
||||
}
|
||||
{ "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." }
|
||||
```
|
||||
|
||||
**成功响应:**
|
||||
@@ -327,12 +290,12 @@ Content-Type: application/json
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -341,10 +304,15 @@ Content-Type: application/json
|
||||
|
||||
| 场景 | 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 | 用户不存在或已被禁用 |
|
||||
|
||||
:::warning 一次性刷新(防重放)
|
||||
旧 refresh_token 刷新后立即从 Redis 删除(Lua 原子操作),重放旧令牌会返回"refresh_token已失效"。客户端必须保存返回的**新** refresh_token,否则下次无法续签。
|
||||
:::
|
||||
|
||||
## 四、完整请求示例
|
||||
|
||||
以"查询用户分页"为例,展示一次认证请求的完整链路:
|
||||
@@ -355,36 +323,32 @@ Content-Type: application/json
|
||||
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
|
||||
│
|
||||
▼
|
||||
② CorsMiddleware → 设置跨域头,放行
|
||||
② ProxyMiddleware / Cors / ForceJsonResponse → 依次放行
|
||||
│
|
||||
▼
|
||||
③ AuthMiddleware
|
||||
│ getTokenFromHeader() → "eyJ..."
|
||||
│ Jwt::decode() → { uid:1, username:"admin", type:"access", exp:... }
|
||||
③ Authenticate
|
||||
│ 提取 token → parseToken() → { uid:1, username:"admin", type:"access", jti, exp }
|
||||
│ type === 'access' ✓
|
||||
│ $request->userInfo = $decoded
|
||||
│ checkPermission(1, 'sys:user:page')
|
||||
│ uid=1 → 超级管理员,跳过
|
||||
▼
|
||||
④ TenantMiddleware
|
||||
│ 查询用户租户 → tenantId=1
|
||||
│ $request->tenantId = 1
|
||||
▼
|
||||
⑤ DemoMiddleware → 非演示模式,放行
|
||||
│ 注入 attributes['userInfo']
|
||||
│ checkPermission(1, 'sys:user:page') → uid=1 超管跳过 ✓
|
||||
│
|
||||
▼
|
||||
⑥ LogMiddleware → GET 查询不记录日志,放行
|
||||
④ ResolveTenant → uid=1 → tenantId=0, isSuperAdmin=true
|
||||
│
|
||||
▼
|
||||
⑦ UserController::page()
|
||||
│ return parent::_index($this->logic)
|
||||
▼
|
||||
⑧ UserLogic::pageList($params)
|
||||
│ applyTenantScope() → WHERE tenant_id = 1
|
||||
│ applyDataScope() → 按角色 data_scope 过滤
|
||||
│ → SELECT * FROM think_user WHERE is_delete=0 AND tenant_id=1 ...
|
||||
▼
|
||||
⑨ 返回 JSON
|
||||
⑤ LogOperation → GET 普通查询不记录(非导入导出类型),放行
|
||||
│
|
||||
▼
|
||||
⑥ UserController::page()
|
||||
│ return $this->_page($this->service)
|
||||
│
|
||||
▼
|
||||
⑦ UserService::pageList($params)
|
||||
│ buildQuery:LIKE/精确匹配 + 软删除全局范围
|
||||
│ → SELECT ... FROM laravel_user WHERE is_delete = 0 ...
|
||||
│
|
||||
▼
|
||||
⑧ 返回 JSON(驼峰转换后)
|
||||
{
|
||||
"code": 0,
|
||||
"ok": true,
|
||||
@@ -392,84 +356,57 @@ Content-Type: application/json
|
||||
"data": {
|
||||
"records": [...],
|
||||
"total": 42,
|
||||
"size": 20,
|
||||
"current": 1,
|
||||
"size": 20,
|
||||
"pages": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 五、前端自动刷新 Token 实现
|
||||
## 五、前端 401 处理(实际实现)
|
||||
|
||||
`ui/` 管理后台当前**未内置自动续签**:access_token 过期(code=401)时直接提示重新登录(摘自 `ui/src/utils/http/axios/index.ts` 实际代码):
|
||||
|
||||
```typescript
|
||||
// src/utils/http/axios/index.ts
|
||||
|
||||
let isRefreshing = false;
|
||||
let pendingRequests: Function[] = [];
|
||||
|
||||
axios.interceptors.response.use(
|
||||
response => response,
|
||||
async error => {
|
||||
const { config, response } = error;
|
||||
|
||||
// 非 401 错误直接抛出
|
||||
if (response?.status !== 401) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// 已在刷新中,将请求加入队列等待
|
||||
if (isRefreshing) {
|
||||
return new Promise(resolve => {
|
||||
pendingRequests.push((newToken: string) => {
|
||||
config.headers['Authorization'] = `Bearer ${newToken}`;
|
||||
resolve(axios(config));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
isRefreshing = true;
|
||||
config._retry = true;
|
||||
|
||||
try {
|
||||
const refreshToken = localStorage.getItem('refresh_token');
|
||||
const res = await refreshTokenApi(refreshToken);
|
||||
const { accessToken, refreshToken: newRefreshToken } = res.data;
|
||||
|
||||
// 更新存储
|
||||
localStorage.setItem('access_token', accessToken);
|
||||
localStorage.setItem('refresh_token', newRefreshToken);
|
||||
|
||||
// 重试当前请求
|
||||
config.headers['Authorization'] = `Bearer ${accessToken}`;
|
||||
|
||||
// 执行队列中的等待请求
|
||||
pendingRequests.forEach(cb => cb(accessToken));
|
||||
pendingRequests = [];
|
||||
|
||||
return axios(config);
|
||||
} catch (e) {
|
||||
// 刷新失败,清除登录态,跳转登录页
|
||||
localStorage.clear();
|
||||
window.location.href = '/login';
|
||||
return Promise.reject(e);
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
// 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;
|
||||
```
|
||||
|
||||
## 六、OAuth2 兼容接口
|
||||
自动续签所需的后端能力已就绪([三、Token 刷新流程](#三-token-刷新流程) 的 `POST /api/refresh`),后续前端升级时可在此处接入刷新与请求重试逻辑。
|
||||
|
||||
## 六、登出与 OAuth2 兼容接口
|
||||
|
||||
### 登出(令牌即时失效)
|
||||
|
||||
```bash
|
||||
# 携带 access_token,吊销对应 refresh_token
|
||||
POST /api/logout
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
|
||||
`JwtService::revokeToken()` 解析 `uid + jti` 后删除 Redis 中的 `refresh:{uid}:{jti}`——旧 access_token 到期后将无法续签。改密、封号等场景调用 `revokeAllTokens($userId)` 一次性吊销该用户**全部**令牌并清除权限缓存(踢下线)。
|
||||
|
||||
### OAuth2 兼容接口
|
||||
|
||||
系统同时提供 OAuth2 风格的 Token 接口,兼容标准 OAuth2 客户端:
|
||||
|
||||
| 接口 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/api/oauth2/token` | POST | 获取令牌(grant_type=password 或 refresh_token) |
|
||||
| `/api/oauth2/remove/{token}` | DELETE | 吊销令牌 |
|
||||
| `/api/oauth2/check/token` | GET | 检测令牌有效性 |
|
||||
| 接口 | 方法 | 认证 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `/api/oauth2/token` | POST | 公开 | 获取令牌(grant_type=password 或 refresh_token) |
|
||||
| `/api/oauth2/check/token` | GET | 公开 | 检测令牌有效性 |
|
||||
| `/api/oauth2/remove/{token}` | DELETE | 认证组 | 吊销令牌 |
|
||||
|
||||
### 密码模式获取令牌
|
||||
#### 密码模式获取令牌
|
||||
|
||||
```bash
|
||||
POST /api/oauth2/token
|
||||
@@ -478,7 +415,7 @@ Content-Type: application/x-www-form-urlencoded
|
||||
grant_type=password&username=admin&password=123456
|
||||
```
|
||||
|
||||
### 刷新模式获取令牌
|
||||
#### 刷新模式获取令牌
|
||||
|
||||
```bash
|
||||
POST /api/oauth2/token
|
||||
|
||||
Reference in New Issue
Block a user