sa-token全局过滤器之写法优化(包含设置Order属性)
背景:在手写登录的过程中需要继承sa-token框架,并使用sa-token的过滤器功能,那全局过滤器怎么写呢 ?我们一起来看看吧。
根据sa-token的官方文档:
https://sa-token.cc/doc.html#/up/global-filter
如下图所示:下面是最简单的一种sa-token过滤器的实现方式,这种写法简单,但是会有一些问题。
第一种写法:
相较于第一种写法,配置更为全面的过滤器写法:
登录中的具体写法:第一种,不设置Order的写法:
@Configuration
@Slf4j
public class SaTokenConfigure implements WebMvcConfigurer {
/**
* 白名单
**/
private static String[] EXCLUDE_PATH = {"/login/**",
"/doc.html",
"/service-worker.js",
"/swagger-resources",
"/webjars/**",
"/v3/api-docs/**",
"/favicon.ico",
"/ocr/**",
"/location/**"};
/**
* 注册拦截器
*
**/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
.addPathPatterns("/**")
//设置白名单
.excludePathPatterns(EXCLUDE_PATH);
}
/**
* 全局过滤器
*
* @return cn.dev33.satoken.filter.SaServletFilter
**/
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
// 拦截与排除 path
.addInclude("/**").addExclude(EXCLUDE_PATH)
// 全局认证函数
.setAuth(obj -> {
// ...
log.info("这里是全局认证函数");
//这里可以写判断是否登录的逻辑,比如sa-token是有工具类StpUtil.isLogin()来判断当前请求的用户是否已经登录了,如果登录过滤器则放行,否则的话拦截,比如用户未登录的情况下,访问了一些在白名单之外的url地址,那此时就会经过这个位置,这里可以写校验逻辑,还有一些登录上下文的初始化逻辑。
if(StpUtil.isLogin()){
//Thread_local实现的上下文工具类
//每次先清除一下上下文内容,
TMSSsoContextHelper.clearSession();
//重新初始化这个容器,其实也就是从sa-session中取得内容放到上下文信息中,以方便后续使用。
initTMSSsoContextIfLogin();
}else {
//这里抛出了未登录的异常,会被下面的setError函数捕获并处理相关逻辑。
throw new NotLoginException("未登录","web","");
}
})
// 异常处理函数
.setError(e -> {
log.info("------------->NotLogin-------------");
// 1. 设置响应头为 JSON 格式
SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8");
// 2. 捕获未登录异常,这样设置上,会响应到前台具体的 如“请先登录”的提示。其实也可以在全局拦截器那捕获这个未登录的异常,提示未登录请重新登陆,也可以在这里进行包装后返回提示,如果这里不写和全局捕获那都不写的话,就会返回500 内部服务错误这种不好的信息到前台,所以这些需要进行响应信息的设置包装。
if (e instanceof NotLoginException) {
SaHolder.getResponse().setStatus(401); // 401 Unauthorized
return SaResult.error("请先登录").setCode(401);
}
// 3. 其他异常返回 500
SaHolder.getResponse().setStatus(500);
return SaResult.error("服务器繁忙,请稍后重试").setCode(500);
})
// 前置函数:在每次认证函数之前执行
.setBeforeAuth(obj -> {
//设置跨域响应头
SaHolder.getResponse()
.setHeader("Access-Control-Allow-Origin", "*")
.setHeader("Access-Control-Allow-Methods", "*")
.setHeader("Access-Control-Max-Age", "3600")
.setHeader("Access-Control-Allow-Headers", "*");
// 如果是预检请求,则立即返回到前端
SaRouter.match(SaHttpMethod.OPTIONS)
.free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
.back();
});
}
/**
* 跨域请求设置
*
* @param registry
**/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("*")
.allowCredentials(false)
.maxAge(3600);
}
/**
* 设置上下文信息
* 其实就是通过登录之后,登录的具体逻辑里面会初始化sa-session对象,然后登录后调用其他请求的时候,这个sasession容器中肯定是有相关信息了,直接初始化放到Thread_local对象中即可让别的程序中现用现取得了。
* @Date
**/
private void initTMSSsoContextIfLogin() {
SaSession saSession = StpUtil.getSession();
String tokenValue = StpUtil.getTokenValue();
Long userId = (Long) saSession.get(BaseConstant.CURRENT_USER_ID);
String tenantIds = (String)saSession.get(BaseConstant.CURRENT_TENANT_ID);
String phone = (String)saSession.get(BaseConstant.CURRENT_USER_PHONE);
//供service使用 当前登录用户关联到审核的企业后:将租户放入RPC容器
if (ObjectUtil.isNotEmpty(tenantIds)) {
RpcContext.getContext().setAttachment(BaseConstant.RPC_CONTEXT_TENANT_ID, tenantIds);
}
TMSSsoContextInfo tmsSsoContextInfo = TMSSsoContextInfo.builder()
.token(tokenValue)
.userId(userId+"")
.tenantId(tenantIds)
.phone(phone)
.build();
TMSSsoContextHelper.setTMSSsoContextInfo(tmsSsoContextInfo);
}
}
第二种,设置Order的写法:
这种是先创建了FilterRegistrationBean对象,同时给这个对象set值的时候,new 了一个SaServletFilter对象,并同时设置上了相关属性逻辑。
@Bean
public FilterRegistrationBean<SaServletFilter> saFilterRegistration() {
FilterRegistrationBean<SaServletFilter> frBean = new FilterRegistrationBean<>();
frBean.setFilter(
new SaServletFilter()
.addInclude("/**")
.addExclude("/favicon.ico")
.setAuth(obj -> {
SaRouter.match("/**", "/user/doLogin", () -> StpUtil.checkLogin());
})
.setError(e -> SaResult.error(e.getMessage()))
);
frBean.setOrder(-101); // 确保在 Spring Security 过滤器之前执行
frBean.addUrlPatterns("/**"); // 显式指定 URL 模式(避免覆盖问题)
return frBean;
}
或者
可以先创建一个SaServletFilter对象,并设置上相关的属性逻辑后,然后再去创建FilterRegistrationBean对象,然后再给这个对象设置Filter属性及Order等属性。具体如下:
@Bean
public FilterRegistrationBean<SaServletFilter> saFilterRegistration() {
// 1. 创建并配置 SaServletFilter
SaServletFilter saServletFilter = new SaServletFilter()
.addInclude("/**")
.addExclude(EXCLUDE_PATH)
.setAuth(obj -> { /* 认证逻辑 */ })
.setError(e -> { /* 异常处理 */ })
.setBeforeAuth(r -> { /* 跨域配置 */ });
// 2. 包装为 FilterRegistrationBean
FilterRegistrationBean<SaServletFilter> frBean = new FilterRegistrationBean<>();
frBean.setFilter(saServletFilter);
frBean.setOrder(-99);
frBean.addUrlPatterns("/**"); // 使用 /** 匹配所有路径
return frBean;
}
将通过第一种写法的全局拦截器代码进行优化(设置上相关执行顺序了)后如下:
改写思路:
首先,我需要回忆Spring
Boot中如何注册过滤器。通常,通过FilterRegistrationBean
可以注册Servlet过滤器,并设置其顺序、URL模式等属性。而直接返回SaServletFilter
作为Bean,Spring
Boot会自动将其注册为过滤器,但可能无法灵活设置优先级。接下来,我需要将现有的
getSaServletFilter()
方法转换为创建FilterRegistrationBean
实例。原方法中,用户配置了拦截路径、排除路径、认证函数、异常处理函数以及前置函数。这些配置需要保留,但需要包装到FilterRegistrationBean
中。步骤可能如下:
创建一个
FilterRegistrationBean
实例。将原有的
SaServletFilter
配置移动到该实例中。设置过滤器的顺序为-99。
确保URL模式的正确设置,可能需要显式指定
addUrlPatterns
,尽管原代码中已经通过addInclude
设置了路径,但为了保险起见,可能在FilterRegistrationBean
中再次指定。需要注意的地方包括:
确保
SaServletFilter
的所有配置(如addInclude, addExclude, setAuth, setError, setBeforeAuth)都被正确转移到新的FilterRegistrationBean
中。检查是否有任何依赖于Spring Boot自动配置的部分,可能需要显式设置。
确认优先级设置是否正确,-99的值是否合适,这取决于系统中其他过滤器的优先级设置,比如Spring Security通常使用-100,所以-99会让Sa-Token过滤器在其之后执行,但用户可能需要根据实际需求调整。
此外,用户提到要设置优先级为-99,我需要确保在
FilterRegistrationBean
中通过setOrder
方法设置。例如:frBean.setOrder(-99);
在代码转换过程中,还需要测试以确保修改后的过滤器仍然按预期工作,包括拦截正确的路径、处理异常、设置响应头等。特别是跨域设置和认证逻辑是否在正确的位置执行。最后,需要检查是否有其他配置可能影响过滤器的行为,比如
WebMvcConfigurer
中的其他设置,确保没有冲突。
以下是修改后的代码,将 SaServletFilter 的注册方式改为通过 FilterRegistrationBean,并设置优先级为 -99:
@Configuration
@Slf4j
public class SaTokenConfigure implements WebMvcConfigurer {
// ... 其他代码保持不变 ...
/**
* 注册全局过滤器,并设置优先级为 -99
*/
@Bean
public FilterRegistrationBean<SaServletFilter> saFilterRegistration() {
// 1. 创建 SaServletFilter 实例并配置
SaServletFilter saServletFilter = new SaServletFilter()
.addInclude("/**")
.addExclude(EXCLUDE_PATH)
.setAuth(obj -> {
log.info("这里是全局认证函数");
if(SsoManager.isLogin()){
TMSSsoContextHelper.clearSession();
initTMSSsoContextIfLogin();
} else {
throw new NotLoginException("未登录", "web", "");
}
})
.setError(e -> {
log.info("------------->NotLogin-------------");
SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8");
if (e instanceof NotLoginException) {
SaHolder.getResponse().setStatus(401);
return SaResult.error("请先登录").setCode(401);
}
SaHolder.getResponse().setStatus(500);
return SaResult.error("服务器繁忙,请稍后重试").setCode(500);
})
.setBeforeAuth(obj -> {
SaHolder.getResponse()
.setHeader("Access-Control-Allow-Origin", "*")
.setHeader("Access-Control-Allow-Methods", "*")
.setHeader("Access-Control-Max-Age", "3600")
.setHeader("Access-Control-Allow-Headers", "*");
SaRouter.match(SaHttpMethod.OPTIONS)
.free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
.back();
});
// 2. 包装成 FilterRegistrationBean 并设置优先级
FilterRegistrationBean<SaServletFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(saServletFilter);
registrationBean.setOrder(-99); // 设置优先级为-99
registrationBean.addUrlPatterns("/*"); // 显式指定拦截路径(覆盖 addInclude 的配置)
return registrationBean;
}
// ... 其他方法保持不变 ...
}
关键改动说明:
-
返回类型修改:
// 原返回类型 @Bean public SaServletFilter getSaServletFilter() { ... } // 新返回类型 @Bean public FilterRegistrationBean<SaServletFilter> saFilterRegistration() { ... }
-
显式设置优先级:
registrationBean.setOrder(-99); // 确保优先级高于 Spring Security(默认-100)
-
URL 模式显式声明:
registrationBean.addUrlPatterns("/*"); // 明确拦截所有路径
- 虽然
SaServletFilter
内部已通过.addInclude("/**")
声明,但FilterRegistrationBean
会覆盖该配置,建议保持一致
- 虽然
-
保持原有配置:
- 认证逻辑(
.setAuth
) - 异常处理(
.setError
) - 跨域配置(
.setBeforeAuth
) - 排除路径(
.addExclude(EXCLUDE_PATH)
)
- 认证逻辑(
注意事项:
-
优先级与 Spring Security 的关系:
- Spring Security 的过滤器链默认优先级为
-100
- 设置
-99
表示您的过滤器会在 Spring Security 之后执行 - 如果需要先于 Spring Security 执行鉴权,需设置为比
-100
更小的值(如-101
)
- Spring Security 的过滤器链默认优先级为
-
URL 匹配模式:
SaServletFilter.addInclude("/**")
匹配所有路径(包括子路径)FilterRegistrationBean.addUrlPatterns("/*")
匹配所有路径(但不会匹配子路径)- **建议统一使用
/***
以保证拦截范围一致
-
排除路径处理:
addExclude(EXCLUDE_PATH)
已通过SaServletFilter
配置- 需确保
EXCLUDE_PATH
中的路径不会被 Spring Security 或其他过滤器拦截
-
跨域配置冲突:
- 如果项目中同时存在其他跨域配置(如
@CrossOrigin
或CorsFilter
),需确保配置不冲突 - 建议统一在 一处 配置跨域逻辑(推荐在
setBeforeAuth
中配置)
- 如果项目中同时存在其他跨域配置(如
完整配置建议:
@Bean
public FilterRegistrationBean<SaServletFilter> saFilterRegistration() {
// 1. 创建并配置 SaServletFilter
SaServletFilter saServletFilter = new SaServletFilter()
.addInclude("/**")
.addExclude(EXCLUDE_PATH)
.setAuth(obj -> { /* 认证逻辑 */ })
.setError(e -> { /* 异常处理 */ })
.setBeforeAuth(r -> { /* 跨域配置 */ });
// 2. 包装为 FilterRegistrationBean
FilterRegistrationBean<SaServletFilter> frBean = new FilterRegistrationBean<>();
frBean.setFilter(saServletFilter);
frBean.setOrder(-99);
frBean.addUrlPatterns("/**"); // 使用 /** 匹配所有路径
return frBean;
}
梳理完毕,两种实现方式都练习🆗啦,大家选一个使用就行。
但是具体两个写法都i有什么优略呢?我们可以看下篇文章: 两种写法的区别 敬请期待~