Skip to content

5.2 认证流程实战 ​

概述

本节通过完整的请求链路,展示从登录到接口调用的认证全流程,包括登录、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"
        }
    }
}

失败响应:

场景codemsg
验证码错误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
    }
}

失败响应:

场景codemsg
缺少 refresh_token1缺少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/tokenPOST获取令牌(grant_type=password 或 refresh_token)
/api/oauth2/remove/{token}DELETE吊销令牌
/api/oauth2/check/tokenGET检测令牌有效性

密码模式获取令牌 ​

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

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