sa-token-doc/more/common-questions.md
本篇整理大家在群聊里经常提问的一些问题,如有补充,欢迎提交pr
[[toc]]
可能1::你在 异步上下文 / 响应式上下文 里调用了 Sa-Token 的同步 API,解决方案参考:异步 & Mock 上下文
可能2:访问了一个不存在的路由,而且 SaInterceptor 拦截器里有鉴权代码。
SpringBoot 默认会把 404 请求转发到 /error,如果恰好 SaInterceptor 里有鉴权代码,就会造成:
写入上下文 → 进入拦截器(有上下文,可调用鉴权代码) → 发现是404 → 清除上下文 → 将请求转发至 /error -> 再次进入拦截器(无上下文,不可调用鉴权代码) → 报错:SaTokenContext 上下文尚未初始化。
解决方案:将 "/error" 地址排除在拦截器之外:
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handle -> {
// 鉴权代码 ...
}))
.addPathPatterns("/**")
.excludePathPatterns("/error");
}
}
这个错是说明调用接口的人没有通过登录校验,请注意:通常情况下,异常提示语已经描述清楚了没有通过校验的具体原因:
如果是:未能读取到有效Token
satoken,可通过配置文件 sa-token.token-name: satoken 来更改。is-read-header=false(关闭header读取),此时你再从 header 里提交token,框架就无法读取到。如果是:Token无效:6ad93254-b286-4ec9-9997-4430b0341ca0
is-read-header=false(关闭header读取),然后你从header提交token-A,而框架从Cookie里读取token-B,导致鉴权不通过(框架读取顺序为body->header->cookie)token-name=x-token(自定义token名称),此时你从header提交:satoken:token-A(参数名没对上),然后框架从header里读取不到你提交的token,转而继续从Cookie读取到了token-B。jwtSecretKey 配置项。maxLoginCount 来配置,默认值12,-1代表不做限制。is-concurrent=true, is-share=true的情况下,你和别人共同登录了同一账号,此时对方注销了登录,由于你们使用的是同一个token,导致你这边的会话也失效了。StpUserUtil.login() 颁发的token,你从 StpUtil.checkLogin() 进行校验,永远都是无效token,因为账号体系没对上。如果是:Token已过期:6ad93254-b286-4ec9-9997-4430b0341ca0
如果是:Token已被顶下线:6ad93254-b286-4ec9-9997-4430b0341ca0
is-concurrent=false 的前提下,这个账号又被别人登录了,导致旧登录被挤了下去。StpUtil.replaced(loginId, device) 方法强制顶下线了。如果是:Token已被踢下线:6ad93254-b286-4ec9-9997-4430b0341ca0
StpUtil.kickout(loginId) 方法强制踢下线了。WebMvcConfigurer和WebMvcConfigurationSupport时, 也会导致拦截器失效.
WebMvcConfigurationSupport中配置addResourceHandlers方法开放Swagger等相关静态资源映射, 同时基于Sa-Token添加了WebMvcConfigurer配置addInterceptors方法注册注解拦截器, 这样会导致注解拦截器失效.WebMvcConfigurer和WebMvcConfigurationSupport只选一个配置, 建议统一通过实现WebMvcConfigurer接口进行配置.尝试按照下面的代码测试一下看看:
// 注册拦截器
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
System.out.println("--------- flag 1");
registry.addInterceptor(new SaInterceptor(handle -> {
System.out.println("--------- flag 2,请求进入了拦截器,访问的 path 是:" + SaHolder.getRequest().getRequestPath());
StpUtil.checkLogin(); // 登录校验,只有会话登录后才能通过这句代码
}))
.addPathPatterns("/user/**")
.excludePathPatterns("/user/doLogin");
}
}
在启动时 flag 1 被打印出来,才证明拦截器注册成功了,在访问请求时 flag 2 被打印出来,才证明请求进入了拦截器。
如果拦截器没有注册成功,则:
<!-- - 可能1:SpringBoot 版本较高(`>= 2.6.0`),请尝试在启动类加上 `@EnableWebMvc` 注解再重新启动。 -->SaTokenConfigure 配置类不在启动类的同包或者子包下,导致没有被 SpringBoot 扫描到。@ComponentScan("com.xxx") 注解,导致包扫描范围不正确,请将此注解删除或移动到其它配置类上。SaTokenConfigure 和启动类没有在一个模块,且启动类模块没有引入配置类的模块,导致加载不到。如果拦截器已经注册成功,但请求没有进入拦截器:
.addPathPatterns("/user/**") 拦截住,或者被 .excludePathPatterns("/xxx/xx") 排除掉了。如果请求进入拦截器也成功了,那可能是:
注:以上的排查步骤,对过滤器不生效的情形一样适用。
/error,然后被再次拦截。请确保你访问的 path 有对应的 Controller 承接!@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handle -> {
try {
System.out.println("-------- 前端访问path:" + SaHolder.getRequest().getRequestPath());
StpUtil.checkLogin();
System.out.println("-------- 此 path 校验成功:" + SaHolder.getRequest().getRequestPath());
} catch (Exception e) {
System.out.println("-------- 此 path 校验失败:" + SaHolder.getRequest().getRequestPath());
throw e;
}
})).addPathPatterns("/**");
}
/favicon.ico,所以你需要找出是哪个path被拦截了,怎么找呢?用【可能4】的代码来测试找。context-path 上下文地址,比如 server.servlet.context-path=/shop,注意这个地址是不需要加在拦截器上的:// 这是错误示例,不需要把 context-path 上下文参数写在下面的 excludePathPatterns 地址上。
registry.addInterceptor(new SaInterceptor(hadnle -> StpUtil.checkLogin()))
.addPathPatterns("/**").excludePathPatterns("/shop/user/login");
// 这是正确示例,无论你的 context-path 上下文配置了什么样的值,下面的 excludePathPatterns 地址都不需要写上它
registry.addInterceptor(new SaInterceptor(hadnle -> StpUtil.checkLogin()))
.addPathPatterns("/**").excludePathPatterns("/user/login");
// 以下代码,当你未登录访问 `/user/doLogin` 时,会被第1条规则越过,然后被第2条拦下,校验登录,然后抛出异常:`NotLoginException:xxx`
registry.addInterceptor(new SaInterceptor(handler -> {
SaRouter.match("/**").notMatch("/user/doLogin").check(r -> StpUtil.checkLogin()); // 第1个规则
SaRouter.match("/**").notMatch("/article/getList").check(r -> StpUtil.checkLogin()); // 第2个规则
SaRouter.match("/**").notMatch("/goods/getList").check(r -> StpUtil.checkLogin()); // 第3个规则
})).addPathPatterns("/**");
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handle -> {
// 调用自定义的 excludePaths() 方法获取数据排除鉴权
SaRouter.match("/**").notMatch(excludePaths()).check(r -> StpUtil.checkLogin());
})).addPathPatterns("/**");
}
// 自定义查询排查鉴权的地址方法
public static List<String> excludePaths() {
List<String> list = ... // 从数据源查询...;
return list;
}
如上方法, excludePaths() 可能并不会像你预想的一样正确执行返回相应的值,请在 .notMatch() 处 一律先硬编码写固定死值来测试,这时就有两种情况:
- 情况1:写固定死值时,代码能正常执行了,那说明你自定义的 excludePaths() 方法有问题,执行结果不正确。
- 情况2:写固定也不行,那说明不是 excludePaths() 的问题,那再从其它地方开始排查。
首先,有没有生效的最佳判断方式是,在main方法中加一个打印,看看打印出来的和你配置文件的一致吗:
@SpringBootApplication
public class SaTokenApplication {
public static void main(String[] args) {
SpringApplication.run(SaTokenApplication.class, args);
System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig());
}
}
如果不一致,请排查:
application.yml 中配置,详细参考:框架配置。application.yml 或 application.properties。# 错误示例,多加了 spring 前缀
spring:
sa-token:
token-name: xxx-token
# 错误示例,缩进不对
sa-token:
token-name: xxx-token
# 正确的应该是以 sa-token 开头
sa-token:
token-name: xxx-token
1、可能组件没有注入成功,排查方法为在 main 里打印这个组件,是否为自定义的class限定名:
@SpringBootApplication
public class SaTokenApplication {
public static void main(String[] args) {
SpringApplication.run(SaTokenApplication.class, args);
System.out.println(SaManager.getStpInterface()); // 打印全局的 StpInterface 实现类
}
}
如果打印出的是你的自定义实现类,则证明注入成功,如果不是,则证明没有注入成功,请排查:
@Component 注解,只有加上这个注解,组件才会被 Spring 自动实例化并注入。@ComponentScan 注解,导致包扫描范围不正确,请将此注解删除或移动到其它配置类上。2、这个组件注入成功了,但是还没到执行时机,比如 StpInterface 组件,只有在鉴权时才会触发,如果你的代码仅仅是登录校验,就不会执行到这个组件。
根据以往的处理经验,发生这种情况 90% 的概率是因为你找错了Redis,即:代码连接的Redis和你用管理工具看到的Redis并不是同一个。
你可能会问:我看配置文件明明是同一个啊?
我的回答是:别光看配置文件,不一定准确,在启动时直接执行 SaManager.getSaTokenDao().set("name", "value", 100000);,
随便写入一个值,看看能不能根据你的预期写进这个Redis,如果能的话才能证明代码连接的Reids 和你用管理工具看到的Redis 是同一个,再进行下一步排查。
与之类似的的报错还有:
这些功能有个统一的特点,就是需要多个项目连接同一个 Redis 才能搭建成功,如果连接的不是同一个 Redis,就会导致 Token / ticket 无法互相认证。
你可能会问:我看配置文件明明就是连接的同一个 Redis 啊?
别急,和上一个问题一样,不要凭借肉眼检查下定论,在你的两个服务之间,分别使用以下代码测试一下:
@SpringBootApplication
public class SaTokenApplication {
public static void main(String[] args) {
SpringApplication.run(SaTokenApplication.class, args);
// 写值测试:注意一定要用下列方法测试,不要用自己封装的 RedisUtil 之类的测试
SaManager.getSaTokenDao().set("name", "value", 100000);
}
}
如果都能根据你的预期写进同一个 Redis,那才能证明两个服务确实连接的是同一个 Redis。
实际上,在交流群中提问这些问题的同学,90%的经过以上测试以后,都会发现两者连接的不是同一个 Reids,原因大多是:Redis配置没有生效、使用了 Alone-Redis 之类的……
如果你是剩下的 10%,那么继续排查:两边的 sa-token 配置是否完全一致,比如 token-name 配置不一致,也会导致数据无法相互认证。最好是把所有 sa-token 相关的配置都复制过去,试验一下看看。
is-concurrent=false,不允许同一账号多端登录,有别人登录了这个账号把你顶下去了。is-concurrent=true,但是is-share=false,同一账号每次登录产生不同的 token,默认最高可以同时登录12个客户端,超过将自动注销最原先的会话。StpUtil.logout() 为单 token 注销,StpUtil.logout(10001) 为账号所有 token 注销。sa-token.timeout 配置了 30 天,但是 sa-token.active-timeout 配置了较短的值,超过这个时间无操作,token 就过期了。sa-token.token-name 配置项的值,会导致会话保存的 key 发生改变,效果等同于手动清空了 Redis 数据,需要重新登录。is-read-cookie: false,然后重启项目再测试一下。extends WebMvcConfigurationSupport。@EnableWebMvc。解决方案:不要加 @EnableWebMvc,不要 extends WebMvcConfigurationSupport,要 implements WebMvcConfigurer
如果一定要 extends WebMvcConfigurationSupport ,可以通过手动注册 Spring 上下文初始化过滤器试试:
@Configuration
public class SaTokenConfigure extends WebMvcConfigurationSupport {
// Spring 上下文初始化过滤器 可能由于各种原因没有被注册到,这里手动帮忙注册一下
@Bean
@ConditionalOnMissingBean({ RequestContextListener.class, RequestContextFilter.class })
@ConditionalOnMissingFilterBean(RequestContextFilter.class)
public static RequestContextFilter requestContextFilter() {
System.out.println("--------------------------- 注册了"); // 加个打印语句或者断点确保这里注册到了
return new OrderedRequestContextFilter();
}
}
如果不是以上原因,可以加群提供复现demo。
<!-- 目前能复现此问题的情况是:在项目中有配置类继承 `WebMvcConfigurationSupport` 时,再从 `SaServletFilter` 中调用 `SpringMVCUtil.getRequest()` 就会报错:`非Web上下文无法获取Request`。 解决方案是将 `extends WebMvcConfigurationSupport` 改为 `implements WebMvcConfigurer`。 -->不更新是正常现象,active-timeout不是根据 ttl 计算的,是根据value值计算的,value 记录的是该 Token 最后访问系统的时间戳,
每次验签时用:当前时间 - 时间戳 > active-timeout,来判断这个 Token 是否已经超时。
两者的序列化算法不一致导致的反序列化失败,如果要更改序列化方式,则需要先将 Redis 中历史数据清除,再做更新。
StpUtil.getExtra("name") 报错:this api is disabled。StpUtil.getExtra(key) 是给 sa-token-jwt 插件提供的,不集成这个插件就不能调用这个API,如果是普通模式需要存储自定义参数,请在 SaSession 上存储
// 在登录时缓存参数
StpUtil.getSession().set("name", "zhangsan");
// 然后我们就可以在任意处获取这个参数
String name = StpUtil.getSession().getString("name");
参考:https://juejin.cn/post/7247376558367981627
是。
参考:前后端分离
假设执行如下代码:
@Data
public class User implements Serializable {
private Long userId;
private String username;
private String password;
}
User user = new User();
user.setUserId(10000L);
user.setUsername("oneName");
user.setPassword("onePass");
StpUtil.getSession().set("userObjKey", user); // 这里报错
报错信息如下:
SerializationException: Could not read JSON:
Cannot deserialize value of type `java.lang.Long` from Array value (token `JsonToken.START_ARRAY`)
Springboot 集成 Sa-Token Redis 后, 一旦 Springboot 切换版本就有可能出现此问题
原因是 Redis 里面有之前的 Sa-Token 会话数据, 清空 Redis 即可。
不进入是正常现象, StpInterface 是鉴权接口,在执行鉴权代码时才会进入 StpInterface 实现类,登录认证时不会进入。
Caused by: java.lang.ClassNotFoundException: cn.dev33.satoken.same.SaSameTemplate
一般找不到类,或者找不到方法,都是版本冲突了,使用 Sa-Token 时一定要注意版本对齐,意思是所有和 Sa-Token 相关的依赖都需要版本一致。
比如说你如果一个依赖是 1.32.0,一个是 1.31.0,就会造成无法启动:
<!-- 如下样例:一个是 `1.32.0`,一个是 `1.31.0`, 版本没对齐,就会造成项目无法启动 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.32.0</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-core</artifactId>
<version>1.31.0</version>
</dependency>
请仔细排查你的 pom.xml 文件,是否有 Sa-Token 依赖没对齐,请不要肉眼检查,用全局搜索 "sa-token" 关键词来找,如果是多模块或者微服务项目,就整个项目搜索。
报这个错说明对应 type 的 StpLogic 尚未初始化到全局 StpLogicMap 中,一般会有两种原因造成这种情况:
可尝试添加以下配置解决:
spring.web.resources.add-mappings=false
spring.mvc.throw-exception-if-no-handler-found=true
开启了全局懒加载后,能启动项目,但是访问接口报异常 InvalidContextException: 未能获取有效的上下文处理器, 配置如下:
spring:
main:
lazy-initialization: true
原因是 Sa-Token 自动配置入口类 SaBeanInject 被延迟加载了,只需要手动指定懒加载排除掉 SaBeanInject 就可以了,实现代码如下:
@Configuration
class MyConfiguration {
@Bean
LazyInitializationExcludeFilter integrationLazyInitExcludeFilter() {
return LazyInitializationExcludeFilter.forBeanTypes(SaBeanInject.class);
}
}
报错原因:SpringBoot3.x 版本默认将路由匹配机制由 ant_path_matcher 改为了 path_pattern_parser 模式,
而此模式有一个规则,就是写路由匹配符的时候,不允许 ** 之后再出现内容。例如:/admin/**/info 就是不允许的。
如果你的项目报了这个错,说明你写的路由匹配符出现了上述问题,有三种解决方案:
path_pattern_parser 模式能力,使之可以支持 ** 之后再出现内容。** 之后再出现内容。ant_path_matcher。步骤1:先改项目的:
spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
步骤2:再改 Sa-Token 的:
/**
* 重写路由匹配算法,切换为 ant_path_matcher 模式,使之可以支持 `**` 之后再出现内容
*/
@PostConstruct
public void customRouteMatcher() {
SaStrategy.instance.routeMatcher = (pattern, path) -> {
return SaPatternsRequestConditionHolder.match(pattern, path);
};
}
注意点:
SpringBoot2.x 的 WebFlux或 SC Gateway 项目,按照上述步骤改造,可能会报错
java.lang.NoClassDefFoundError: org/springframework/web/servlet/mvc/condition/PatternsRequestCondition
只需要将“步骤2”中的代码 return SaPatternsRequestConditionHolder.match(pattern, path);
更换为 return SaPathMatcherHolder.getPathMatcher().match(pattern, path); 即可,例如:
/**
* 重写路由匹配算法,切换为 ant_path_matcher 模式,使之可以支持 `**` 之后再出现内容
*/
@PostConstruct
public void customRouteMatcher() {
SaStrategy.instance.routeMatcher = (pattern, path) -> {
return SaPathMatcherHolder.getPathMatcher().match(pattern, path);
};
}
java.lang.NoSuchFieldError: defaultInstancejava.lang.NoSuchFieldError: defaultInstance
at cn.dev33.satoken.spring.pathmatch.SaPathPatternParserUtil.match(SaPathPatternParserUtil.java:40)
at cn.dev33.satoken.reactor.spring.SaTokenContextForSpringReactor.matchPath(SaTokenContextForSpringReactor.java:34)
at cn.dev33.satoken.router.SaRouter.isMatch(SaRouter.java:58)
at cn.dev33.satoken.router.SaRouter.isMatch(SaRouter.java:72)
...
原因:SpringBoot 版本用的太低了,导致一些类不存在。
2.3.x 以上/**
* 重写路由匹配算法,将 PathPatternParser.defaultInstance 改为 SaPathMatcherHolder.getPathMatcher()
*/
@PostConstruct
public void customRouteMatcher() {
SaStrategy.instance.routeMatcher = (pattern, path) -> {
return SaPathMatcherHolder.getPathMatcher().match(pattern, path);
};
}
在低于 2.2.0 时 (不包含2.2.0本身) 的 SpringBoot 项目中引入 Sa-Token 后,项目启动时会报错:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'cn.dev33.satoken.spring.SaBeanInject': Bean instantiation via constructor failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [cn.dev33.satoken.spring.SaBeanInject]: Constructor threw exception; nested exception is java.lang.NoClassDefFoundError: com/fasterxml/jackson/databind/jsontype/PolymorphicTypeValidator
这是由于缺少 jackson 相关依赖导致的,可以手动添加以下依赖来解决:
<!-- SpringBoot 版本过低时,需要追加的包 (低于 2.2.0 时,不包含 2.2.0 本身) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.17.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.17.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.3</version>
</dependency>
在项目根目录进入 cmd,执行 mvn package,然后重新运行试试。
如果不行,先执行 maven clean ,然后删除 .idea 文件夹里除 icon.png 外的所有文件,然后执行 mvn package,然后重新运行试试。
如果还不行,删除整个项目,重新从 git 地址拉取一遍,再运行。
报错原因解析:
Sa-Token 的部分 API 只能在 Web 上下文中才能调用,例如:StpUtil.getLoginId() 获取当前用户Id,这个方法第一步需要先从前端提交的参数里获取 token 值,
当你在 main 方法里调用这个 API 时,由于 main 方法本质上不是一个 Controller 请求,所以框架无法完成 “从前端提交的参数里获取 token 值” 这一步骤,框架就只能抛出异常。
按照此标准,Sa-Token 的 API 可粗浅的分为两大类:
StpUtil.getLoginId()、StpUtil.getTokenValue() 等等。StpUtil.getLoginType()、SaManager.getConfig() 等等。此处无法逐一列出到底哪些 API 属于 “必须依赖 Web 上下文的 API”,因为太多了,你只需要记住关键的一点: 当一个 API 执行的代码需要先从前端请求中获取一些数据时,这个 API 就属于 “必须依赖 Web 上下文的 API”。
如果你的代码报这个错,说明你在不是 Web 上下文中的地方,调用了 “必须依赖 Web 上下文的 API”,请排查:
@Async 注解的方法中调用了 “必须依赖 Web 上下文的 API”。MyBatis-Plus 的 insertFill 自动填充。@PostConstruct 修饰的方法。报错原因解析:
在 sa-token-core 核心包中,Sa-Token 底层不能确认最终运行的 web 容器,所以抽象了 SaTokenContext 接口,对接不同容器时需要注入不同的实现,
通常这个注入工作都是框架自动完成的,你只需要按照文档开始部分集成相应的依赖即可。例如:
SpringBoot 2.x,请引入 sa-token-spring-boot-starter。SpringBoot 3.x,请引入 sa-token-spring-boot3-starter。SpringBoot 4.x,请引入 sa-token-spring-boot4-starter。sa-token-reactor-spring-boot-starter(3.x 用 sa-token-reactor-spring-boot3-starter,4.x 用 sa-token-reactor-spring-boot4-starter)。sa-token-solon-plugin。如果你的代码报 “未能获取有效的上下文处理器” 这个错,大概率是因为你没有正确引入所需的包,导致框架没有注入正确的 SaTokenContext 上下文实现,请排查:
sa-token-spring-boot-starter 依赖,参考:在SpringBoot环境集成sa-token-reactor-spring-boot-starter 依赖,参考:在WebFlux环境集成spring-boot-starter-web就是 SpringMVC 环境。spring-boot-starter-webflux 就是WebFlux环境。SpringBoot 3.x 或 SpringBoot 4.x,请分别引入 sa-token-spring-boot3-starter 或 sa-token-spring-boot4-starter,不要错误引入 sa-token-spring-boot-starter,不然会导致框架报错。是的,不同于shiro等框架,Sa-Token不会在登录流程中强插一脚,开发者比对完用户的账号和密码之后,只需要调用StpUtil.login(id)通知一下框架即可
// 会话登录接口
@RequestMapping("doLogin")
public SaResult doLogin(String name, String pwd) {
// 第一步:比对前端提交的账号名称、密码
if("zhang".equals(name) && "123456".equals(pwd)) {
// 第二步:比对成功后,调用通知框架,xxx账号登录成功
StpUtil.login(10001);
return SaResult.ok("登录成功");
}
return SaResult.error("登录失败");
}
可以,在全局异常拦截器里捕获NotPermissionException,可以通过getPermission()获取没有通过认证的权限码,可以据此自定义返回信息
@RestControllerAdvice
public class GlobalExceptionHandler {
// 全局 NotPermissionException 异常捕获
@ExceptionHandler(NotPermissionException.class)
public SaResult handlerException(NotPermissionException e) {
e.printStackTrace();
return SaResult.error("缺少权限:" + e.getPermission());
}
}
框架没有提供直接的 API,但你有以下两种方式可以做到这一点:
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 路由拦截鉴权
registry.addInterceptor(new SaInterceptor(r -> {
// 路由拦截鉴权的代码 ...
}).isAnnotation(false)).addPathPatterns("/**");
// 打开注解鉴权
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}
}
如上,第一个完成路由拦截鉴权功能,第二个完成注解鉴权功能。
无论什么模型,只要能把一个用户具有的所有权限塞到一个List里返回给框架,就能集成
每次鉴权时执行,例如你调用了 StpUtil.checkgetPermission("xxx") 方法,框架就会调用底层的 StpInterface#getPermissionList 方法来获取权限数据。
如果你的 getPermissionList 里有读数据库的代码,那么你每鉴一次权,系统将访问一次数据库。如果要减小性能消耗,可以把权限数据放在缓存中,参考:把权限放在缓存里。
首先,不删除旧 Token 的原因是为了在旧 Token 再次访问系统时提示他:已被顶下线。
而且这个 Token 不会永远留在 Redis 里,在其 TTL 到期后就会自动清除,如果你想让它立即消失,可以:
is-concurrent 和 is-share 都打开,这样每次登陆都会复用以前的旧 Token,就不会有废弃 Token 产生了。StpUtil.logout(10001) ,把这个账号的旧登录都给清除了。尝试加上排除 "/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**" ,"/doc.html/**","/error","/favicon.ico"
不同版本可能会有所不同,其实在前端摁一下 F12 看看哪个 url 报错排除哪个就行了(另附:注解鉴权是不需要排除的,因为 Swagger 本身也没有使用 Sa-Token 的注解)
可以点进去源码看一下,SaRouter.match方法有多个重载,可以放一个集合, 例如:
SaRouter.match("/**").notMatch("/login", "/reg").check(r -> StpUtil.checkLogin());
StpUtil.login()只是为了给当前会话做个唯一标记,通常写入UserId即可,如果要存储User对象,可以使用StpUtil.getSession()获取Session对象进行存储。
主要是失去了Cookie无法自动化保存和提交token秘钥,可以参考章节:前后端分离
默认是satoken,如果想换一个名字,更改一下配置文件的tokenName即可。
权限本来就是动态的,框架预留的 StpInterface 接口,就是为了让你可以写任意代码来获取数据
参考:把路由拦截鉴权动态化
在配置文件将isReadCookie值配置为false
在配置文件将isPrint值配置为false
StpUtil.getSession()获取的是Account-Session,必须登录后才能使用,如果需要在未登录状态下也使用Session功能,请使用Token-Session
步骤:先在配置文件里将tokenSessionCheckLogin配置为false,然后通过StpUtil.getTokenSession()获取Session 。或者直接调用 StpUtil.getAnonTokenSession() 获取匿名 Token-Session。
不需要,如果只使用header来传输token,可以在配置文件关闭Cookie模式,例:isReadCookie=false
框架内置 [强制指定账号下线] 的APi,在执行修改密码逻辑之后调用此API即可: StpUtil.logout()
这个问题没有标准答案,这里只能给你提供一些建议,从鉴权粒度的角度来看:
So:从鉴权粒度的角度来看,需要针对一个模块鉴权的时候,就用路由拦截鉴权,需要控制到方法级的时候,就用注解鉴权,需要根据条件判断是否鉴权的时候,就用代码鉴权
为了保证相关组件能够及时初始化,框架默认给过滤器注册的优先级为-100,如果你想更改优先级,直接在注册过滤器的方法上加上 @Order(xxx) 即可覆盖框架的默认配置
你的理解是对的,但是框架现在只能做到返回-2,因为 token 过期后,就从 Redis 中消失了,框架没法分辨这个 token 是曾经有过然后过期的,还是从来就没有在Redis中存在过, 所以只能统一抛出-2,这个行为也和具体使用的 SaTokenDao 有关联,例如集成 sa-token-jwt 插件后,框架就能分辨出来是 token 过期了,抛出-3。
关于长短 token,Sa-Token 没有提供直接的 API 支持,但是你可以利用 “临时 token 认证模块” 轻易的达到这一点:
sa-token.timeout 的值配置小一点,然后把 StpUtil.login(10001) 生成的 token 作为短 token ,用来鉴权。String refreshToken = SaTempUtil.createToken(10001, 2592000);。@RequestMapping("/refreshToken")
public SaResult refreshToken(String refreshToken) {
// 1、验证
Object userId = SaTempUtil.parseToken(refreshToken);
if(userId == null) {
return SaResult.error("无效 refreshToken");
}
// 2、为其生成新的短 token
String accessToken = StpUtil.createLoginSession(userId);
// 3、返回
return SaResult.data(accessToken);
}
可以在拦截跳转登录页面时,把原 url 作为 back 参数挂载到登录页后方,登录完成后读取 back 参数并跳转
@RestControllerAdvice
public class GlobalException {
// 未登录异常拦截
@ExceptionHandler(NotLoginException.class)
public Object handlerException(NotLoginException e) {
e.printStackTrace();
return SaHolder.getResponse().redirect("/login?back=" + SaHolder.getRequest().getUrl());
}
}
SaHolder.getResponse().setStatus(401)
以 sa-token-redis-template 为例:Sa-Token 底层使用的是 RedisTemplate 对象来操作数据的,也就是说,你只要给 RedisTemplate 配置上集群模式,Sa-Token 自动就是集群模式了。
首先,如无特殊需求,建议多个项目不要共用同一个 redis,如果非要共用,有以下方式避免数据冲突:
sa-token.token-name 值,此配置项默认为 satoken,是框架在 Redis 存储数据时使用的统一前缀。sa-token-three-redis-jackson-add-prefix 插件,参考:sa-token-three-plugin。CSRF 攻击的核心在于利用浏览器自动提交 Cookie 的特性,代替用户发送自己不想发送的请求。
方案一:关闭 Cookie模式。
在配置文件里配置 sa-token.is-read-cookie=false 关闭 Cookie 读取模式,采用 localStorage 存储 token + header 头提交,即可避免 CSRF 攻击。
方案二:增加 csrf-token 验证
如果项目必须采用 Cookie 模式验证,可以在请求中增加 csrf-token 验证的环节:
1、在登录时,生成一个 csrf_token 返回到前端:
// 测试登录
@RequestMapping("/login")
public SaResult login() {
StpUtil.login(10001);
String csrfToken = StpUtil.getSession().get("csrf_token", () -> SaFoxUtil.getRandomString(60));
return SaResult.ok().set("csrf_token", csrfToken);
}
2、前端将 csrf_token 存储在 localStorage 中(注意一定要存储在 localStorage 而非 Cookie 中,存储在 Cookie 中还是可能会被浏览器自动提交)
localStorage.setItem('csrf_token', csrf_token);
每次请求将 csrf_token 塞到 Header 中。
3、在需要防止 CSRF 攻击的接口验证 csrf_token:
@RequestMapping("/test")
public SaResult test() {
// 先验证 csrfToken
String csrfToken = SaHolder.getRequest().getHeader("csrf_token");
if (csrfToken == null || ! csrfToken.equals(StpUtil.getSession().get("csrf_token")) ) {
throw new SaTokenException("csrf_token 不匹配");
}
// 通过后再处理具体业务
// ...
return SaResult.ok();
}
也可以将验证代码写到全局拦截器中,为所有接口提供校验。
方式一:通过 StpUtil.getStpLogic().setTokenValueToStorage("abcdefgxxxxxxxx") 自定义 token 值
如果你可以在框架读取 token 之前写一些代码,那么你可以通过如下代码自定义当前请求的 token 值:
@RequestMapping("/test")
public SaResult test() {
System.out.println(StpUtil.getTokenValue()); // 此时读取到的是前端提交的: cebcc930-c0f5-4009-8eb0-1b6aee63b4aa
StpUtil.getStpLogic().setTokenValueToStorage("abcdefgxxxxxxxx");
System.out.println(StpUtil.getTokenValue()); // 此时读取到的是我们自定义的: abcdefgxxxxxxxx
return SaResult.ok();
}
方式二:重写 StpLogic 读取 token 的方法
@Component
public class MyStpLogic extends StpLogic {
public MyStpLogic() {
super("login");
}
// 自定义 token 读取方式,例如此处改为读取请求头为 my-token 的值
@Override
public String getTokenValue() {
String token = SaHolder.getRequest().getHeader("my-token");
return token;
}
}
文档已完整开源,请访问 Sa-Token 官方仓库,根目录下的 sa-token-doc 文件夹就是文档。
请在gitee 、 github 提交 issues,或者加入qq群交流,群链接