Become a sponsor

本章概要
登录 → Token 获取 → 权限校验 → Token 刷新 → 退出登录的完整认证链路实战说明。
本章将登录 → Token 获取 → 权限校验 → Token 刷新 → 退出登录的完整认证链路串联说明,帮助开发者理解端到端的认证机制。
┌──────────┐ ①获取验证码 ┌──────────┐
│ │ ──────────────────> │ │
│ 前端 │ │ 后端 │
│ Vue3 │ ②登录(账号+密码+验证码) │ FastAPI │
│ │ ──────────────────> │ │
│ │ ③返回 Token │ │
│ │ <────────────────── │ │
│ │ │ │
│ │ ④携带 Token 请求 │ │
│ │ ──────────────────> │ │
│ │ ⑤返回业务数据 │ │
│ │ <────────────────── │ │
│ │ │ │
│ │ ⑥刷新 Token │ │
│ │ ──────────────────> │ │
│ │ ⑦返回新 Token │ │
│ │ <────────────────── │ │
│ │ │ │
│ │ ⑧退出登录 │ │
│ │ ──────────────────> │ │
└──────────┘ └──────────┘登录前需先获取图形验证码,防止暴力破解。
GET /api/captcha{
"code": 0,
"msg": "操作成功",
"data": {
"captcha": "data:image/png;base64,iVBORw0KGgo...",
"key": "captcha:a3f8b2c1d4e5"
}
}captcha:Base64 编码的验证码图片,前端直接赋值给 <img> 的 srckey:验证码唯一标识,登录时需回传// 获取验证码
const { data } = await getCaptchaApi();
captchaImg.value = data.captcha;
captchaKey.value = data.key;# src/api/v1/endpoints/auth.py
@router.get('/captcha')
async def captcha(request: Request):
result = await generate_captcha(request)
id_key = result['idKey']
data = result['data']
img_str = "data:image/png;base64," + data
return R.ok({"captcha": img_str, "key": id_key})验证码通过 Redis 存储,Key 格式为 captcha:{uuid},默认有效期 300 秒。
携带账号、密码、验证码和 Key 发起登录请求。
POST /api/login
Content-Type: application/json
{
"username": "admin",
"password": "123456",
"code": "a3Kp",
"key": "captcha:a3f8b2c1d4e5"
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
username | str | 是 | 登录账号(1-20 字符) |
password | str | 是 | 登录密码(6-128 字符) |
code | str | 是 | 验证码(长度由 CAPTCHA_LENGTH 配置) |
key | str | 是 | 验证码 Key |
{
"code": 0,
"msg": "登录成功",
"data": {
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"must_change_password": false
},
"ok": true
}access_token:JWT 令牌,后续请求需携带must_change_password:是否需要修改密码(检测到默认密码时为 true)// 验证码错误
{"code": 1, "msg": "验证码不正确", "data": null, "ok": false}
// 账号或密码错误
{"code": 1, "msg": "用户名或密码不正确", "data": null, "ok": false}
// 账号被锁定
{"code": 1, "msg": "登录失败次数过多,请280秒后再试", "data": null, "ok": false}1. 检查是否被锁定(IP+用户名维度)
└─ 锁定中 → 返回剩余等待秒数
2. 验证码校验(Lua 原子消费,防重放)
└─ 不正确 → 记录失败次数
3. 查询用户(同步 DB 查询放入线程池)
└─ 不存在 → 记录失败次数
4. 比对密码(bcrypt CPU 密集,放入线程池)
└─ 不匹配 → 记录失败次数
5. 登录成功
├─ 清除失败记录
├─ 检测是否为默认密码
├─ 生成 JWT Token
└─ 返回 Token| 维度 | 策略 | 配置项 |
|---|---|---|
| 失败计数 | IP+用户名维度,Redis 原子递增 | LOGIN_FAIL_MAX=5 |
| 锁定机制 | 达到上限后锁定 | LOGIN_LOCK_SECONDS=300 |
| 验证码防刷 | 窗口期内限制获取次数 | CAPTCHA_RATE_LIMIT=10 |
| 验证码消费 | Lua 原子消费,防重放 | - |
登录成功后,前端将 Token 存入 localStorage,后续所有请求自动携带。
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...// 请求拦截器(自动添加 Token)
axios.interceptors.request.use(config => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
});请求进入 → login_required 依赖
├─ 提取 Authorization 头
├─ 解析 JWT Token
│ ├─ 验证签名(HS256 + JWT_SALT)
│ ├─ 验证过期时间
│ └─ 检查黑名单(Redis)
├─ 注入 request.state
│ ├─ user_id
│ ├─ username
│ └─ realname
└─ 业务代码通过 get_user_id(request) 获取用户信息每个业务接口通过 @permission_required 装饰器校验权限。
请求到达 → permission_required("sys:user:add")
├─ 用户 ID = 1(admin)→ 跳过权限校验,直接放行
└─ 其他用户
├─ 查询用户角色
├─ 查询角色权限列表
├─ 匹配权限字符串
│ ├─ 匹配 → 放行
│ └─ 不匹配 → 返回 "权限不足"
└─ 结果缓存(Redis,菜单变更时失效)sys:{module}:{action}示例:sys:user:add、sys:role:page、sys:article:delete
Token 支持无感刷新,刷新时执行令牌轮换(旧 Token 立即失效)。
POST /api/v1/refreshToken
Authorization: Bearer <old_token>1. 验证原令牌有效性
2. 检查是否已注销(黑名单)
3. 移除 JWT 标准字段(exp/iat/nbf),保留业务数据
4. 生成新令牌
5. 旧令牌加入黑名单
6. 返回新令牌// 响应拦截器:检测 Token 过期
axios.interceptors.response.use(
response => response,
async error => {
if (error.response?.status === 401) {
// 尝试刷新 Token
const newToken = await refreshToken();
if (newToken) {
// 用新 Token 重试原请求
error.config.headers['Authorization'] = `Bearer ${newToken}`;
return axios(error.config);
}
// 刷新失败,跳转登录页
router.push('/login');
}
return Promise.reject(error);
}
);退出时将当前 Token 加入 Redis 黑名单,使其立即失效。
GET /api/v1/logout
Authorization: Bearer <token>{
"code": 0,
"msg": "注销成功",
"data": null,
"ok": true
}1. 提取当前 Token
2. 计算 Token 的 SHA256 指纹
3. 存入 Redis 黑名单(TTL = Token 剩余有效期)
4. 记录退出日志
5. 返回成功token:blacklist:{sha256_fingerprint}前端 后端 Redis 数据库
│ │ │ │
│ ① GET /captcha │ │ │
│ ────────────────────> │ 生成验证码 │ │
│ │ ────────────────────> │ 存储 captcha key │
│ 返回验证码图片+key │ <──────────────────── │ │
│ <──────────────────── │ │ │
│ │ │ │
│ ② POST /login │ │ │
│ (username+pwd+code) │ │ │
│ ────────────────────> │ ① 检查锁定 │ │
│ │ ────────────────────> │ 查询 lock key │
│ │ <──────────────────── │ │
│ │ ② 验证码校验 │ │
│ │ ────────────────────> │ Lua 原子消费 │
│ │ <──────────────────── │ │
│ │ ③ 查询用户 │ │
│ │ ─────────────────────────────────────────> │
│ │ <───────────────────────────────────────── │
│ │ ④ 比对密码 │ │
│ │ ⑤ 生成 JWT Token │ │
│ 返回 Token │ │ │
│ <──────────────────── │ │ │
│ │ │ │
│ ③ GET /api/v1/user/page │ │
│ Authorization: Bearer xxx │ │
│ ────────────────────> │ 解析 Token │ │
│ │ 检查黑名单 │ │
│ │ ────────────────────> │ 查询 blacklist │
│ │ <──────────────────── │ │
│ │ 校验权限 │ │
│ │ 查询业务数据 │ │
│ │ ─────────────────────────────────────────> │
│ 返回业务数据 │ <───────────────────────────────────────── │
│ <──────────────────── │ │ │
│ │ │ │
│ ④ GET /logout │ │ │
│ ────────────────────> │ Token 加入黑名单 │ │
│ │ ────────────────────> │ SET blacklist │
│ 返回注销成功 │ <──────────────────── │ │
│ <──────────────────── │ │ │| 环境变量 | 默认值 | 说明 |
|---|---|---|
JWT_SALT | 无(必填) | JWT 签名密钥,至少 32 字节 |
JWT_EXPIRE_MINUTES | 20 | Token 过期时间(分钟) |
LOGIN_FAIL_MAX | 5 | 登录失败次数上限 |
LOGIN_LOCK_SECONDS | 300 | 登录锁定时间(秒) |
CAPTCHA_LENGTH | 6 | 验证码长度 |
CAPTCHA_EXPIRE | 300 | 验证码有效期(秒) |
认证流程覆盖了从获取验证码到退出登录的完整链路,核心安全机制包括: