sa-token/sa-token-doc/more/common-questions.md
2024-05-04 11:02:22 +08:00

42 KiB
Raw Blame History

常见问题排查

本篇整理大家在群聊里经常提问的一些问题如有补充欢迎提交pr

toc


一、常见报错

Q报错非 web 上下文无法获取 HttpServletRequest。

报错原因解析:

Sa-Token 的部分 API 只能在 Web 上下文中才能调用,例如:StpUtil.getLoginId() 获取当前用户Id这个方法第一步需要先从前端提交的参数里获取 token 值, 当你在 main 方法里调用这个 API 时,由于 main 方法本质上不是一个 Controller 请求,所以框架无法完成 “从前端提交的参数里获取 token 值” 这一步骤,框架就只能抛出异常。

按照此标准Sa-Token 的 API 可粗浅的分为两大类:

  • 必须在 Web 上下文中才能调用的 API例如StpUtil.getLoginId()StpUtil.getTokenValue() 等等。
  • 无需 Web 上下文也能调用的 API例如StpUtil.getLoginType()SaManager.getConfig() 等等。

此处无法逐一列出到底哪些 API 属于 “必须依赖 Web 上下文的 API”,因为太多了,你只需要记住关键的一点: 当一个 API 执行的代码需要先从前端请求中获取一些数据时,这个 API 就属于 “必须依赖 Web 上下文的 API”

如果你的代码报这个错,说明你在不是 Web 上下文中的地方,调用了 “必须依赖 Web 上下文的 API”,请排查:

  1. 是否在 main 方法中调用了 “必须依赖 Web 上下文的 API”
  2. 是否在带有 @Async 注解的方法中调用了 “必须依赖 Web 上下文的 API”
  3. 是否在一些丢失 web 上下文的子线程中调用了 “必须依赖 Web 上下文的 API”,例如 MyBatis-PlusinsertFill 自动填充。
  4. 是否在一些非 Http 协议的 RPC 框架中(例如 Dubbo调用了 “必须依赖 Web 上下文的 API”
  5. 是否在 SpringBoot 启动初始化的方法中调用了 “必须依赖 Web 上下文的 API”,例如 @PostConstruct 修饰的方法。
  6. 是否在定时任务中调用了 “必须依赖 Web 上下文的 API”

Q报错未能获取有效的上下文处理器。

报错原因解析:

在 sa-token-core 核心包中Sa-Token 底层不能确认最终运行的 web 容器,所以抽象了 SaTokenContext 接口,对接不同容器时需要注入不同的实现, 通常这个注入工作都是框架自动完成的,你只需要按照文档开始部分集成相应的依赖即可。例如:

  • 你要在 Springboot2.x 中使用 Sa-Token就引入sa-token-spring-boot-starter
  • 你要在 Springboot3.x 中使用 Sa-Token就引入sa-token-spring-boot3-starter
  • 你要在基于 webflux 架构的网关中使用 Sa-Token就引入sa-token-reactor-spring-boot-starter
  • 你要在 Solon 中使用 Sa-Token就引入sa-token-solon-plugin
  • 等等等等……

如果你的代码报 “未能获取有效的上下文处理器” 这个错,大概率是因为你没有正确引入所需的包,导致框架没有注入正确的 SaTokenContext 上下文实现,请排查:

  1. 如果你的项目是微服务项目,请直接参考:微服务-依赖引入说明,如果是单体项目,请往下看:
  2. 请判断你的项目是 SpringMVC 环境还是 WebFlux 环境:
  3. 如果你还无法分辨你是哪个环境,就看你的 pom.xml 依赖:
    • 如果引入了spring-boot-starter-web就是 SpringMVC 环境。
    • 如果引入了 spring-boot-starter-webflux 就是WebFlux环境。
    • 什么?你说你两个都引入了?那你的项目能启动成功吗?
  4. 如果是 WebFlux 环境而且正确引入了依赖,依然报错,请检查是否注册了 SaReactorFilter 全局过滤器,在 WebFlux 下这一步是必须的,具体还是请参考上面的 [ 在WebFlux环境集成 ] 章节。
  5. 需要仔细注意,如果你使用的是 Springboot3.x 版本,就不要错误的引入 sa-token-spring-boot-starter,需要引入的是 sa-token-spring-boot3-starter,不然就会导致框架报错。
  6. 如果你的项目开启了全局懒加载(spring.main.lazy-initialization=true)后,能启动项目,但是访问接口报异常,请直接参考:Q开启了全局懒加载后能启动项目但是访问接口报未能获取有效的上下文处理器
  7. 如果以上步骤排除无误后依然报错,请直接提 issue 或者加入QQ群求助。

Q报错NotLoginExceptionxxx

这个错是说明调用接口的人没有通过登录校验,请注意:通常情况下,异常提示语已经描述清楚了没有通过校验的具体原因:

如果是未能读取到有效Token

  • 可能1前端没有提交 Token最好从前端f12控制台看看请求参数里有 token 吗)。
  • 可能2前端提交了 Token但是参数名不对。默认参数名是 satoken,可通过配置文件 sa-token.token-name: satoken 来更改。
  • 可能3前端提交了 Token但是你配置了框架不读取比如说你配置了 is-read-header=false关闭header读取此时你再从 header 里提交token框架就无法读取到。
  • 可能4前端提交了 Token但是 Token前缀 不对,可参考:自定义 Token 前缀
  • 可能5你的项目属于前后端分离架构此时浏览器默认不自动提交 Cookie参考前后端分离
  • 可能6你使用了 Nginx 反向代理,而且配置了 自定义Token名称而且自定义的名称还带有下划线比如 shop_token而且还是你的项目还是从 Header头提交Token的此时 Nginx 默认会吞掉你的下划线参数,可参考:nginx做转发时带下划线的header参数丢失
  • 可能7可能是跨域了导致前端提交不上 token看看前端浏览器有没有跨域的报错。

如果是Token无效6ad93254-b286-4ec9-9997-4430b0341ca0

  • 可能1前端提交的 token 是乱填的,或者从别的项目拷过来的,或者多个项目一起开发时彼此的 Token 串项目了。
  • 可能2前端提交的 token 已过期timeout超时了
  • 可能3在不集成 Redis 的情况下:颁发 token 后,项目重启了,导致 token 无效。
  • 可能4在集成 Redis 的情况下:颁发 token 后Redis重启了导致 token 无效。
  • 可能5你提交的 token 和框架读取到的 token 不一致:
    • 可能5.1:比如说你配置了is-read-header=false关闭header读取然后你从header提交token-A而框架从Cookie里读取token-B,导致鉴权不通过(框架读取顺序为body->header->cookie
    • 可能5.2:比如说你配置了token-name=x-token自定义token名称此时你从header提交satoken:token-A参数名没对上然后框架从header里读取不到你提交的token转而继续从Cookie读取到了token-B
  • 可能6在集成 jwt 插件的情况下:
    • 如果使用的是 Simple 模式情况和不集成jwt一样。
    • 如果使用的是 Mixin 和 Stateless 模式:查看这个 token 颁发后是否更改了 jwtSecretKey 配置项。
  • 可能7同一账号登录数量超过12个导致最先登录的被强制注销掉这个值可以通过 maxLoginCount 来配置默认值12-1代表不做限制。
  • 可能8在配置了 is-concurrent=true, is-share=true的情况下你和别人共同登录了同一账号此时对方注销了登录由于你们使用的是同一个token导致你这边的会话也失效了。
  • 可能9可能是多账号鉴权的关系在多账号模式下如果是 StpUserUtil.login() 颁发的token你从 StpUtil.checkLogin() 进行校验永远都是无效token因为账号体系没对上。

如果是Token已过期6ad93254-b286-4ec9-9997-4430b0341ca0

  • 可能1前端提交的 token 已被冻结active-timeout超时了比如配置了 active-timeout=120但是超过了120秒没有访问接口
  • 可能2集成jwt而且使用的是 Mixin 或 Stateless 模式而且token过期了timeout超时了

如果是Token已被顶下线6ad93254-b286-4ec9-9997-4430b0341ca0

  • 可能1在项目配置了 is-concurrent=false 的前提下,这个账号又被别人登录了,导致旧登录被挤了下去。
  • 可能2这个账号被 StpUtil.replaced(loginId, device) 方法强制顶下线了。

如果是Token已被踢下线6ad93254-b286-4ec9-9997-4430b0341ca0

  • 可能1这个账号被 StpUtil.kickout(loginId) 方法强制踢下线了。

Q加了注解进行鉴权认证不生效

  1. 注解鉴权功能默认关闭两种方式任选其一进行打开注册注解拦截器、集成AOP模块参考注解式鉴权
  2. 在Spring环境中, 如果同时配置了WebMvcConfigurerWebMvcConfigurationSupport时, 也会导致拦截器失效.
    • 常见场景: 很多项目中会在WebMvcConfigurationSupport中配置addResourceHandlers方法开放Swagger等相关静态资源映射, 同时基于Sa-Token添加了WebMvcConfigurer配置addInterceptors方法注册注解拦截器, 这样会导致注解拦截器失效.
    • 解决方案: WebMvcConfigurerWebMvcConfigurationSupport只选一个配置, 建议统一通过实现WebMvcConfigurer接口进行配置.
  3. 如果以上步骤处理后仍然没有效果,加群说明一下复现步骤

Q我加了拦截器鉴权但是好像没有什么效果请求没有被拦截住

  • 可能1这个拦截器可能没有注册成功。
  • 可能2你访问的请求没有进入这个拦截器。

尝试按照下面的代码测试一下看看:

// 注册拦截器 
@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 被打印出来,才证明请求进入了拦截器。

如果拦截器没有注册成功,则:

  • 可能1SaTokenConfigure 配置类不在启动类的同包或者子包下,导致没有被 SpringBoot 扫描到。
  • 可能2你的项目启动类上加了 @ComponentScan("com.xxx") 注解,导致包扫描范围不正确,请将此注解删除或移动到其它配置类上。
  • 可能3项目属于 Maven 多模块项目,SaTokenConfigure 和启动类没有在一个模块,且启动类模块没有引入配置类的模块,导致加载不到。

如果拦截器已经注册成功,但请求没有进入拦截器:

  • 可能1你访问的 path没有被 .addPathPatterns("/user/**") 拦截住,或者被 .excludePathPatterns("/xxx/xx") 排除掉了。
  • 可能2你访问的是另一个项目请把当前项目停掉看看你的请求还能不能访问成功。

如果请求进入拦截器也成功了,那可能是:

  • 可能1前端访问时提交了会话 Token且这个 Token 是有效的,通过了拦截器的代码校验。
  • 可能2你访问的 path和你预期不符仔细观察一下打印出来的 path 信息,和你的预期相符吗。

注:以上的排查步骤,对过滤器不生效的情形一样适用。

Q我使用拦截器鉴权时明明排除了某个路径却仍然被拦截了

  • 可能1你的项目可能是跨域了先把跨域问题解决掉参考解决跨域问题
  • 可能2你访问的接口可能是404了SpringBoot环境下如果访问接口404后会被转发到/error,然后被再次拦截。请确保你访问的 path 有对应的 Controller 承接!
  • 可能3可能拦截器这里并没有拦截但是又被其他地方拦截了。请先把这个拦截器给注释掉看看还会不会拦截如果依然拦截那说明不是这个拦截器的锅请仔细查看一下控制台抛出的堆栈信息定位一下到底是哪行代码拦截住这个请求的。
  • 可能4后端拦截的 path 未必是你前端访问的这个path特别是经过网关转发后的path可能会有变化建议先打印一下 path 信息,看看和你预想的是否一致,再做分析。
@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("/**"); 
}
  • 可能5可能你只提交了一个请求但是浏览器自动帮你提交了其它请求举个例子首次访问网站时浏览器一般会自动提交 /favicon.ico,所以你需要找出是哪个path被拦截了怎么找呢用【可能4】的代码来测试找。
  • 可能6你的项目配置了 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");
  • 可能7你写了多个匹配规则请求越过了第一个规则但又被其它规则拦下来了例如以下代码
// 以下代码,当你未登录访问 `/user/doLogin` 时会被第1条规则越过然后被第2条拦下校验登录然后抛出异常`NotLoginExceptionxxx`
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("/**");
  • 可能8你自定义的封装方法并没有按照你的预想情况执行
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() 的问题,那再从其它地方开始排查。

Q我在配置文件中加了一些关于 Sa-Token 的配置,但是没有生效。

首先有没有生效的最佳判断方式是在main方法中加一个打印看看打印出来的和你配置文件的一致吗

@SpringBootApplication
public class SaTokenApplication {
	public static void main(String[] args) {
		SpringApplication.run(SaTokenApplication.class, args); 
		System.out.println("\n启动成功Sa-Token配置如下" + SaManager.getConfig());
	}
}

如果不一致,请排查:

  • 可能1项目中还存在代码配置而代码配置会覆盖 application.yml 中配置,详细参考:框架配置
  • 可能2你的配置文件名字错误SpringBoot 项目正常情况下配置文件名称应该是:application.ymlapplication.properties
  • 可能3可能是你的配置前缀不对或者配置缩进不对
# 错误示例,多加了 spring 前缀
spring:
	sa-token: 
		token-name: xxx-token
# 错误示例,缩进不对
sa-token: 
token-name: xxx-token
# 正确的应该是以 sa-token 开头
sa-token: 
	token-name: xxx-token

Q我自定义了组件但是好像没有生效

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 自动实例化并注入。
  • 自定义的组件实现类是否在启动类的同目录或者子目录上,如果不在则无法被 springboot 启动时扫描,扫描不到也就无法注入。
  • 启动类上是否加了 @ComponentScan 注解,导致包扫描范围不正确,请将此注解删除或移动到其它配置类上。

2、这个组件注入成功了但是还没到执行时机比如 StpInterface 组件,只有在鉴权时才会触发,如果你的代码仅仅是登录校验,就不会执行到这个组件。

Q集成 Redis 后,明明 Redis 中有值却还是提示无效Token

根据以往的处理经验,发生这种情况 90% 的概率是因为你找错了Redis代码连接的Redis和你用管理工具看到的Redis并不是同一个。

你可能会问:我看配置文件明明是同一个啊?

我的回答是:别光看配置文件,不一定准确,在启动时直接执行 SaManager.getSaTokenDao().set("name", "value", 100000); 随便写入一个值看看能不能根据你的预期写进这个Redis如果能的话才能证明代码连接的Reids你用管理工具看到的Redis 是同一个,再进行下一步排查。

Q报错无效Same-Tokenxxxxxxxxxxx

与之类似的的报错还有:

  • SSO模式二时报错无效ticketxxxxxxxxxx
  • OAuth2模块跨多个项目搭建Server时报错无效 Access-Tokenxxxxxx
  • 微服务做分布式 Session 认证时,报错:无效 Tokenxxxxxxxxx
  • 等等等等....

这些功能有个统一的特点,就是需要多个项目连接同一个 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 相关的配置都复制过去,试验一下看看。

