一、 全链路交互流程 (The Full Lifecycle)
整个流程可以划分为四个阶段:人机验证 -> 身份认证 -> 令牌颁发 -> 业务鉴权。
阶段 1:获取验证码 (初始化)
- 前端 :用户打开登录页,前端发起
GET /system/captcha/get请求。 - 后端 (Controller) :
- 接口标记了
@PermitAll(安全放行)和@TenantIgnore(租户放行)。 - 调用
CaptchaService生成背景图、滑块图、以及一个唯一标识 Token (UUID)。 - Redis 操作 :存储
Key=captcha:Token,Value=正确坐标(X=150)。此时状态隐含为 "未校验"。
- 接口标记了
- 返回 :后端将 图片 Base64 和 Token 返回给前端。
阶段 2:校验验证码 (动作验证)
- 前端 :用户拖动滑块,前端收集 Token 和 用户轨迹坐标 ,发起
POST /system/captcha/check请求。 - 后端 (Controller) :
- 接口同样标记了
@PermitAll。 - 逻辑计算 :从 Redis 取出正确坐标,计算
Math.abs(用户坐标 - 正确坐标)。 - 判定 :
- 失败:返回错误,删除 Redis Key(强制刷新)。
- 成功 :修改 Redis 中该 Key 的状态 (或生成加密凭证),标记为 "已通过 (Passed)"。
- 接口同样标记了
- 返回:告诉前端"验证通过",前端准备发起登录。
阶段 3:登录与颁发令牌 (身份认证)
- 前端 :发起
POST /system/auth/login请求,Payload 包含{ username, password, captchaVerification(Token) }。 - 后端 (AuthService) :
- 步骤 A (凭证检查) :调用
CaptchaService,拿着 Token 去 Redis 查。不计算坐标,只检查该 Token 是否标记为"已通过"。如果不通过或不存在,阻断登录。 - 步骤 B (密码验证):查询 MySQL,比对账号密码。
- 步骤 C (令牌生成) :验证全部通过。生成 Access Token 和 Refresh Token。
- 数据落库 :
- Access Token -> 存入 Redis (短效,用于鉴权) + MySQL (用于管理)。
- Refresh Token -> 存入 MySQL (长效,用于续期)。
- 步骤 A (凭证检查) :调用
- 返回:将双 Token 返回给前端。
阶段 4:业务接口访问 (鉴权访问)
-
前端 :发起业务请求(如
/list-all-simple),Header 携带Authorization: Bearer AccessToken。 -
后端TokenAuthenticationFilter (核心过滤器):
- 请求进入
TokenAuthenticationFilter。 - 查 Redis:验证 Token 是否存在且未过期。
- 存上下文(关键动作) :如果 Redis 命中且有效,执行以下两步操作:
- 构建身份:将 Redis 数据转换为 LoginUser 对象。
- 填充环境:
SecurityContextHolder.setAuthentication(...):为了通过 Spring Security 的后续鉴权。WebFrameworkUtils.setLoginUser(...):为了 Request 级别的日志记录。
- 请求进入
-
后端AuthorizationFilter (权限守门员):
- Spring Security 检查
SecurityContextHolder。发现里面有合法的用户身份。 - 根据配置(如
.anyRequest().authenticated()),决定 放行。
- Spring Security 检查
-
后端 (Controller 层):
- 执行业务逻辑,返回数据。
二、 核心疑问解答
1. 为什么要校验两次验证码?两次逻辑有什么不同?
这是为了平衡 "用户体验 / 服务器性能" 与 "业务安全性"。
| 校验阶段 | 第一次校验 (/captcha/check) | 第二次校验 (/login) |
|---|---|---|
| 发生时机 | 用户松开鼠标/手指的瞬间 | 用户点击"登录"按钮后 |
| 校验内容 | 动作验证(数学计算) 计算 ` | 用户坐标 - 真实坐标 |
| 核心目的 | 1. 即时反馈 :滑错了立马提示,不用等点击登录。 2. 挡箭牌:挡住 99% 的无效流量,防止恶意请求打到昂贵的"密码校验"逻辑上。 | 1. 业务闭环 :防止黑客跳过滑块步骤,直接调用登录接口。 2. 最终确认:确保这个登录请求是"合法人类"发起的。 |
| 资源消耗 | 极低 (纯内存计算) | 极低 (Redis Key 查询) |
总结: 第一次是为了体验和防刷 ,第二次是为了确保流程完整。
2. TokenAuthenticationFilter 的验证逻辑是什么?
TokenAuthenticationFilter 是系统识别"你是谁"的唯一入口。它的验证逻辑简单而粗暴,核心就是 "查 Redis"。
代码逻辑链:
-
提取 (Extract):
- 从 HTTP Header 中获取
Authorization字段。 - 去掉
Bearer前缀,得到纯净的token字符串。 - (若为空,直接放行,交给后续 Security 拦截匿名访问)。
- 从 HTTP Header 中获取
-
查询 (Query):
- 调用
OAuth2TokenApi->OAuth2TokenService。 - 执行
redisTemplate.opsForValue().get(token)。
- 调用
-
判定 (Judge):
- Redis 返回
null:- 结论:Token 无效(可能是伪造的,也可能是过期被 Redis 自动删除了)。
- 动作 :不报错,但不 向
SecurityContext注入用户信息。请求变成"匿名用户"继续往下走(随即被 Security 拦截返回 401)。
- Redis 返回
AccessTokenDO:- 结论:Token 有效。
- 动作 :解析出
userId,userType,scopes,构建LoginUser对象,写入SecurityContextHolder。
- Redis 返回
3. 为什么要设置 SecurityContext?
因为SpringSecurity框架不认accesstoken,SpringSecurity框架下的验证filterAuthorizationFilter 只看SecurityContext是否存有用户信息(代码中为Authentication对象,包含用户信息)。
4. 为什么既要设置 SecurityContext,又要设置 WebFrameworkUtils?
这是为了解决 组件职责不同 以及 生命周期不同步 的问题。
SecurityContextHolder- 作用 :给 Spring Security 看的 。后续的
AuthorizationFilter只认这个上下文,有它才能鉴权通过。 - 生命周期 :较短。Spring Security 为了防止线程池污染,通常会在过滤器链执行完毕前自动清空它。
- 作用 :给 Spring Security 看的 。后续的
WebFrameworkUtils(Request Attribute)- 作用 :给基础设施(如 AccessLogFilter)看的。
- 原因 :访问日志通常是在请求彻底结束(Response 返回)之后才记录的。此时
SecurityContext可能已经被清空了。 - 生命周期 :最长 。它挂载在
HttpServletRequest对象上,只要请求没结束,数据就一直在。