登录注册番外篇(二) - Sa-Token:权限认证完全指南
登录注册番外篇(二) - Sa-Token:权限认证完全指南
Prorise第一章. 全局侦听器——订阅框架的关键事件
阶段式学习路径
前三篇笔记解决的都是"如何控制访问"的问题——登录、鉴权、下线。但还有一类需求没有覆盖:当这些事件发生时,我们能不能知道?
用户在哪个 IP 登录了?用哪种设备?账号被踢下线的原因是管理员操作还是被新设备顶替?二级认证是什么时候开启的?这些问题在业务层面非常重要——审计日志、异地登录告警、行为分析都依赖它们。
Sa-Token 的侦听器机制就是为此而生。它允许你订阅框架的关键性事件,在事件触发时执行自定义逻辑,而不需要侵入框架本身的任何代码。
1.1. 工作原理与内置侦听器
Sa-Token 内部在每个关键动作执行时,都会向 事件发布中心 SaTokenEventCenter 广播一个事件。所有已注册的侦听器会按注册顺序依次收到通知并执行各自的回调方法。
框架默认内置了一个侦听器实现 SaTokenListenerForLog,功能是将所有事件以 log 的形式打印到控制台。它默认关闭,通过一行配置开启:
1 | sa-token: |
开启后,用户每次登录、注销、被踢下线,控制台都会打印对应的日志行,方便开发阶段快速感知框架行为。生产环境建议关闭,改用自定义侦听器写入结构化日志。
1.2. 全部事件方法一览
SaTokenListener 接口一共定义了 12 个回调方法,覆盖了从登录到 Session 销毁的完整生命周期。以下逐一说明每个方法的触发时机和参数含义:
登录与注销
1 | /** |
下线事件
1 | /** |
封禁事件
1 | /** |
二级认证事件
1 | /** |
Session 事件
1 | /** |
Token 续期事件
1 | /** |
1.3. 三种实现方式
方式一:实现 SaTokenListener 接口(全量实现)
适合需要处理多个事件的场景。缺点是必须实现接口中的全部方法,即使某些方法你根本不关心,也得写空实现。
方式二:继承 SaTokenListenerForSimple(推荐)
SaTokenListenerForSimple 是框架提供的适配器类,对所有事件提供了空实现。继承它之后,只需重写你关心的方法,其余方法保持默认空实现即可。这是最推荐的方式:
1 |
|
方式三:匿名内部类(轻量场景)
适合只需要临时注册、或在测试代码中快速验证的场景:
1 | SaTokenEventCenter.registerListener(new SaTokenListenerForSimple() { |
1.4. 注册方式与 SaTokenEventCenter 管理 API
自动注册(推荐)
只要实现类上加了 @Component 注解,SpringBoot 启动时会自动将其扫描并注册到事件中心,无需任何额外配置:
1 | // 有这个注解,框架自动发现并注册 |
手动注册
在非 IoC 环境下,或者需要在运行时动态注册侦听器时,使用 SaTokenEventCenter 手动操作:
1 | // 注册一个侦听器 |
SaTokenEventCenter 还提供了完整的侦听器管理能力:
1 | // 获取已注册的所有侦听器 |
多个侦听器可以同时存在,彼此独立,互不影响,按照注册顺序依次接收到事件通知。
1.5. 实战:登录审计日志记录器
System.out.println 能说明机制,却没有实际业务价值。下面用侦听器实现一个完整的登录审计日志记录器,覆盖登录、注销、踢人、顶替四种事件,记录每次操作的时间、账号、Token,以及下线原因。
📄 src/main/java/com/example/authsatoken/listener/AuditLogListener.java(新建)
1 | package com.example.authsatoken.listener; |
这个实现有几个值得注意的设计决策。
Token 脱敏是必须的。完整的 Token 相当于用户的身份凭证,如果日志被攻击者获取,就可以直接冒充用户发起请求。mask() 方法只保留前 8 位,足以在日志中定位问题,又不会造成安全风险。
doKickout 和 doReplaced 分开处理,而不是合并到一个方法里。这两种事件对用户的含义截然不同:被踢下线说明有管理员操作,被顶下线说明可能有异地登录。前者可以推送客服通知,后者更适合触发安全告警——分开处理才能做出有意义的差异化响应。
1.6. 重要坑点:try-catch 是必须的
侦听器的回调方法在 Sa-Token 的主流程中被同步调用——登录动作完成后,框架立即调用所有侦听器的 doLogin 方法,等所有侦听器都执行完才返回结果给调用方。
这意味着:如果你的侦听器代码抛出了未捕获的异常,整个登录流程就会被强制中断,用户会看到一个 500 错误,而不是成功登录。
1 | // ❌ 危险写法:如果数据库操作失败,登录接口直接报 500 |
侦听器中任何可能失败的操作——数据库写入、HTTP 请求、消息推送——都必须用 try-catch 包裹。侦听器的职责是"观察和记录",不应该反过来影响主流程的成败。
1.7. 本章总结
本章回顾
本章完整讲解了 Sa-Token 的全局侦听器机制。从工作原理出发,理解了事件发布中心 SaTokenEventCenter 的广播模型,以及内置 SaTokenListenerForLog 的快速开启方式。在事件方法层面,逐一解释了全部 12 个回调方法的触发时机和每个参数的具体含义,补全了官方文档只有签名没有说明的空白。在实现方式上,比较了直接实现接口、继承 SaTokenListenerForSimple(推荐)、匿名内部类三种方式的适用场景。通过登录审计日志记录器的实战示例,将侦听器能力落地到真实业务中,演示了 Token 脱敏、差异化下线响应、异地登录告警等实践要点。最后着重说明了 try-catch 的必要性——侦听器异常会中断 Sa-Token 主流程,这是使用中最容易忽略的坑。
核心汇总表
| 事件方法 | 触发时机 | 关键参数 |
|---|---|---|
doLogin | 用户登录时 | loginParameter(含设备类型、有效期等登录细节) |
doLogout | 用户主动注销时 | loginId / tokenValue |
doKickout | 被管理员踢下线时 | loginId / tokenValue |
doReplaced | 被新设备登录顶下线时 | loginId / tokenValue |
doDisable | 账号被封禁时 | service(业务标识)/ level(封禁等级)/ disableTime(时长) |
doUntieDisable | 账号被解封时 | service |
doOpenSafe | 开启二级认证时 | service(业务标识)/ safeTime(有效期) |
doCloseSafe | 关闭二级认证时 | service |
doCreateSession | Session 创建时 | id(Session 唯一标识) |
doLogoutSession | Session 注销时 | id |
doRenewTimeout | Token 续期时 | tokenValue / timeout(续期后有效期) |
| 实现方式 | 适用场景 | 优缺点 |
|---|---|---|
实现 SaTokenListener 接口 | 需要处理所有事件 | 必须实现全部方法,代码量大 |
继承 SaTokenListenerForSimple | 只关心部分事件(推荐) | 只重写需要的方法,简洁 |
| 匿名内部类 | 轻量场景 / 测试代码 | 简单快速,但不适合生产 |
| 注册方式 | 适用场景 |
|---|---|
@Component 自动注册 | SpringBoot 项目(推荐) |
SaTokenEventCenter.registerListener() 手动注册 | 非 IoC 环境 / 运行时动态注册 |
| 安全规则 | 说明 |
|---|---|
| try-catch 包裹不安全代码 | 侦听器异常会中断主流程,数据库写入、HTTP 请求等必须保护 |
| Token 脱敏后再记录日志 | 完整 Token 相当于身份凭证,不应出现在日志文件中 |
doKickout 与 doReplaced 分开处理 | 两种下线原因对应不同的业务响应(客服通知 vs 安全告警) |
第二章. 全局过滤器——更底层的请求拦截
阶段式学习路径
第三篇笔记中,我们用 SaInterceptor 拦截器实现了路由鉴权。拦截器已经能满足绝大多数场景,但它并非唯一的选择。Sa-Token 同时提供了全局过滤器,作为拦截器的替代或补充方案。
两者不是为了互相取代而存在的——它们工作在请求处理链的不同层次,各自有擅长的场景。本章先把选型问题讲清楚,再完整介绍过滤器的注册和配置,以及几个容易踩坑的细节。
2.1. 过滤器 vs 拦截器:完整选型指南
理解两者的区别,首先要理解它们在 Spring 请求处理链中的位置:
1 | HTTP 请求 |
过滤器更靠前,在 Spring 的 DispatcherServlet 之前就介入了请求;拦截器在 DispatcherServlet 之后才介入,此时 Spring 已经完成了请求的路由解析。
这个位置差异决定了两者的能力边界:
| 维度 | 拦截器(SaInterceptor) | 过滤器(SaServletFilter) |
|---|---|---|
| 执行时机 | DispatcherServlet 之后 | DispatcherServlet 之前 |
| 异常处理 | 进入 @ExceptionHandler 全局处理 | 不进入,必须在 setError 中手动处理 |
| 静态资源拦截 | 不拦截(Spring 静态资源直接响应) | 可以拦截 |
获取 HandlerMethod | 可以(知道请求将进入哪个方法) | 不可以 |
| 注解鉴权支持 | ✅ 内置,@SaCheckLogin 等注解生效 | ❌ 不支持 |
| WebFlux 支持 | ❌ 无 | ✅ SaReactorFilter |
| 防渗透扫描能力 | 一般 | 更强(执行更早,攻击流量更早被拦截) |
选型建议:
绝大多数 SpringBoot + SpringMVC 项目,优先使用拦截器。原因很简单:异常会自动进入全局处理器,不需要额外编写错误响应逻辑;支持注解鉴权;对日常的登录校验和权限控制完全够用。
以下情况考虑使用过滤器:
- 项目使用 Spring WebFlux(没有拦截器机制,过滤器是唯一选择)
- 需要拦截静态资源的访问(如图片、PDF 需要权限才能下载)
- 需要在请求处理链最早期介入,例如统一设置安全响应头
Sa-Token 同时提供两种机制,不是让谁取代谁,而是让你根据实际业务合理选择。两者也可以同时注册,互不冲突。
2.2. 注册 SaServletFilter
过滤器默认处于关闭状态,需要手动注册为 Spring Bean。与拦截器注册在 WebMvcConfigurer 中不同,过滤器以 @Bean 方式注册:
📄 src/main/java/com/example/authsatoken/config/SaTokenFilterConfig.java(新建)
1 | package com.example.authsatoken.config; |
四个配置方法构成了过滤器的完整生命周期,理解它们的执行顺序和约束范围是正确使用过滤器的关键。
2.3. setBeforeAuth 的正确用途:设置安全响应头
setBeforeAuth 不受 addInclude / addExclude 的限制,对所有进入过滤器的请求都会执行。这个特性看起来像个坑,但其实暗示了它的正确用途:做对所有请求都需要生效的无副作用预处理,最典型的就是设置安全响应头。
安全响应头是一组 HTTP 响应头,告诉浏览器如何更安全地处理响应内容,防御 XSS、点击劫持等常见攻击。将它放在 setBeforeAuth 中,可以保证每一个响应(包括静态资源、错误页面)都携带这些头信息:
1 | .setBeforeAuth(req -> { |
2.4. setError 的正确写法
setError 是过滤器与拦截器相比最容易踩坑的地方。拦截器中,SaInterceptor 抛出的异常会自动进入 @ExceptionHandler 全局处理,我们在番外篇二第四章写的 GlobalExceptionHandler 会帮我们把异常转换成规范的 JSON 响应。
但过滤器中的异常不走这套机制。setAuth 里抛出的任何异常都会被 setError 捕获,setError 的返回值(一个字符串)直接写入 HTTP 响应体,响应就结束了。
这带来两个问题:
问题一:Content-Type 默认是 text/plain
如果不手动设置响应头,前端收到的是纯文本字符串,而不是 application/json,导致前端的 JSON 解析失败。
问题二:SaResult.toString() 不是合法 JSON
SaResult 对象的 toString() 方法返回的是 Java 对象的字符串表示(如 SaResult{code=500, ...}),不是 JSON 格式。需要用 JSON 序列化工具将其转换。
正确写法:
1 | .setError(e -> { |
三种方案的选择建议:项目已使用 Spring Boot 时优先选方案 A(Jackson 已经在依赖中),项目已引入 Hutool 则选方案 B,其余情况用方案 C 兜底。
2.5. 自定义过滤器执行顺序
SaServletFilter 默认注册顺序为 -100(在 Spring Boot 中 Order 值越小执行越早)。如果项目中存在多个过滤器,或者需要 Sa-Token 过滤器在某个自定义过滤器之前 / 之后执行,可以通过 FilterRegistrationBean 包装来指定顺序:
1 |
|
2.6. WebFlux 中注册过滤器
Spring WebFlux 不提供拦截器机制,因此如果你的项目基于 WebFlux(响应式编程模型),过滤器是实现路由鉴权的唯一选择。
Sa-Token 为 WebFlux 提供了 SaReactorFilter,写法与 SaServletFilter 几乎完全一致,只需替换类名:
1 | /** |
WebFlux 项目中引入的是 sa-token-reactor-spring-boot3-starter 依赖,而不是 sa-token-spring-boot3-starter,两个包提供的 API 基本相同,核心区别在于底层响应模型(阻塞 vs 响应式)。
2.7. 本章总结
本章回顾
本章系统讲解了 Sa-Token 全局过滤器的完整用法,并给出了明确的选型建议。选型层面,通过执行时机、异常处理、WebFlux 支持等六个维度的对比,得出"SpringMVC 项目优先用拦截器,WebFlux 项目或需要拦截静态资源时才用过滤器"的结论。在配置层面,详细讲解了 addInclude / addExclude / setBeforeAuth / setAuth / setError 五个配置方法的用途和约束范围,并重点说明了两个高频坑点:setBeforeAuth 不受 exclude 限制因此不适合放鉴权逻辑,setError 必须手动设置 Content-Type 响应头并使用 JSON 序列化工具才能返回正确格式。最后介绍了通过 FilterRegistrationBean 自定义执行顺序,以及 WebFlux 环境下只需将类名换为 SaReactorFilter 的迁移写法。
核心汇总表
| 选型维度 | 拦截器(推荐) | 过滤器 |
|---|---|---|
| 适用框架 | Spring MVC | Spring MVC / WebFlux |
| 异常处理 | 自动进入 @ExceptionHandler | 必须在 setError 中手动处理 |
| 注解鉴权 | 支持 | 不支持 |
| 静态资源拦截 | 不拦截 | 可拦截 |
| 推荐场景 | 绝大多数 Spring Boot 项目 | WebFlux / 静态资源鉴权 / 最早期安全策略 |
| 配置方法 | 作用 | 关键约束 |
|---|---|---|
addInclude(path) | 指定过滤器拦截的路径 | 支持 ** 通配符 |
addExclude(path) | 直接放行的路径 | 不进入 setAuth,但仍会进入 setBeforeAuth |
setBeforeAuth(lambda) | 前置函数,认证前执行 | 不受 include/exclude 限制,适合设置安全响应头 |
setAuth(lambda) | 认证函数,受 include/exclude 约束 | 写法与拦截器中 SaRouter 完全一致 |
setError(lambda) | 异常处理函数,认证函数抛出异常时执行 | 返回值直接输出到前端,必须手动设置 Content-Type |
| 坑点 | 原因 | 解决方案 |
|---|---|---|
setError 前端收到纯文本 | 未设置 Content-Type 响应头 | 在 setError 内调用 SaHolder.getResponse().setHeader(...) |
setError 返回非法 JSON | SaResult.toString() 不是 JSON | 使用 Jackson / Hutool 序列化,或手动拼接 JSON 字符串 |
setBeforeAuth 对被排除的路径也生效 | 不受 include/exclude 限制 | 不在 setBeforeAuth 中做鉴权,只做无副作用的预处理 |
| 安全响应头 | 作用 |
|---|---|
X-Frame-Options: SAMEORIGIN | 防止点击劫持,只允许同域 iframe 嵌入 |
X-XSS-Protection: 1; mode=block | 启用浏览器 XSS 过滤,检测到攻击时停止渲染 |
X-Content-Type-Options: nosniff | 禁止浏览器进行内容类型嗅探 |