Q我把 token 有效期设置为 30 天,但是总感觉不到 30 天的时候 token 就无效了,怎么回事?

  • 可能1你没有为 sa-token 集成 Redis框架默认将会话数据保存在内存中项目重启后数据会消失。
  • 可能2你为 sa-token 集成了 Redis但是 Redis 重启了,导致会话消失。
  • 可能3你配置了 is-concurrent=false,不允许同一账号多端登录,有别人登录了这个账号把你顶下去了。
  • 可能4你配置了 is-concurrent=true,但是is-share=false,同一账号每次登录产生不同的 token默认最高可以同时登录12个客户端超过将自动注销最原先的会话。
  • 可能5你的这个账号别人也登录了别人调用了注销方法把你这边的也注销了。StpUtil.logout() 为单 token 注销,StpUtil.logout(10001) 为账号所有 token 注销。
  • 可能6你虽然 sa-token.timeout 配置了 30 天,但是 sa-token.active-timeout 配置了较短的值超过这个时间无操作token 就过期了。
  • 可能7你换了浏览器或者换了电脑或者清空了浏览器最近缓存记录自然而然需要重新登录。
  • 可能8你中途改了项目配置比如改了 sa-token.token-name 配置项的值,会导致会话保存的 key 发生改变,效果等同于手动清空了 Redis 数据,需要重新登录。

Q有时候我不加 Token 也可以通过鉴权,请问是怎么回事?

  • 可能1你访问的这个接口根本就没有鉴权的代码所以可以安全的访问通过。
  • 可能2可能是 Cookie 帮你自动提交了 Token在浏览器或 Postman 中会自动维护Cookie模式如不需要可以在配置文件is-read-cookie: false,然后重启项目再测试一下。

Q一个 User 对象存进 Session 后,再取出来时报错:无法从 User 类型转换成 User 类型?

  • 可能1你的 User 类中途换了包名,导致存进去时和取出来时对不上,无法成功创建实例。
  • 可能2你打开了代码热刷新模式先存进去的对象热刷新后再取出会报错关闭热刷新即可解决。

Q在 SaServletFilter 中调用 SpringMVCUtil.getRequest() 报错非Web上下文无法获取Request

  • 可能1项目中有配置类继承了 extends WebMvcConfigurationSupport
  • 可能2项目中有配置类添加了注解 @EnableWebMvc

解决方案:不要加 @EnableWebMvc,不要 extends WebMvcConfigurationSupport,要 implements WebMvcConfigurer

如果不是以上原因可以加群提供复现demo。

Q我配置了 active-timeout 值,但是当我每次续签时 Redis 中的 ttl 并没有更新,是不是 bug 了?

不更新是正常现象,active-timeout不是根据 ttl 计算的是根据value值计算的value 记录的是该 Token 最后访问系统的时间戳, 每次验签时用:当前时间 - 时间戳 > active-timeout来判断这个 Token 是否已经超时。

Q整合 Redis 时先选择了默认jdk序列化后又改成 jackson 序列化程序开始报错SerializationException

两者的序列化算法不一致导致的反序列化失败,如果要更改序列化方式,则需要先将 Redis 中历史数据清除,再做更新。

Q调用 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");

Q我加了 Sa-Token 的全局过滤器,浏览器报错跨域了怎么办?

参考:https://juejin.cn/post/7247376558367981627

是。

参考:前后端分离

Q集成redis后对象模型序列化异常

假设执行如下代码:

@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 即可。

Q我实现了 StpInterface 接口,但是在登录时没有进入我的实现类代码?

不进入是正常现象, StpInterface 是鉴权接口,在执行鉴权代码时才会进入 StpInterface 实现类,登录认证时不会进入。

Q启动时报错找不到 xx 类 xx 方法:

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" 关键词来找,如果是多模块或者微服务项目,就整个项目搜索。

Q在多账号模式的注解鉴权时报错未能获取对应StpLogictype=xxx

报这个错说明对应 type 的 StpLogic 尚未初始化到全局 StpLogicMap 中,一般会有两种原因造成这种情况:

  1. 注解里的 loginType 拼写错误,请改正 (建议使用常量)。
  2. 自定义 StpUtil 尚未初始化(静态类中的属性至少一次调用后才会初始化),解决方法两种:
    • (1) 从main方法里调用一次
    • (2) 在自定义StpUtil类加上类似 @Component 的注解让容器启动时扫描到自动初始化

