外观
认证
认证体系总览
认证体系的职责是"识别当前是谁在访问系统",并向请求链路安全地传递操作人上下文。它建立在 app/framework/security/ 之上,为权限、数据范围等提供"当前用户"这一基础。
系统采用 JWT + Redis 会话 的混合模式:
- JWT(access_token / refresh_token):无状态令牌,携带
session_id(即sub)用于定位会话,由SECRET_KEY签名,可独立校验签名与过期时间。 - Redis 会话(USER_SESSION / ACCESS_TOKEN / REFRESH_TOKEN):有状态支撑,保存完整的在线用户信息,支撑在线用户列表、强制下线、滑动续期与令牌吊销。
二者的关系可以用一张图说明:
核心代码分布在四个文件:
| 文件 | 职责 |
|---|---|
framework/security/jwt.py | JWT 的签发与解码校验 |
framework/security/oauth2.py | OAuth2 Bearer 提取器、登录表单模型 |
framework/security/schema.py | JWT 载荷、响应、刷新、退出等数据模型 |
framework/security/auth.py | 会话解析、get_current_user 依赖、AuthSchema 上下文 |
JWT 令牌
jwt.py 封装了两个函数,分别负责签发与验证:
create_access_token(payload):将JWTPayloadSchema转为字典,把datetime类型的exp转为时间戳,使用settings.SECRET_KEY与settings.ALGORITHM签名。decode_access_token(token):校验签名与过期时间,并对各类异常返回语义化提示(无效凭证、已过期、已失效等),统一抛出401的CustomException。
JWTPayloadSchema 的关键字段:
| 字段 | 含义 |
|---|---|
sub | 会话编号 session_id,用于关联 Redis 中的会话信息 |
is_refresh | 是否为刷新令牌,认证时禁止用 refresh_token 当 access_token 使用 |
exp | 过期时间 |
注意:access_token 与 refresh_token 复用同一套载荷结构,仅靠
is_refresh区分用途。刷新接口会显式校验该字段,防止把刷新令牌当访问令牌用。
OAuth2 提取器
oauth2.py 在 FastAPI 原生 OAuth2PasswordBearer 之上做了定制:
CustomOAuth2PasswordBearer:从Authorization头解析scheme与token,并校验前缀与settings.TOKEN_TYPE(默认Bearer)一致,否则抛出统一的401异常。CustomOAuth2PasswordRequestForm:在标准用户名/密码字段之外,扩展出captcha_key、captcha、login_type(PC端 | 移动端),供登录接口直接使用表单提交(主要用于 Swagger 的 Authorize 按钮)。OAuth2Schema:全局可用的依赖实例,token_url指向system/auth/login-form。
会话与在线状态
登录成功后,LoginService.create_token 会把会话信息写入 Redis,键结构如下:
| Redis 键 | 内容 | 过期 |
|---|---|---|
USER_SESSION:{session_id} | 完整在线用户信息(JSON) | 跟随 refresh_token |
ACCESS_TOKEN:{session_id} | access_token 值 | access_token 有效期 |
REFRESH_TOKEN:{session_id} | refresh_token 值 | refresh_token 有效期 |
这套结构的价值:
- 在线用户:
USER_SESSION即在线用户记录,可供管理端"在线用户"模块展示与强制踢出。 - 强制下线:删除对应用户的 Redis 键后,旧令牌在下次请求校验时即被判定为"认证已失效"。
- 令牌吊销:
logout正是删除这三个键。
核心认证依赖
auth.py 是整个认证能力的入口,关键对象如下。
AuthSchema
AuthSchema 是请求级的认证上下文模型,通过依赖注入链传递:
| 字段 | 含义 |
|---|---|
db | 当前请求的数据库会话 |
user | 当前登录用户(已加载部门、角色、岗位等关联) |
check_data_scope | 是否启用数据权限(部门数据范围)检查,默认开启 |
session_id | 会话编号 |
data_permission | 数据权限解析结果缓存,请求级复用,避免重复查部门表 |
get_current_user
get_current_user 是 HTTP 路由最常用的依赖,整合 db_getter、redis_getter 与 OAuth2Schema,最终调用内部 _authenticate 完成认证,返回 AuthSchema。
其认证流程:
要点说明:
- 中间件协同:
framework/web/middleware.py的RequestLogMiddleware会预先解析Authorization头并填充request.state.ctx。_authenticate优先复用该上下文,避免重复解码。 - 在线校验:
_check_token_online检查ACCESS_TOKEN:{session_id}是否存在,防止被强制下线后旧 token 仍可用。 - 滑动续期:当
TOKEN_SLIDING_EXPIRE=True且 token 剩余存活小于有效期一半时,自动延长 access/refresh 的 Redis TTL,实现"活跃会话自动续期"。 - 用户校验:
_load_user_from_db会联表加载部门、角色(含菜单)、岗位,并过滤被停用的角色与岗位;用户不存在或status==1(停用)都会抛出401。
WebSocket 场景使用 get_current_user_ws,差异在于 token 通过查询参数传入(WebSocket 没有请求头),并通过 redis_getter_ws 获取客户端。
在路由中使用
python
from app.framework.security.auth import get_current_user, AuthSchema
@SomeRouter.get("/list")
async def list_controller(auth: AuthSchema = Depends(get_current_user)):
# auth.user 已是校验通过的当前用户
return auth.user登录流程
账号密码登录由 module_system/auth/controller.py 与 service.py 负责,流程如下:
- 密码算法:
PwdUtil基于 PBKDF2-HMAC-SHA256(迭代 60 万次)加盐哈希,结果与 JWT 密钥一样,都是安全存储、不可逆校验。 - 验证码:由
settings.CAPTCHA_ENABLE控制;从 Swagger 文档页(docs/redoc)发起的请求会跳过验证码。 - 日志:无论成功失败都会落登录日志(账号、IP、操作系统、浏览器、登录方式),便于审计与风控。
注册接口 /system/auth/register 复用了同一套 Token 生成逻辑,注册成功后直接签发令牌并登录。
刷新与续期
- 主动刷新:
POST /system/auth/refresh-token?refreshToken=xxx,后端校验刷新令牌合法且会话仍在,重新签发一对令牌,并延长USER_SESSION的 TTL。 - 被动续期(滑动过期):每次请求经过
get_current_user时,若开启TOKEN_SLIDING_EXPIRE且剩余时间小于一半,自动延长在线状态。前端无需额外操作即可保持长会话。
退出登录
POST /system/auth/logout 由 LoginService.logout 实现:解码 token 得到 session_id,删除 ACCESS_TOKEN、REFRESH_TOKEN、USER_SESSION 三个 Redis 键,使该会话立即失效,并写入退出日志。
前端集成
前端位于 apps/web-ele,认证相关 API 集中在 api/core/auth.ts,状态由 store/auth.ts(useAuthStore)与 @vben/stores 的 accessStore/userStore 协作管理。
核心流程:
要点:
- Token 存储:
accessToken与refreshToken存入accessStore,每次请求通过拦截器以Authorization: Bearer {accessToken}形式携带。 - 权限信息下发:登录后调用
/system/auth/get-permission-info,返回{ user, roles, menus, permissions },其中permissions是按钮级权限码(定义在type=3的菜单上),供前端useAccess/v-access做按钮鉴权(详见03-前端手册/权限集成.md)。 - 自动刷新:请求客户端内置
authenticateResponseInterceptor,当响应为401时自动用refreshToken调用/system/auth/refresh-token换新令牌并重试;若刷新失败则跳转登录页。并发请求会进入队列,待刷新完成后统一重放。 - 社交登录:
socialAuthRedirect直接返回完整 URL 并赋值给window.location.href,因为第三方 302 重定向无法用 axios 跟随。 - 退出:
logout()调用/system/auth/logout后清空所有 store 并跳回登录页。
配置项速查
相关配置集中在 app/config/settings.py:
| 配置 | 默认值 | 说明 |
|---|---|---|
SECRET_KEY | 随机串 | JWT 签名密钥,生产务必替换 |
ALGORITHM | HS256 | JWT 签名算法 |
ACCESS_TOKEN_EXPIRE_SECONDS | 43200(12h) | access_token 有效期 |
REFRESH_TOKEN_EXPIRE_SECONDS | 43200(12h) | refresh_token 有效期 |
TOKEN_TYPE | Bearer | token 类型前缀 |
TOKEN_SLIDING_EXPIRE | True | 滑动过期(活跃自动续期) |
CAPTCHA_ENABLE | False | 是否启用验证码 |
OAUTH_DEFAULT_ROLE_IDS | [2] | 社交登录注册用户的默认角色 |
OAUTH_FRONTEND_FALLBACK | http://127.0.0.1:5173/login | 社交登录前端回退地址 |
OAUTH_REDIRECT_ALLOWED_HOSTS | 127.0.0.1:5173 等 | 回调地址白名单 |