# 5.2 认证流程实战 :::tip 概述 本节通过完整的请求链路,展示从登录到接口调用的认证全流程,包括登录、Token 刷新、接口认证、权限校验、租户隔离五个核心环节。 ::: ## 认证全景图 ```text ┌─────────────────────────────────────────────────────────────────────┐ │ 认证授权全景 │ │ │ │ ① 登录 POST /api/login │ │ 用户名+密码+验证码 → 校验 → 签发 access_token + refresh_token │ │ │ │ ② 请求 API GET/POST /api/xxx │ │ Bearer token → AuthMiddleware → 权限校验 → 注入 userInfo │ │ │ │ ③ Token 过期 响应 code=401 │ │ refresh_token → POST /api/oauth2/token → 新令牌对 │ │ │ │ ④ 租户隔离 TenantMiddleware │ │ 解析租户 → 注入 tenantId → Logic 层自动过滤 │ │ │ │ ⑤ 登出 前端清除 localStorage │ │ (JWT 无状态,服务端无需处理) │ └─────────────────────────────────────────────────────────────────────┘ ``` ## 一、登录流程 ### 完整流程图 ```text POST /api/login { "username": "admin", "password": "123456", "code": "a3Bx", "key": "xxx" } │ ▼ ┌─────────────────────────────────────────────────────────┐ │ LoginController::login() │ │ │ │ 1. getParams() 获取请求参数 │ │ 2. validate($params, LoginValidate::class . '.login') │ │ │ 失败 → {"code":422, "msg":"参数验证失败"} │ │ ▼ │ │ 3. LoginLogic::login($username, $password, $code, $key) │ └─────────────────────────┬───────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ LoginLogic::login() │ │ │ │ 1. CaptchaService::check($code, $key) │ │ │ 失败 → throw "验证码错误" │ │ ▼ │ │ 2. JwtService::login($username, $password) │ └─────────────────────────┬───────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ JwtService::login() │ │ │ │ 1. User::withoutGlobalScope(['soft_delete']) │ │ ->where('username', $username)->find() │ │ │ 不存在 → throw "用户名或密码错误" │ │ ▼ │ │ 2. $user->status != 1 │ │ │ 禁用 → throw "账号已被禁用" │ │ ▼ │ │ 3. PasswordService::verify($password, $hashed, $salt) │ │ │ 不匹配 → throw "用户名或密码错误" │ │ ▼ │ │ 4. Jwt::createAccessToken($uid, $username) │ │ Jwt::createRefreshToken($uid, $username) │ │ ▼ │ │ 5. 返回 { access_token, refresh_token, user } │ └─────────────────────────┬───────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ LoginController 续 │ │ │ │ 成功 → LoginLogic::recordLoginLog(成功) │ │ → $this->success($result, '登录成功', true) │ │ │ │ 失败 → LoginLogic::recordLoginLog(失败) │ │ → $this->fail($e->getMessage()) │ └─────────────────────────────────────────────────────────┘ ``` ### 登录接口详情 **请求:** ```bash POST /api/login Content-Type: application/json { "username": "admin", "password": "123456", "code": "a3Bx", "key": "captcha_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" } } } ``` **失败响应:** | 场景 | code | msg | |------|------|-----| | 验证码错误 | 1 | 验证码错误 | | 用户不存在 | 1 | 用户名或密码错误 | | 密码错误 | 1 | 用户名或密码错误 | | 账号禁用 | 1 | 账号已被禁用 | | 参数缺失 | 422 | 用户名不能为空 / 密码不能为空 | ## 二、接口认证流程 ### 中间件链执行顺序 ```text 请求 GET /api/user/page Authorization: Bearer eyJ... │ ▼ ┌─────────────────────────────────────────────────────────┐ │ CorsMiddleware(全局) │ │ OPTIONS 预检 → 返回 204 + CORS 头 │ │ 普通请求 → 设置跨域头,放行 │ └─────────────────────────┬───────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ 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. 放行 │ └─────────────────────────┬───────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ TenantMiddleware │ │ │ │ 1. 从 JWT payload 获取 uid │ │ 2. 查询用户所属租户 │ │ 3. $request->tenantId = 租户ID │ │ 4. $request->tenantInfo = 租户信息 │ │ │ 租户禁用/过期 → {"code":403, "msg":"租户异常"} │ │ ▼ │ │ 5. 放行 │ └─────────────────────────┬───────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ DemoMiddleware │ │ │ │ 1. 检查 DEMO 配置(env('app_demo')) │ │ 2. 非演示模式 → 放行 │ │ 3. 演示模式 + 写操作(POST/PUT/DELETE) │ │ │ 未标注 #[DemoAllow] → {"code":403, "msg":"演示环境"} │ │ ▼ │ │ 4. 放行 │ └─────────────────────────┬───────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ LogMiddleware │ │ │ │ 1. 读取 #[Log] 注解 │ │ 2. 记录操作日志(异步/同步) │ │ 3. 放行 │ └─────────────────────────┬───────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ UserController::page() │ │ │ │ return parent::_index($this->logic); │ │ → Logic::pageList() → Model → Database │ └─────────────────────────┬───────────────────────────────┘ │ ▼ JSON 响应返回 ``` ### 权限校验详解 ```text AuthMiddleware::checkPermission($uid, $permissionCode) │ ▼ 1. $uid === 1(超级管理员) │ 是 → return true(跳过校验) ▼ 2. UserRole::where('user_id', $uid)->column('role_id') │ 无角色 → return false ▼ 3. RoleMenu → Menu::whereIn('role_id', $roleIds) .where('type', 1) // type=1 为权限节点 .column('permission') ▼ 4. in_array($permissionCode, $permissions) │ 包含 → return true(放行) │ 不包含 → return false(返回 403) ``` **权限码格式:** `sys:{模块名}:{操作}` ```text sys:user:add 添加用户 sys:user:update 修改用户 sys:user:delete 删除用户 sys:user:page 用户分页 sys:article:add 添加文章 sys:article:list 文章列表 ``` ## 三、Token 刷新流程 ### 流程图 ```text 前端检测到 code=401 │ ▼ ┌─────────────────────────────────────────────────────────┐ │ 前端响应拦截器 │ │ │ │ 1. 从 localStorage 读取 refresh_token │ │ 2. POST /api/oauth2/token │ │ { grant_type: "refresh_token", refresh_token: "..." } │ └─────────────────────────┬───────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ LoginController::token() │ │ │ │ 1. 判断 grant_type === 'refresh_token' │ │ 2. LoginLogic::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. 更新 localStorage 中的 access_token │ │ 2. 更新 localStorage 中的 refresh_token │ │ 3. 用新 access_token 重试原失败请求 │ └─────────────────────────────────────────────────────────┘ ``` ### 刷新接口详情 **请求:** ```bash POST /api/oauth2/token Content-Type: application/json { "grant_type": "refresh_token", "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." } ``` **成功响应:** ```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 | 用户不存在或已被禁用 | ## 四、完整请求示例 以"查询用户分页"为例,展示一次认证请求的完整链路: ```text ① 前端发起请求 GET /api/user/page?pageNo=1&pageSize=20 Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... │ ▼ ② CorsMiddleware → 设置跨域头,放行 │ ▼ ③ AuthMiddleware │ getTokenFromHeader() → "eyJ..." │ Jwt::decode() → { uid:1, username:"admin", type:"access", exp:... } │ type === 'access' ✓ │ $request->userInfo = $decoded │ checkPermission(1, 'sys:user:page') │ uid=1 → 超级管理员,跳过 ▼ ④ TenantMiddleware │ 查询用户租户 → tenantId=1 │ $request->tenantId = 1 ▼ ⑤ DemoMiddleware → 非演示模式,放行 │ ▼ ⑥ LogMiddleware → GET 查询不记录日志,放行 │ ▼ ⑦ 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 { "code": 0, "ok": true, "msg": "操作成功", "data": { "records": [...], "total": 42, "size": 20, "current": 1, "pages": 3 } } ``` ## 五、前端自动刷新 Token 实现 ```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; } } ); ``` ## 六、OAuth2 兼容接口 系统同时提供 OAuth2 风格的 Token 接口,兼容标准 OAuth2 客户端: | 接口 | 方法 | 说明 | |------|------|------| | `/api/oauth2/token` | POST | 获取令牌(grant_type=password 或 refresh_token) | | `/api/oauth2/remove/{token}` | DELETE | 吊销令牌 | | `/api/oauth2/check/token` | GET | 检测令牌有效性 | ### 密码模式获取令牌 ```bash POST /api/oauth2/token Content-Type: application/x-www-form-urlencoded grant_type=password&username=admin&password=123456 ``` ### 刷新模式获取令牌 ```bash POST /api/oauth2/token Content-Type: application/x-www-form-urlencoded grant_type=refresh_token&refresh_token=eyJ... ```