一、Sa-Token简介
Sa-Token是一款轻量级Java权限认证框架,专为解决Web系统的登录认证、权限控制、Session会话等问题而设计。其核心优势在于API简洁、功能丰富、扩展性强,支持注解鉴权、多账号体系、单点登录等场景相比Shiro和Spring Security,Sa-Token的学习成本和集成难度更低,适合快速开发。
Sa-token功能概览
Sa-Token 目前主要五大功能模块:登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权。
- 登录认证 —— 单端登录、多端登录、同端互斥登录、七天内免登录。
- 权限认证 —— 权限认证、角色认证、会话二级认证。
- 踢人下线 —— 根据账号id踢人下线、根据Token值踢人下线。
- 注解式鉴权 —— 优雅的将鉴权与业务代码分离。
- 路由拦截式鉴权 —— 根据路由拦截鉴权,可适配 restful 模式。
- Session会话 —— 全端共享Session,单端独享Session,自定义Session,方便的存取值。
- 持久层扩展 —— 可集成 Redis,重启数据不丢失。
- 前后台分离 —— APP、小程序等不支持 Cookie 的终端也可以轻松鉴权。
- Token风格定制 —— 内置六种 Token 风格,还可:自定义 Token 生成策略。
- 记住我模式 —— 适配 [记住我] 模式,重启浏览器免验证。
- 二级认证 —— 在已登录的基础上再次认证,保证安全性。
- 模拟他人账号 —— 实时操作任意用户状态数据。
- 临时身份切换 —— 将会话身份临时切换为其它账号。
- 同端互斥登录 —— 像QQ一样手机电脑同时在线,但是两个手机上互斥登录。
- 账号封禁 —— 登录封禁、按照业务分类封禁、按照处罚阶梯封禁。
- 密码加密 —— 提供基础加密算法,可快速 MD5、SHA1、SHA256、AES 加密。
- 会话查询 —— 提供方便灵活的会话查询接口。
- Http Basic认证 —— 一行代码接入 Http Basic、Digest 认证。
- 全局侦听器 —— 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作。
- 全局过滤器 —— 方便的处理跨域,全局设置安全响应头等操作。
- 多账号体系认证 —— 一个系统多套账号分开鉴权(比如商城的 User 表和 Admin 表)
- 单点登录 —— 内置三种单点登录模式:同域、跨域、同Redis、跨Redis、前后端分离等架构都可以搞定。
- 单点注销 —— 任意子系统内发起注销,即可全端下线。
- OAuth2.0认证 —— 轻松搭建 OAuth2.0 服务,支持openid模式 。
- 分布式会话 —— 提供共享数据中心分布式会话方案。
- 微服务网关鉴权 —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证。
- RPC调用鉴权 —— 网关转发鉴权,RPC调用鉴权,让服务调用不再裸奔
- 临时Token认证 —— 解决短时间的 Token 授权问题。
- 独立Redis —— 将权限缓存与业务缓存分离。
- Quick快速登录认证 —— 为项目零代码注入一个登录页面。
- 标签方言 —— 提供 Thymeleaf 标签方言集成包,提供 beetl 集成示例。
- jwt集成 —— 提供三种模式的 jwt 集成方案,提供 token 扩展参数能力。
- RPC调用状态传递 —— 提供 dubbo、grpc 等集成包,在RPC调用时登录状态不丢失。
- 参数签名 —— 提供跨系统API调用签名校验模块,防参数篡改,防请求重放。
- 自动续签 —— 提供两种Token过期策略,灵活搭配使用,还可自动续签。
- 开箱即用 —— 提供SpringMVC、WebFlux、Solon 等常见框架集成包,开箱即用。
- 最新技术栈 —— 适配最新技术栈:支持 SpringBoot 3.x,jdk 17。
项目官网:https://sa-token.cc/index.html
二、环境准备
使用springboot前确保已经安装JDK,使用这行命令即可查看当前JDK版本。
java -version
如果没有安装JDK请自行到Oracle官网下载安装,推荐JDK版本在17及以上。
下面将sa-token的起步依赖导入到springboot项目中。
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.40.0</version>
</dependency>
如果不是springboot3.x版本以上的起步依赖导入下面这个
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.34.0</version>
</dependency>
三、核心功能实现
在application.yml文件中添加如下配置:
spring:
application:
name: spring-sa-token
server:
port: 8081
#sa-token configuration
sa-token:
token-name: satoken
active-timeout: -1
same-token-timeout: 2592000
is-concurrent: false
is-share: false
token-style: uuid
is-log: true
1.登录认证
下面我们进行模拟登录认证的流程,新建一个controller,导入以下代码:
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 登录测试
*/
@RestController
@RequestMapping("/acc/")
public class LoginController {
// 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456
@RequestMapping("doLogin")
public SaResult doLogin(String name, String pwd) {
// 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对
if("zhang".equals(name) && "123456".equals(pwd)) {
StpUtil.login(10001);
return SaResult.ok("登录成功");
}
return SaResult.error("登录失败");
}
// 查询登录状态 ---- http://localhost:8081/acc/isLogin
@RequestMapping("isLogin")
public SaResult isLogin() {
return SaResult.ok("是否登录:" + StpUtil.isLogin());
}
// 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo
@RequestMapping("tokenInfo")
public SaResult tokenInfo() {
return SaResult.data(StpUtil.getTokenInfo());
}
// 测试注销 ---- http://localhost:8081/acc/logout
@RequestMapping("logout")
public SaResult logout() {
StpUtil.logout();
return SaResult.ok();
}
}
启动springboot工程后在浏览器输入地址测试。
首先测试登录功能
登录成功后查询登录状态:
查询token信息:
测试退出登录:
2.权限认证
权限认证就是查询一下有没有权限,有就通过,没有就禁止访问。
例如:当前账号拥有权限码集合 ["user-add", "user-delete", "user-get"]
,这时候我来校验权限 "user-update"
,则其结果就是:验证失败,禁止访问。
import cn.dev33.satoken.stp.StpInterface;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义权限加载接口实现类
*/
@Component // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展
public class StpInterfaceImpl implements StpInterface {
/**
* 返回一个账号所拥有的权限码集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询权限
List<String> list = new ArrayList<String>();
list.add("101");
list.add("user.add");
list.add("user.update");
list.add("user.get");
// list.add("user.delete");
list.add("art.*");
return list;
}
/**
* 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询角色
List<String> list = new ArrayList<String>();
list.add("admin");
list.add("super-admin");
return list;
}
}
在案例中,我们重写了StpInterface接口中的getPermissionList和getRoleList集合,这样权限就加入到标识集合中,用户访问某个接口时会去查询权限集合中的权限,如果用户有此权限就通过,反之就禁止访问。
3.踢人下线
所谓踢人下线,核心操作就是找到指定 loginId
对应的 Token
,并设置其失效。
强制注销
StpUtil.logout(10001); // 强制指定账号注销下线
StpUtil.logout(10001, "PC"); // 强制指定账号指定端注销下线
StpUtil.logoutByTokenValue("token"); // 强制指定 Token 注销下线
踢人下线
StpUtil.kickout(10001); // 将指定账号踢下线
StpUtil.kickout(10001, "PC"); // 将指定账号指定端踢下线
StpUtil.kickoutByTokenValue("token"); // 将指定 Token 踢下线
强制注销 和 踢人下线 的区别在于:
强制注销等价于对方主动调用了注销方法,再次访问会提示:Token无效。
踢人下线不会清除Token信息,而是将其打上特定标记,再次访问会提示:Token已被踢下线。
4.注解鉴权
注解鉴权 —— 优雅的将鉴权与业务代码分离!
@SaCheckLogin
: 登录校验 —— 只有登录之后才能进入该方法。@SaCheckRole("admin")
: 角色校验 —— 必须具有指定角色标识才能进入该方法。@SaCheckPermission("user:add")
: 权限校验 —— 必须具有指定权限才能进入该方法。@SaCheckSafe
: 二级认证校验 —— 必须二级认证之后才能进入该方法。@SaCheckHttpBasic
: HttpBasic校验 —— 只有通过 HttpBasic 认证后才能进入该方法。@SaCheckHttpDigest
: HttpDigest校验 —— 只有通过 HttpDigest 认证后才能进入该方法。@SaIgnore
:忽略校验 —— 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。@SaCheckDisable("comment")
:账号服务封禁校验 —— 校验当前账号指定服务是否被封禁。
Sa-Token 使用全局拦截器完成注解鉴权功能,为了不为项目带来不必要的性能负担,拦截器默认处于关闭状态
因此,为了使用注解鉴权,必须手动将 Sa-Token 的全局拦截器注册到项目中
手动注册拦截器
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}
}
注册拦截器之后就可以使用注解鉴权
// 登录校验:只有登录之后才能进入该方法
@SaCheckLogin
@RequestMapping("info")
public String info() {
return "查询用户信息";
}
// 角色校验:必须具有指定角色才能进入该方法
@SaCheckRole("super-admin")
@RequestMapping("add")
public String add() {
return "用户增加";
}
// 权限校验:必须具有指定权限才能进入该方法
@SaCheckPermission("user-add")
@RequestMapping("add")
public String add() {
return "用户增加";
}
// 二级认证校验:必须二级认证之后才能进入该方法
@SaCheckSafe()
@RequestMapping("add")
public String add() {
return "用户增加";
}
// Http Basic 校验:只有通过 Http Basic 认证后才能进入该方法
@SaCheckHttpBasic(account = "sa:123456")
@RequestMapping("add")
public String add() {
return "用户增加";
}
// Http Digest 校验:只有通过 Http Digest 认证后才能进入该方法
@SaCheckHttpDigest(value = "sa:123456")
@RequestMapping("add")
public String add() {
return "用户增加";
}
// 校验当前账号是否被封禁 comment 服务,如果已被封禁会抛出异常,无法进入方法
@SaCheckDisable("comment")
@RequestMapping("send")
public String send() {
return "查询用户信息";
}
设定校验模式
@SaCheckRole
与@SaCheckPermission
注解可以进行设置校验模式
// 注解式鉴权:只要具有其中一个权限即可通过校验
@RequestMapping("atJurOr")
@SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR)
public SaResult atJurOr() {
return SaResult.data("用户信息");
}
mode有两种取值:
SaMode.AND
,标注一组权限,会话必须全部具有才可通过校验。SaMode.OR
,标注一组权限,会话只要具有其一即可通过校验。
角色权限双重验证
假设有以下业务场景:一个接口在具有权限 user.add
或角色 admin
时可以调通。怎么写?
// 角色权限双重 “or校验”:具备指定权限或者指定角色即可通过校验
@RequestMapping("userAdd")
@SaCheckPermission(value = "user.add", orRole = "admin")
public SaResult userAdd() {
return SaResult.data("用户信息");
}
orRole 字段代表权限校验未通过时的次要选择,两者只要其一校验成功即可进入请求方法,其有三种写法:
- 写法一:
orRole = "admin"
,代表需要拥有角色 admin 。 - 写法二:
orRole = {"admin", "manager", "staff"}
,代表具有三个角色其一即可。 - 写法三:
orRole = {"admin, manager, staff"}
,代表必须同时具有三个角色。
忽略认证
@SaCheckLogin
@RestController
public class TestController {
// ... 其它方法
// 此接口加上了 @SaIgnore 可以游客访问
@SaIgnore
@RequestMapping("getList")
public SaResult getList() {
// ...
return SaResult.ok();
}
}
5.路由注解鉴权
注册Sa-token路由拦截器
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
// 注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
.addPathPatterns("/**")
.excludePathPatterns("/user/doLogin");
}
}
以上代码注册了一个基于 StpUtil.checkLogin()
的登录校验拦截器,并且排除了/user/doLogin
接口用来开放登录(除了/user/doLogin
以外的所有接口都需要登录才能访问)。
校验函数详解
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
// 注册 Sa-Token 的拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册路由拦截器,自定义认证规则
registry.addInterceptor(new SaInterceptor(handler -> {
// 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录
SaRouter.match("/**", "/user/doLogin", r -> StpUtil.checkLogin());
// 角色校验 -- 拦截以 admin 开头的路由,必须具备 admin 角色或者 super-admin 角色才可以通过认证
SaRouter.match("/admin/**", r -> StpUtil.checkRoleOr("admin", "super-admin"));
// 权限校验 -- 不同模块校验不同权限
SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice"));
SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment"));
// 甚至你可以随意的写一个打印语句
SaRouter.match("/**", r -> System.out.println("----啦啦啦----"));
// 连缀写法
SaRouter.match("/**").check(r -> System.out.println("----啦啦啦----"));
})).addPathPatterns("/**");
}
}
6.自定义token风格
// 1. token-style=uuid —— uuid风格 (默认风格)
"623368f0-ae5e-4475-a53f-93e4225f16ae"
// 2. token-style=simple-uuid —— 同上,uuid风格, 只不过去掉了中划线
"6fd4221395024b5f87edd34bc3258ee8"
// 3. token-style=random-32 —— 随机32位字符串
"qEjyPsEA1Bkc9dr8YP6okFr5umCZNR6W"
// 4. token-style=random-64 —— 随机64位字符串
"v4ueNLEpPwMtmOPMBtOOeIQsvP8z9gkMgIVibTUVjkrNrlfra5CGwQkViDjO8jcc"
// 5. token-style=random-128 —— 随机128位字符串
"nojYPmcEtrFEaN0Otpssa8I8jpk8FO53UcMZkCP9qyoHaDbKS6dxoRPky9c6QlftQ0pdzxRGXsKZmUSrPeZBOD6kJFfmfgiRyUmYWcj4WU4SSP2ilakWN1HYnIuX0Olj"
// 6. token-style=tik —— tik风格
"gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__"
7.账号封禁
对指定账号进行封禁:
// 封禁指定账号
StpUtil.disable(10001, 86400);
参数含义:
- 参数1:要封禁的账号id。
- 参数2:封禁时间,单位:秒,此为 86400秒 = 1天(此值为 -1 时,代表永久封禁)。
对于正在登录的账号,将其封禁并不会使它立即掉线,如果我们需要它即刻下线,可采用先踢再封禁的策略
// 先踢下线
StpUtil.kickout(10001);
// 再封禁账号
StpUtil.disable(10001, 86400);
待到下次登录时,我们先校验一下这个账号是否已被封禁
// 校验指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException`
StpUtil.checkDisable(10001);
// 通过校验后,再进行登录:
StpUtil.login(10001);
账号封禁模块的所有方法
// 封禁指定账号
StpUtil.disable(10001, 86400);
// 获取指定账号是否已被封禁 (true=已被封禁, false=未被封禁)
StpUtil.isDisable(10001);
// 校验指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException`
StpUtil.checkDisable(10001);
// 获取指定账号剩余封禁时间,单位:秒,如果该账号未被封禁,则返回-2
StpUtil.getDisableTime(10001);
// 解除封禁
StpUtil.untieDisable(10001);
8.密码加密
常见的加密算法
// md5加密
SaSecureUtil.md5("123456");
// sha1加密
SaSecureUtil.sha1("123456");
// sha256加密
SaSecureUtil.sha256("123456");
对称加密
// 定义秘钥和明文
String key = "123456";
String text = "Sa-Token 一个轻量级java权限认证框架";
// 加密
String ciphertext = SaSecureUtil.aesEncrypt(key, text);
System.out.println("AES加密后:" + ciphertext);
// 解密
String text2 = SaSecureUtil.aesDecrypt(key, ciphertext);
System.out.println("AES解密后:" + text2);
非对称加密
// 定义私钥和公钥
String privateKey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAO+wmt01pwm9lHMdq7A8gkEigk0XKMfjv+4IjAFhWCSiTeP7dtlnceFJbkWxvbc7Qo3fCOpwmfcskwUc3VSgyiJkNJDs9ivPbvlt8IU2bZ+PBDxYxSCJFrgouVOpAr8ar/b6gNuYTi1vt3FkGtSjACFb002/68RKUTye8/tdcVilAgMBAAECgYA1COmrSqTUJeuD8Su9ChZ0HROhxR8T45PjMmbwIz7ilDsR1+E7R4VOKPZKW4Kz2VvnklMhtJqMs4MwXWunvxAaUFzQTTg2Fu/WU8Y9ha14OaWZABfChMZlpkmpJW9arKmI22ZuxCEsFGxghTiJQ3tK8npj5IZq5vk+6mFHQ6aJAQJBAPghz91Dpuj+0bOUfOUmzi22obWCBncAD/0CqCLnJlpfOoa9bOcXSusGuSPuKy5KiGyblHMgKI6bq7gcM2DWrGUCQQD3SkOcmia2s/6i7DUEzMKaB0bkkX4Ela/xrfV+A3GzTPv9bIBamu0VIHznuiZbeNeyw7sVo4/GTItq/zn2QJdBAkEA8xHsVoyXTVeShaDIWJKTFyT5dJ1TR++/udKIcuiNIap34tZdgGPI+EM1yoTduBM7YWlnGwA9urW0mj7F9e9WIQJAFjxqSfmeg40512KP/ed/lCQVXtYqU7U2BfBTg8pBfhLtEcOg4wTNTroGITwe2NjL5HovJ2n2sqkNXEio6Ji0QQJAFLW1Kt80qypMqot+mHhS+0KfdOpaKeMWMSR4Ij5VfE63WzETEeWAMQESxzhavN1WOTb3/p6icgcVbgPQBaWhGg==";
String publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvsJrdNacJvZRzHauwPIJBIoJNFyjH47/uCIwBYVgkok3j+3bZZ3HhSW5Fsb23O0KN3wjqcJn3LJMFHN1UoMoiZDSQ7PYrz275bfCFNm2fjwQ8WMUgiRa4KLlTqQK/Gq/2+oDbmE4tb7dxZBrUowAhW9NNv+vESlE8nvP7XXFYpQIDAQAB";
// 文本
String text = "Sa-Token 一个轻量级java权限认证框架";
// 使用公钥加密
String ciphertext = SaSecureUtil.rsaEncryptByPublic(publicKey, text);
System.out.println("公钥加密后:" + ciphertext);
// 使用私钥解密
String text2 = SaSecureUtil.rsaDecryptByPrivate(privateKey, ciphertext);
System.out.println("私钥解密后:" + text2);
生成公、私钥
// 生成一对公钥和私钥,其中Map对象 (private=私钥, public=公钥)
System.out.println(SaSecureUtil.rsaGenerateKeyPair());
Base64编码与解码
// 文本
String text = "Sa-Token 一个轻量级java权限认证框架";
// 使用Base64编码
String base64Text = SaBase64Util.encode(text);
System.out.println("Base64编码后:" + base64Text);
// 使用Base64解码
String text2 = SaBase64Util.decode(base64Text);
System.out.println("Base64解码后:" + text2);
Bcrypt加密
// 使用方法
String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt());
// 使用checkpw方法检查被加密的字符串是否与原始字符串匹配:
BCrypt.checkpw(candidate_password, stored_hash);
// gensalt方法提供了可选参数 (log_rounds) 来定义加盐多少,也决定了加密的复杂度:
String strong_salt = BCrypt.gensalt(10);
String stronger_salt = BCrypt.gensalt(12);
9.全局监听器
Sa-Token 提供一种侦听器机制,通过注册侦听器,你可以订阅框架的一些关键性事件,例如:用户登录、退出、被踢下线等。
自定义监听器实现
/**
* 自定义侦听器的实现
*/
@Component
public class MySaTokenListener implements SaTokenListener {
/** 每次登录时触发 */
@Override
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
System.out.println("---------- 自定义侦听器实现 doLogin");
}
/** 每次注销时触发 */
@Override
public void doLogout(String loginType, Object loginId, String tokenValue) {
System.out.println("---------- 自定义侦听器实现 doLogout");
}
/** 每次被踢下线时触发 */
@Override
public void doKickout(String loginType, Object loginId, String tokenValue) {
System.out.println("---------- 自定义侦听器实现 doKickout");
}
/** 每次被顶下线时触发 */
@Override
public void doReplaced(String loginType, Object loginId, String tokenValue) {
System.out.println("---------- 自定义侦听器实现 doReplaced");
}
/** 每次被封禁时触发 */
@Override
public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {
System.out.println("---------- 自定义侦听器实现 doDisable");
}
/** 每次被解封时触发 */
@Override
public void doUntieDisable(String loginType, Object loginId, String service) {
System.out.println("---------- 自定义侦听器实现 doUntieDisable");
}
/** 每次二级认证时触发 */
@Override
public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {
System.out.println("---------- 自定义侦听器实现 doOpenSafe");
}
/** 每次退出二级认证时触发 */
@Override
public void doCloseSafe(String loginType, String tokenValue, String service) {
System.out.println("---------- 自定义侦听器实现 doCloseSafe");
}
/** 每次创建Session时触发 */
@Override
public void doCreateSession(String id) {
System.out.println("---------- 自定义侦听器实现 doCreateSession");
}
/** 每次注销Session时触发 */
@Override
public void doLogoutSession(String id) {
System.out.println("---------- 自定义侦听器实现 doLogoutSession");
}
/** 每次Token续期时触发 */
@Override
public void doRenewTimeout(String tokenValue, Object loginId, long timeout) {
System.out.println("---------- 自定义侦听器实现 doRenewTimeout");
}
}
10.全局过滤器
拦截器与过滤器区别:
- 相比于拦截器,过滤器更加底层,执行时机更靠前,有利于防渗透扫描。
- 过滤器可以拦截静态资源,方便我们做一些权限控制。
- 部分Web框架根本就没有提供拦截器功能,但几乎所有的Web框架都会提供过滤器机制。
但是过滤器也有一些缺点,比如:
- 由于太过底层,导致无法率先拿到
HandlerMethod
对象,无法据此添加一些额外功能。 - 由于拦截的太全面了,导致我们需要对很多特殊路由(如
/favicon.ico
)做一些额外处理。 - 在Spring中,过滤器中抛出的异常无法进入全局
@ExceptionHandler
,我们必须额外编写代码进行异常处理。
Sa-Token同时提供过滤器和拦截器机制,不是为了让谁替代谁,而是为了让大家根据自己的实际业务合理选择,拥有更多的发挥空间。
/**
* [Sa-Token 权限认证] 配置类
*/
@Configuration
public class SaTokenConfigure {
/**
* 注册 [Sa-Token全局过滤器]
*/
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
// 指定 拦截路由 与 放行路由
.addInclude("/**").addExclude("/favicon.ico") /* 排除掉 /favicon.ico */
// 认证函数: 每次请求执行
.setAuth(obj -> {
System.out.println("---------- 进入Sa-Token全局认证 -----------");
// 登录认证 -- 拦截所有路由,并排除/user/doLogin 用于开放登录
SaRouter.match("/**", "/user/doLogin", () -> StpUtil.checkLogin());
// 更多拦截处理方式,请参考“路由拦截式鉴权”章节 */
})
// 异常处理函数:每次认证函数发生异常时执行此函数
.setError(e -> {
System.out.println("---------- 进入Sa-Token异常处理 -----------");
return SaResult.error(e.getMessage());
})
// 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入)
.setBeforeAuth(r -> {
// ---------- 设置一些安全响应头 ----------
SaHolder.getResponse()
// 服务器名称
.setServer("sa-server")
// 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以
.setHeader("X-Frame-Options", "SAMEORIGIN")
// 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面
.setHeader("X-XSS-Protection", "1; mode=block")
// 禁用浏览器内容嗅探
.setHeader("X-Content-Type-Options", "nosniff")
;
})
;
}
}
四、常见问题
- Token失效场景:默认30天无操作失效,可通过
activity-timeout
调整 - 多端登录控制:设置
is-concurrent: false
禁止并发登录 - 跨域问题:需在拦截器中配置CORS或使用
@CrossOrigin
注解
五、总结
Sa-Token优势:
- 开箱即用,API简洁
- 支持注解鉴权与RBAC模型
- 无缝整合Spring生态
- 提供JWT、单点登录等扩展方案
适用场景:中小型Web应用、微服务架构、快速开发项目