Files
rxthinkcmf-wiki/RXThinkCMF_Laravel13_EleVue/zh/core/auth-flow.md
T
2026-10-08 17:50:29 +08:00

20 KiB
Raw Blame History

5.2 认证流程实战

:::tip 概述 本节通过完整的请求链路,展示从登录到接口调用的认证全流程,包括登录、Token 刷新、接口认证、权限校验、租户隔离五个核心环节。 :::

认证全景图

┌─────────────────────────────────────────────────────────────────────┐
│                          认证授权全景                                 │
│                                                                     │
│  ① 登录         POST /api/login(公开接口)                          │
│     用户名+密码+验证码(key/code) → 校验 → 签发双令牌 + 写登录日志      │
│                                                                     │
│  ② 请求 API     GET/POST /api/xxx(auth.jwt 组内)                   │
│     Bearer token → Authenticate → 权限校验 → 注入 userInfo           │
│                                                                     │
│  ③ Token 过期   响应 code=401(HTTP 200)                            │
│     refresh_token → POST /api/refresh → 新令牌对(旧 jti 一次性作废)  │
│                                                                     │
│  ④ 租户隔离     ResolveTenant                                        │
│     解析租户 → 注入 tenantId → Service 层自动过滤                     │
│                                                                     │
│  ⑤ 登出         POST /api/logout → revokeToken(refresh 即时失效)   │
└─────────────────────────────────────────────────────────────────────┘

一、登录流程

完整流程图

POST /api/login
{ "username": "admin", "password": "123456", "key": "captcha-uuid", "code": "a3Bx" }
    │
    ▼
┌─────────────────────────────────────────────────────────┐
│ AuthController::login(LoginRequest $request)            │
│                                                         │
│ 1. LoginRequest 类型注入 → rules() 格式校验             │
│    │ 失败 → {"code":1, "msg":"参数验证失败"}             │
│    ▼                                                     │
│ 2. 读取 key / code 参数(与 captcha 响应字段命名一致)   │
│    CaptchaService::verify($key, $code)                   │
│    │ 仅当 config('captcha.length') > 0 时启用           │
│    │ 失败 → fail("请输入验证码" / "验证码错误")           │
│    ▼                                                     │
│ 3. JwtService::login($username, $password)              │
└─────────────────────────┬───────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│ JwtService::login()                                     │
│                                                         │
│ 1. User::where('username', $username)->first()          │
│    │ 不存在 → BizException("用户名或密码错误")           │
│    ▼                                                     │
│ 2. $user->status 禁用                                    │
│    │ → BizException("账号已被禁用")                      │
│    ▼                                                     │
│ 3. PasswordService::verify($password, $hashed, $salt)   │
│    │ 不匹配 → BizException("用户名或密码错误")           │
│    ▼                                                     │
│ 4. createTokenPair($user)                               │
│    │ 生成 jti(UUID) → 签发 access + refresh             │
│    │ Redis::setex("refresh:{uid}:{jti}", 7天, token)    │
│    │ loadPermissions() → 缓存 perms:{uid}               │
│    ▼                                                     │
│ 5. 返回 { access_token, refresh_token, token_type,      │
│           expires_in, user }                            │
└─────────────────────────┬───────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│ AuthController 续                                       │
│                                                         │
│ 成功 → writeLoginLog(成功) → success($result,'登录成功') │
│ 失败 → writeLoginLog(失败) → fail($e->getMessage())     │
│ (登录日志成功/失败均记录,含 IP 归属地、浏览器)         │
└─────────────────────────────────────────────────────────┘

登录接口详情

请求:

POST /api/login
Content-Type: application/json

{
    "username": "admin",
    "password": "123456",
    "key": "5c62cae9-3bdb-4356-bc4a-c4c61b7c3bea",
    "code": "a3Bx"
}

成功响应:

{
    "code": 0,
    "ok": true,
    "msg": "登录成功",
    "data": {
        "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
        "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
        "token_type": "Bearer",
        "expires_in": 7200,
        "user": {
            "id": 1,
            "username": "admin",
            "realname": "管理员",
            "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 账号已被禁用

二、接口认证流程

中间件链执行顺序

请求 GET /api/user/page
Authorization: Bearer eyJ...
    │
    ▼
┌─────────────────────────────────────────────────────────┐
│ ProxyMiddleware(全局 api 组)                           │
│ 受信代理校验 → 还原真实 REMOTE_ADDR                       │
└─────────────────────────┬───────────────────────────────┘
                          ▼
┌─────────────────────────────────────────────────────────┐
│ Cors(全局 api 组)                                      │
│ OPTIONS 预检 → 204 + CORS 头;普通请求 → 追加 CORS 头     │
└─────────────────────────┬───────────────────────────────┘
                          ▼
┌─────────────────────────────────────────────────────────┐
│ ForceJsonResponse(全局 api 组)                         │
│ Accept 强制 application/json                             │
└─────────────────────────┬───────────────────────────────┘
                          ▼
┌─────────────────────────────────────────────────────────┐
│ Authenticate(auth.jwt,路由组)                          │
│                                                         │
│ 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":"无访问权限…"}  │
└─────────────────────────┬───────────────────────────────┘
                          ▼
┌─────────────────────────────────────────────────────────┐
│ ResolveTenant(tenant,路由组)                          │
│                                                         │
│ 1. 无 userInfo → 放行(未认证请求不注入租户)            │
│ 2. uid=1 → tenantId=0, isSuperAdmin=true                │
│ 3. 非超管 → 查库 tenant_id                              │
│    │ 用户不存在 → HTTP 200                               │
│    │ 租户不存在/禁用/过期 → HTTP 403                      │
│ 4. 注入 attributes['tenantId' / 'tenantInfo']            │
└─────────────────────────┬───────────────────────────────┘
                          ▼
┌─────────────────────────────────────────────────────────┐
│ LogOperation(log.operation,路由组)                    │
│ handle:收集上下文、读 #[OperationLog]、判断是否记录      │
│ terminate:响应后写入操作日志(不阻塞业务)               │
└─────────────────────────┬───────────────────────────────┘
                          ▼
┌─────────────────────────────────────────────────────────┐
│ UserController::page()                                  │
│ return $this->_page($this->service);                    │
│ → Service::pageList() → Model → Database                │
└─────────────────────────┬───────────────────────────────┘
                          ▼
                    JSON 响应返回

(各中间件源码级解析见 4.2 请求处理链路)

权限校验详解

Authenticate::checkPermission($uid, $permissionCode)
    │
    ▼
1. $uid === 1(超级管理员)
   │ 是 → return true(跳过校验)
   ▼
2. Redis::get("perms:{$uid}")                 ← 一级缓存
   │ 命中 → in_array('*:*:*') 或 in_array(code) → 通过
   │ Redis 不可用 → 降级
   ▼
3. Cache::remember("perms_fallback:{$uid}", 300)  ← 二级缓存(5 分钟)
   │ 命中 → 匹配判断
   ▼
4. loadPermissionsFromDb($uid)                ← 三级:查库并回填缓存
   UserRole(用户角色)→ RoleMenu join Menu
   → 条件:menu 未删除、type=1、permission 非空
   │ in_array($permissionCode, $permissions)
   │ 包含 → return true(放行)
   │ 不包含 → return false(HTTP 403)

权限码格式: sys:{模块名}:{操作}

sys:user:add          添加用户
sys:user:update       修改用户
sys:user:delete       删除用户
sys:user:page         用户分页
sys:article:add       添加文章
sys:article:detail    文章详情

菜单/角色菜单数据变更时 MenuObserver / RoleMenuObserver 自动清除相关缓存,权限调整即时生效。

三、Token 刷新流程

流程图

客户端持有 refresh_token,需要换取新令牌对
(第三方接入调用;管理后台前端当前未内置自动续签,见 8.6)
    │
    ▼
┌─────────────────────────────────────────────────────────┐
│ 客户端请求                                                │
│ POST /api/refresh { refresh_token: "..." }              │
└─────────────────────────┬───────────────────────────────┘
                          ▼
┌─────────────────────────────────────────────────────────┐
│ AuthController::refresh()                               │
│ 1. 读取 refresh_token(缺失 → fail("缺少refresh_token")│
│ 2. JwtService::refreshToken($refreshToken)              │
└─────────────────────────┬───────────────────────────────┘
                          ▼
┌─────────────────────────────────────────────────────────┐
│ JwtService::refreshToken()                              │
│ 1. 解析令牌 → 失败 → BizException("无效的refresh_token") │
│ 2. type === 'refresh' → 否则同上                         │
│ 3. Lua 原子脚本:GETDEL refresh:{uid}:{jti}             │
│    │ 不存在(已用过/已吊销)→ "refresh_token已失效"       │
│ 4. User 状态校验 → "用户不存在或已被禁用"                 │
│ 5. createTokenPair() → 新 jti 签发新令牌对写入 Redis      │
└─────────────────────────┬───────────────────────────────┘
                          ▼
┌─────────────────────────────────────────────────────────┐
│ 客户端处理结果                                            │
│ 1. 保存返回的新 access_token / refresh_token(均为新值)   │
│ 2. 后续请求携带新 access_token                            │
└─────────────────────────────────────────────────────────┘

刷新接口详情

请求(主入口):

POST /api/refresh
Content-Type: application/json

{ "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." }

成功响应:

{
    "code": 0,
    "ok": true,
    "msg": "刷新成功",
    "data": {
        "access_token": "eyJ...(新access_token)",
        "refresh_token": "eyJ...(新refresh_token)",
        "token_type": "Bearer",
        "expires_in": 7200
    }
}

失败响应:

场景 code msg
缺少 refresh_token 1 缺少refresh_token
refresh_token 无效/类型错 1 无效的refresh_token
旧令牌已用过(重放) 1 refresh_token已失效
用户不存在/禁用 1 用户不存在或已被禁用

:::warning 一次性刷新(防重放) 旧 refresh_token 刷新后立即从 Redis 删除(Lua 原子操作),重放旧令牌会返回"refresh_token已失效"。客户端必须保存返回的新 refresh_token,否则下次无法续签。 :::

四、完整请求示例

以"查询用户分页"为例,展示一次认证请求的完整链路:

① 前端发起请求
   GET /api/user/page?pageNo=1&pageSize=20
   Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
       │
       ▼
② ProxyMiddleware / Cors / ForceJsonResponse → 依次放行
       │
       ▼
③ Authenticate
   │ 提取 token → parseToken() → { uid:1, username:"admin", type:"access", jti, exp }
   │ type === 'access' ✓
   │ 注入 attributes['userInfo']
   │ checkPermission(1, 'sys:user:page') → uid=1 超管跳过 ✓
       │
       ▼
④ ResolveTenant → uid=1 → tenantId=0, isSuperAdmin=true
       │
       ▼
⑤ 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,
     "msg": "操作成功",
     "data": {
       "records": [...],
       "total": 42,
       "current": 1,
       "size": 20,
       "pages": 3
     }
   }

五、前端 401 处理(实际实现)

ui/ 管理后台当前未内置自动续签:access_token 过期(code=401)时直接提示重新登录(摘自 ui/src/utils/http/axios/index.ts 实际代码):

// 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;

自动续签所需的后端能力已就绪(三、Token 刷新流程 的 POST /api/refresh),后续前端升级时可在此处接入刷新与请求重试逻辑。

六、登出与 OAuth2 兼容接口

登出(令牌即时失效)

# 携带 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/check/token GET 公开 检测令牌有效性
/api/oauth2/remove/{token} DELETE 认证组 吊销令牌

密码模式获取令牌

POST /api/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=admin&password=123456

刷新模式获取令牌

POST /api/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=eyJ...