Q使用拦截器鉴权访问一个不存在的 path 时springboot 会自动在控制台打印一下异常。

可尝试添加以下配置解决:

spring.resources.add-mappings=false
spring.mvc.throw-exception-if-no-handler-found=true

Q开启了全局懒加载后能启动项目但是访问接口报“未能获取有效的上下文处理器”

开启了全局懒加载后,能启动项目,但是访问接口报异常 InvalidContextException: 未能获取有效的上下文处理器, 配置如下:

spring:
  main:
    lazy-initialization: true

原因是 Sa-Token 自动配置入口类 SaBeanInject 被延迟加载了,只需要手动指定懒加载排除掉 SaBeanInject 就可以了,实现代码如下:

@Configuration
class MyConfiguration {
    @Bean
    LazyInitializationExcludeFilter integrationLazyInitExcludeFilter() {
        return LazyInitializationExcludeFilter.forBeanTypes(SaBeanInject.class);
    }
}

经验来源

QSpringBoot 3.x 路由拦截鉴权报错No more pattern data allowed after {*...} or ** pattern element

报错原因SpringBoot3.x 版本默认将路由匹配机制由 ant_path_matcher 改为了 path_pattern_parser 模式, 而此模式有一个规则,就是写路由匹配符的时候,不允许 ** 之后再出现内容。例如:/admin/**/info 就是不允许的。

如果你的项目报了这个错,说明你写的路由匹配符出现了上述问题,有三种解决方案:

  1. 等待 SpringMVC 官方增强 path_pattern_parser 模式能力,使之可以支持 ** 之后再出现内容。
  2. 在写路由匹配规则时,避免使 ** 之后再出现内容。
  3. 将项目的路由匹配机制改为 ant_path_matcher

步骤1先改项目的

spring:
    mvc:
        pathmatch:
            matching-strategy: ant_path_matcher

步骤2再改 Sa-Token 的:

/**
 * 自定义 SaTokenContext 实现类,重写 matchPath 方法,切换为 ant_path_matcher 模式,使之可以支持 `**` 之后再出现内容
 */
@Primary
@Component
public class SaTokenContextByPatternsRequestCondition extends SaTokenContextForSpringInJakartaServlet {

    @Override
    public boolean matchPath(String pattern, String path) {
        return SaPatternsRequestConditionHolder.match(pattern, path);
    }

}

注意点:

SpringBoot2.x 的 WebFluxSC Gateway 项目,按照上述步骤改造,可能会报错

java.lang.NoClassDefFoundError: org/springframework/web/servlet/mvc/condition/PatternsRequestCondition

只需要将“步骤2”中的代码 return SaPatternsRequestConditionHolder.match(pattern, path); 更换为 return SaPathMatcherHolder.getPathMatcher().match(pattern, path); 即可,例如:

/**
 * 自定义 SaTokenContext 实现类,重写 matchPath 方法,切换为 ant_path_matcher 模式,使之可以支持 `**` 之后再出现内容
 */
@Primary
@Component
public class SaTokenContextByPatternsRequestCondition extends SaTokenContextForSpringReactor {

    @Override
    public boolean matchPath(String pattern, String path) {
        return SaPathMatcherHolder.getPathMatcher().match(pattern, path);
    }

}

QWebflux 环境集成,或者 SpringCloud Gateway 环境集成后,过滤器里路由拦截鉴权报错:java.lang.NoSuchFieldError: defaultInstance

java.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 版本用的太低了,导致一些类不存在。

  • 方案一:升级项目的 SpringBoot 版本至 2.3.x 以上
  • 方案二:像上面的问题解决方案一样,重写一下相关类:
/**
 * 自定义 SaTokenContext 实现类,重写 matchPath 方法,将 PathPatternParser.defaultInstance 改为 SaPathMatcherHolder.getPathMatcher()
 */
