Skip to content

5.1 用户认证(JWT) ​

概述

JWT(JSON Web Token)是系统的核心认证机制。采用双令牌设计:access_token 用于接口认证(2小时),refresh_token 用于令牌刷新(7天)。无状态、可水平扩展,前后端分离场景下的标准方案。

双令牌机制 ​

text
┌──────────┐                    ┌──────────┐
│ 用户登录  │──── username ────►│          │
│          │──── password ────►│JwtService│
│          │──── code/key ────►│  .login()│
└──────────┘                    └────┬─────┘
                                     │
                   ┌─────────────────┼─────────────────┐
                   ▼                 ▼                 ▼
            access_token      refresh_token      user info
            (2小时有效)        (7天有效)         (基础信息)

两种令牌的区别 ​

特性access_tokenrefresh_token
用途接口认证(每个 API 请求携带)刷新令牌(仅在 access_token 过期时使用)
有效期2 小时(7200 秒)7 天(604800 秒)
payload.typeaccessrefresh
传递方式Authorization: Bearer <token>POST /oauth2/token 请求体
校验方AuthMiddlewareJwtService::refreshToken

令牌流转全生命周期 ​

text
1. 登录
   POST /api/login {username, password, code, key}
       │
       ▼
   JwtService::login()
       │ 签发 access_token + refresh_token
       ▼
   前端存储到 localStorage

2. 请求 API
   GET /api/user/page
   Authorization: Bearer <access_token>
       │
       ▼
   AuthMiddleware::handle()
       │ 解析 → 校验签名 → 校验有效期 → 校验 type=access
       │ 注入 $request->userInfo
       ▼
   Controller → Logic → Model → 响应

3. Token 过期
   前端响应拦截器检测 code=401
       │
       ▼
   POST /api/oauth2/token {grant_type: refresh_token, refresh_token: <refresh_token>}
       │
       ▼
   JwtService::refreshToken()
       │ 校验 type=refresh → 校验用户状态 → 签发新令牌对
       ▼
   前端更新 localStorage 中的令牌

4. 登出
   前端清除 localStorage 中的令牌
   (JWT 无状态,服务端无需处理)

配置项 ​

配置键环境变量默认值说明
jwt.secretJWT.SECRET内置默认密钥JWT 签名密钥,生产环境务必修改
jwt.algo—HS256签名算法
jwt.access_ttlJWT.ACCESS_TTL7200access_token 有效期(秒),2 小时
jwt.refresh_ttlJWT.REFRESH_TTL604800refresh_token 有效期(秒),7 天
jwt.issuer—rxthinkcmf签发者标识(iss 字段)
jwt.header_name—Authorization请求头名称
jwt.header_prefix—Bearer 请求头前缀

密钥安全

生产环境务必通过 .env 设置 JWT.SECRET 为强随机密钥:

bash
php -r "echo bin2hex(random_bytes(32));"

核心类说明 ​

Jwt 工具类(extend/jwt/Jwt.php) ​

对 firebase/php-jwt 的薄封装,只做令牌的编解码与提取,不涉及业务逻辑。

方法说明调用方
encode($payload, $ttl)签发 Token(自动补齐 iss/iat/exp)内部方法
decode($token)解码 Token(校验签名+有效期)AuthMiddleware、JwtService
getTokenFromHeader()从请求头提取 TokenAuthMiddleware
createAccessToken($uid, $username)签发 access_tokenJwtService
createRefreshToken($uid, $username)签发 refresh_tokenJwtService

异常处理: decode() 将 firebase 的具体异常统一转为 RuntimeException(code=401):

原始异常转换后消息
ExpiredExceptiontoken已过期
SignatureInvalidExceptiontoken签名无效
其他异常token无效: 原始信息

JwtService 服务类(app/service/JwtService.php) ​

负责登录业务流程,调用 Jwt 工具类签发与刷新令牌。

方法说明返回值
login($username, $password)用户登录,签发令牌对['access_token', 'refresh_token', 'token_type', 'expires_in', 'user']
refreshToken($refreshToken)用 refresh_token 换新令牌对['access_token', 'refresh_token', 'token_type', 'expires_in']
parseToken($token)解析 access_token 获取 payloadobject(含 uid、username、exp)
checkToken($token)检测令牌有效性(不抛异常)['valid' => true/false, ...]
revokeToken($token)吊销令牌(当前简化实现)bool

Token Payload 结构 ​

access_token payload ​

json
{
    "uid": 1,
    "username": "admin",
    "type": "access",
    "iss": "rxthinkcmf",
    "iat": 1727000000,
    "exp": 1727007200
}

refresh_token payload ​

json
{
    "uid": 1,
    "username": "admin",
    "type": "refresh",
    "iss": "rxthinkcmf",
    "iat": 1727000000,
    "exp": 1727604800
}

登录流程详解 ​

text
POST /api/login
{ "username": "admin", "password": "123456", "code": "a3Bx", "key": "xxx" }
    │
    ▼
1. CaptchaService::check($code, $key)
   │ 验证码错误 → {"code":1, "msg":"验证码错误"}
   ▼
2. User::withoutGlobalScope(['soft_delete'])->where('username', $username)->find()
   │ 不存在 → {"code":1, "msg":"用户名或密码错误"}
   ▼
3. $user->status != 1
   │ 禁用 → {"code":1, "msg":"账号已被禁用"}
   ▼
4. PasswordService::verify($password, $user->password, $user->salt)
   │ 不匹配 → {"code":1, "msg":"用户名或密码错误"}
   ▼
5. Jwt::createAccessToken($uid, $username) + Jwt::createRefreshToken(...)
   │
   ▼
6. 返回
   {
     "code": 0,
     "msg": "登录成功",
     "data": {
       "accessToken": "eyJ...",
       "refreshToken": "eyJ...",
       "tokenType": "Bearer",
       "expiresIn": 7200,
       "user": { "id": 1, "username": "admin", "realname": "管理员", "avatar": "..." }
     }
   }

安全设计

  • 用户不存在和密码错误统一提示"用户名或密码错误",避免暴露账号是否存在
  • 验证码校验在密码校验之前,防止暴力破解
  • 已删除用户通过 withoutGlobalScope 查询但不返回,统一走"用户名或密码错误"

Token 解析流程(AuthMiddleware) ​

text
请求进入 AuthMiddleware
    │
    ▼
1. 检查排除路由(login/captcha/oauth2 等直接放行)
    │
    ▼
2. Jwt::getTokenFromHeader()
   │ 从 Authorization 头提取 Token
   │ 空 → {"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] 注解 → 校验权限
   │ 无权限 → {"code":403, "msg":"无访问权限"}
   ▼
7. 放行 → $next($request)

Token 刷新流程 ​

text
POST /api/oauth2/token
{ "grant_type": "refresh_token", "refresh_token": "eyJ..." }
    │
    ▼
1. Jwt::decode($refreshToken)
   │ 过期/无效 → {"code":1, "msg":"无效的refresh_token"}
   ▼
2. 校验 $decoded->type === 'refresh'
   │ 非 refresh → {"code":1, "msg":"无效的refresh_token"}
   ▼
3. User::find($decoded->uid)
   │ 不存在或禁用 → {"code":1, "msg":"用户不存在或已被禁用"}
   ▼
4. 签发新 access_token + 新 refresh_token
   │
   ▼
5. 返回新令牌对

"刷新即换新"策略

刷新时旧 refresh_token 不显式作废,新旧令牌在各自有效期内都可用。如需一次性刷新(旧令牌刷新后立即失效),应结合 Redis 黑名单机制。

前端集成 ​

登录与存储 ​

typescript
// 登录
const response = await login({ username, password, code, key });
localStorage.setItem('access_token', response.data.accessToken);
localStorage.setItem('refresh_token', response.data.refreshToken);

请求拦截器(自动携带 Token) ​

typescript
// 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;
});

响应拦截器(自动刷新 Token) ​

typescript
axios.interceptors.response.use(
    response => response,
    async error => {
        const { config, response } = error;

        if (response?.status === 401 && !config._retry) {
            config._retry = true;

            // 尝试用 refresh_token 刷新
            const refreshToken = localStorage.getItem('refresh_token');
            if (refreshToken) {
                try {
                    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();
                    router.push('/login');
                }
            }
        }

        return Promise.reject(error);
    }
);

安全建议 ​

建议说明
生产环境修改密钥JWT.SECRET 使用 32 字节以上随机字符串
HTTPS 传输Token 明文传输,必须使用 HTTPS
合理设置有效期access_token 不宜过长(2小时),refresh_token 按需调整
敏感操作二次验证修改密码、删除数据等操作建议额外校验
Token 黑名单生产环境建议用 Redis 实现 Token 吊销(当前为简化实现)
前端安全存储避免将 Token 存入 Cookie(防 CSRF),推荐 localStorage

错误响应汇总 ​

场景HTTP 状态codemsg
未提供 Token200401未提供认证令牌
Token 过期200401token已过期
Token 签名无效200401token签名无效
Token 类型错误200401无效的认证令牌
无权限200403无访问权限
refresh_token 无效2001无效的refresh_token
用户被禁用2001用户不存在或已被禁用

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