# 5.1 用户认证(JWT 双令牌) :::tip 概述 JWT(JSON Web Token)是系统核心认证机制。采用双令牌设计:access_token 用于接口认证(2 小时),refresh_token 用于令牌刷新(7 天)。无状态、可水平扩展,配合 jti 黑名单实现可控的会话撤销。 ::: ## 双令牌机制 ```text ┌──────────┐ ┌──────────┐ │ 用户登录 │──── username ────►│ │ │ │──── password ────►│ auth │ │ │──── code/key ────►│ .Login()│ └──────────┘ └────┬─────┘ │ ┌─────────────────┼─────────────────┐ ▼ ▼ ▼ access_token refresh_token user info (2小时有效) (7天有效) (基础信息) ``` ### 两种令牌的区别 | 特性 | access_token | refresh_token | |------|-------------|---------------| | 用途 | 接口认证(每个 API 请求携带) | 刷新令牌(仅在 access_token 过期时使用) | | 有效期 | 2 小时(7200 秒,`jwt.accessTokenExpire`) | 7 天(604800 秒) | | claims.typ | `access` | `refresh` | | 传递方式 | `Authorization: Bearer ` | POST `/api/oauth2/token` 请求体 | | 校验方 | JWT 中间件 | logic/auth Refresh | ## Claims 结构(utility/jwt,真实实现) ```text 算法 HS256;密钥 jwt.secret;签发者 jwt.issuer(默认 easygoadmin) Claims(golang-jwt/v5 + RegisteredClaims): user_id / tenant_id / username / realname / typ(access|refresh) RegisteredClaims:jti(随机)、iss、iat、exp(exp 必填,Parse 强校验) ``` 关键 API: ```text Sign(ctx, tokenType, userID, tenantID, username, realname) → (token, claims, err) Parse(ctx, tokenStr) HS256 方法限定(防算法混淆)+ 签名/过期/签发者校验, 失败统一 401 "Token无效或已失效" ExpireDuration(ctx, typ) 有效期(黑名单 TTL 计算用) ``` ## 令牌全生命周期 ```text 1. 登录 POST /api/login {username, password, code, key} │ 签发 access_token + refresh_token ▼ 前端存储(access 用于后续请求,refresh 用于续期) 2. 请求 API GET /api/user/page Authorization: Bearer │ JWT 中间件:Parse → 黑名单 Exists → 注入身份 ▼ controller → service → logic → 响应 3. access 到期(前端响应拦截器检测 401) POST /api/oauth2/token {grant_type: refresh_token, refresh_token: } │ 校验 typ=refresh → 旧 refresh 已黑?→ 轮换(旧黑签新) ▼ 返回新令牌对(旧 access 自然过期,旧 refresh 入黑名单防重放) 4. 登出 / 强退 GET /api/logout 或 forceLogout │ 当前 access jti 入黑名单(TTL=剩余有效期) │ 强退额外拉黑 refresh jti(防被踢会话经 refresh 复活) ▼ MarkOffline:在线会话注销 ``` ## 黑名单机制 ```text 键:easygoadmin:token:blacklist:{jti} = "1" TTL:令牌剩余有效期(过期即令牌本身已失效,无需主动清理) 写入时机: * 登出(Logout) * refresh 轮换(旧 refresh 拉黑) * RemoveToken 指定撤销(DELETE /api/oauth2/remove/:token) * 在线用户强退(access + refresh 一并拉黑) 读取方:JWT 中间件每请求 Exists 判定,命中即 401 ``` ## 令牌刷新与撤销接口 ```text POST /api/oauth2/token(公开)Refresh: * grant_type=password 或无 refresh_token → 兼容密码模式,复用 Login * refresh_token 模式: - Parse 校验 + typ 必须为 refresh(1008) - 旧 refresh 已在黑名单 → 1008(防重放) - 旧 refresh 入黑名单(轮换) - 签发新 access+refresh,MarkOnline 刷新在线登记 - 返回同登录格式四字段 GET /api/oauth2/check/token 登录可用;JWT 中间件通过即 {valid:true, userId} DELETE /api/oauth2/remove/:token 登录可用;解析指定 token 的 jti 入黑名单 GET /api/logout 当前 jti 入黑名单 + MarkOffline + 登录日志(log_type=2) ``` ## 登录响应格式(OAuth2 惯例,命名例外) ```json { "access_token": "eyJhbGciOiJIUzI1NiIs…", "refresh_token": "eyJhbGciOiJIUzI1NiIs…", "expires_in": 7200, "token_type": "Bearer" } ``` ::: warning 命名例外 这是全系统唯一使用下划线命名的响应(其余入出参一律驼峰)。前端按 `data.access_token` 读取,**勿改驼峰**。 ::: ## 安全设计要点 + HS256 + `WithValidMethods` 限定,防算法混淆攻击(none/RS256 降级) + exp 必填强校验;iss 与配置比对 + 双令牌分工明确:access 不能当 refresh 用(typ 校验) + 黑名单 TTL 精确等于剩余有效期,存储无冗余 + 无服务端 Cookie 会话——会话固定攻击面不存在 + 配置项:`jwt.secret`(生产必须改)、`jwt.issuer`、`accessTokenExpire`、`refreshTokenExpire`