@Primary
@Component
public class SaTokenContextByPatternsRequestCondition extends SaTokenContextForSpringReactor {
    @Override
    public boolean matchPath(String pattern, String path) {
        return SaPathMatcherHolder.getPathMatcher().match(pattern, path);
    }
}

二、常见疑问

Q登录方法需要我自己实现吗

是的,不同于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("登录失败");
}

Q框架抛出的权限不足异常我想根据自定义提示信息可以吗

可以,在全局异常拦截器里捕获NotPermissionException,可以通过getPermission()获取没有通过认证的权限码,可以据此自定义返回信息

@RestControllerAdvice
public class GlobalExceptionHandler {
    // 全局 NotPermissionException 异常捕获 
    @ExceptionHandler(NotPermissionException.class)
    public SaResult handlerException(NotPermissionException e) {
        e.printStackTrace();
        return SaResult.error("缺少权限:" + e.getPermission());
    }
}

Q在 SaInterceptor 中,注解鉴权总是先于路由拦截鉴权执行,能调整一下顺序吗?

框架没有提供直接的 API但你有以下两种方式可以做到这一点

  • 方式1将 SaInterceptor 里的代码复制出来一份,按照你的需求改一下,然后使用你这个自定义的拦截器,不再使用官方的。
  • 方式2注册两次 SaInterceptor 拦截器,例如:
@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("/**");
	}
}

如上,第一个完成路由拦截鉴权功能,第二个完成注解鉴权功能。

Q我的项目权限模型不是RBAC模型很复杂可以集成吗

无论什么模型只要能把一个用户具有的所有权限塞到一个List里返回给框架就能集成

QStpInterface 接口的 方法,在什么时候执行?

每次鉴权时执行,例如你调用了 StpUtil.checkgetPermission("xxx") 方法,框架就会调用底层的 StpInterface#getPermissionList 方法来获取权限数据。

如果你的 getPermissionList 里有读数据库的代码,那么你每鉴一次权,系统将访问一次数据库。如果要减小性能消耗,可以把权限数据放在缓存中,参考:把权限放在缓存里

Q当我配置不并发登录时每次登陆都会产生一个新的 Token旧 Token 依然被保存在 Redis 中,框架为什么不删除呢?

首先,不删除旧 Token 的原因是为了在旧 Token 再次访问系统时提示他:已被顶下线。

而且这个 Token 不会永远留在 Redis 里,在其 TTL 到期后就会自动清除,如果你想让它立即消失,可以:

  • 方法一:配置文件把 is-concurrentis-share 都打开,这样每次登陆都会复用以前的旧 Token就不会有废弃 Token 产生了。
  • 方法二:每次登录前把先调用注销方法 StpUtil.logout(10001) ,把这个账号的旧登录都给清除了。
  • 方法三:写一个定时任务查询 Redis 值进行删除。

Q我使用过滤器鉴权 or 全局拦截器鉴权,结果 Swagger 不能访问了,我应该排除哪些地址?

尝试加上排除 "/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**" ,"/doc.html/**","/error","/favicon.ico"

不同版本可能会有所不同,其实在前端摁一下 F12 看看哪个 url 报错排除哪个就行了(另附:注解鉴权是不需要排除的,因为 Swagger 本身也没有使用 Sa-Token 的注解)

QSaRouter.match 有多个路径需要排除怎么办?

可以点进去源码看一下,SaRouter.match方法有多个重载,可以放一个集合, 例如:

SaRouter.match("/**").notMatch("/login", "/reg").check(r -> StpUtil.checkLogin());

Q为什么StpUtil.login() 不能直接写入一个User对象

StpUtil.login()只是为了给当前会话做个唯一标记,通常写入UserId即可如果要存储User对象可以使用StpUtil.getSession()获取Session对象进行存储。

Q前后端分离模式下和普通模式有何不同

主要是失去了Cookie无法自动化保存和提交token秘钥,可以参考章节:前后端分离

Q前后端分离时前端提交的 header 参数是叫 token 还是 satoken 还是 tokenName

默认是satoken如果想换一个名字更改一下配置文件的tokenName即可。

Q一个账号拥有哪些权限可以做成动态的吗

权限本来就是动态的,框架预留的 StpInterface 接口,就是为了让你可以写任意代码来获取数据

Q路由拦截鉴权可以做成动态的吗

参考:把路由拦截鉴权动态化

Q我不想让框架自动操作Cookie怎么办

在配置文件将isReadCookie值配置为false

Q怎么关掉每次启动时的字符画打印

在配置文件将isPrint值配置为false

QStpUtil.getSession()必须登录后才能调用吗?如果我想在用户未登录之前存储一些数据应该怎么办?

StpUtil.getSession()获取的是Account-Session必须登录后才能使用如果需要在未登录状态下也使用Session功能请使用Token-Session
步骤:先在配置文件里将tokenSessionCheckLogin配置为false,然后通过StpUtil.getTokenSession()获取Session 。或者直接调用 StpUtil.getAnonTokenSession() 获取匿名 Token-Session。

Q我只使用header来传输token还需要打开Cookie模式吗

不需要如果只使用header来传输token可以在配置文件关闭Cookie模式isReadCookie=false

Q我想让用户修改密码后立即掉线重新登录应该怎么做

框架内置 [强制指定账号下线] 的APi在执行修改密码逻辑之后调用此API即可: StpUtil.logout()

Q代码鉴权、注解鉴权、路由拦截鉴权我该如何选择

这个问题没有标准答案,这里只能给你提供一些建议,从鉴权粒度的角度来看:

  1. 路由拦截鉴权:粒度最粗,只能粗略的拦截一个模块进行权限认证
  2. 注解鉴权:粒度较细,可以详细到方法级,比较灵活
  3. 代码鉴权粒度最细不光可以控制到方法级甚至可以if语句决定是否鉴权

So从鉴权粒度的角度来看需要针对一个模块鉴权的时候就用路由拦截鉴权需要控制到方法级的时候就用注解鉴权需要根据条件判断是否鉴权的时候就用代码鉴权

QSa-Token的全局过滤器我应该怎么指定它的优先级呢

为了保证相关组件能够及时初始化,框架默认给过滤器注册的优先级为-100如果你想更改优先级直接在注册过滤器的方法上加上 @Order(xxx) 即可覆盖框架的默认配置

Qtimeout 过期了,获取到的 NotLoginException 场景值是-2按照文档说的应该是-3吧。是我理解的不对还是操作有误

你的理解是对的,但是框架现在只能做到返回-2因为 token 过期后,就从 Redis 中消失了,框架没法分辨这个 token 是曾经有过然后过期的还是从来就没有在Redis中存在过 所以只能统一抛出-2这个行为也和具体使用的 SaTokenDao 有关联,例如集成 sa-token-jwt 插件后,框架就能分辨出来是 token 过期了,抛出-3。

QSa-Token 是否提供类似 RefreshToken 的概念,与 AccessToken 相互配合刷新令牌鉴权。

关于长短 tokenSa-Token 没有提供直接的 API 支持,但是你可以利用 “临时 token 认证模块” 轻易的达到这一点:

  1. sa-token.timeout 的值配置小一点,然后把 StpUtil.login(10001) 生成的 token 作为短 token ,用来鉴权。
  2. 用 “临时 token 认证模块” 生成长 token String refreshToken = SaTempUtil.createToken(10001, 2592000);
  3. 把这两个 token 一起返回到前端。
  4. 你再开个接口,可以让前端通过长 token刷新短 token参考代码
@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);
}

Q怎么改变请求返回的 http 状态码?

SaHolder.getResponse().setStatus(401)

Q还是有不明白到的地方?

请在giteegithub 提交 issues或者加入qq群交流群链接