
项目初期,接口不多,每个 Controller 方法上加个 @SaCheckLogin 注解,清清爽爽。
但随着业务膨胀,接口从 20 个变成 200 个,你发现自己在做一件极其机械的事情 —— 每个方法上都要贴注解,漏一个就是线上事故。
我自己就干过这种蠢事。有一次新同事加了个查询接口,忘了贴注解,结果裸奔了两周才被发现。😅
后来我把整套鉴权逻辑切换成了 Sa-Token 的路由拦截模式,代码量直接砍了一大截,而且再也不用担心"漏注解"这种低级问题了。
这篇文章,咱们就把这套路由拦截鉴权从头到尾拆清楚。
思路其实很简单 —— 把鉴权逻辑从每个接口上抽出来,统一放到拦截器里做前置校验。
就像小区门口的保安,不管你去几号楼,先在大门口查一遍你的门禁卡。
这样做的好处非常明显:
以 SpringBoot2.0 为例,新建一个配置类就行:
@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");
}
}这段代码做了三件事:
addPathPatterns("/**") —— 拦截所有路由excludePathPatterns("/user/doLogin") —— 放行登录接口StpUtil.checkLogin() —— 未登录直接打回去说白了就是:除了登录接口,其他全部需要先登录。
⚠️ 这里有个细节容易踩坑:excludePathPatterns 的路径必须和你 Controller 里的 @RequestMapping 完全一致。我之前就因为多写了一个斜杠,排除没生效,debug 了好一会儿。
SaInterceptor是新版本提供的拦截器,旧版本需要迁移,参考 代码迁移示例[1]。
实际项目中,只做登录校验肯定是不够的。
比如我手上的项目,后台管理系统有这么几个模块:用户管理、商品管理、订单管理、公告管理……不同角色能访问的模块完全不同。
这时候就需要在拦截器里写更细粒度的校验逻辑:
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,定义详细认证规则
registry.addInterceptor(new SaInterceptor(handler -> {
// 指定一条 match 规则
SaRouter
.match("/**") // 拦截的 path 列表,可以写多个 */
.notMatch("/user/doLogin") // 排除掉的 path 列表,可以写多个
.check(r -> StpUtil.checkLogin()); // 要执行的校验动作,可以写完整的 lambda 表达式
// 根据路由划分模块,不同模块不同鉴权
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"));
})).addPathPatterns("/**");
}
}SaRouter.match() 的两个参数很直观:
这里有个我当时纠结过的地方 —— 校验函数里到底能写什么?
答案是:随便写,它就是个普通的 lambda。权限校验、角色校验、甚至打印日志都行:
@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("/**");
}
}说实话,这种"一个配置类管所有鉴权"的感觉,比在每个 Controller 上贴注解舒服太多了。
很多人以为 SaRouter 只能按路径匹配,其实它支持的匹配维度比你想象的多:
// 基础写法样例:匹配一个path,执行一个校验函数
SaRouter.match("/user/**").check(r -> StpUtil.checkLogin());
// 根据 path 路由匹配 ——— 支持写多个path,支持写 restful 风格路由
// 功能说明: 使用 /user , /goods 或者 /art/get 开头的任意路由都将进入 check 方法
SaRouter.match("/user/**", "/goods/**", "/art/get/{id}").check( /* 要执行的校验函数 */ );
// 根据 path 路由排除匹配
// 功能说明: 使用 .html , .css 或者 .js 结尾的任意路由都将跳过, 不会进入 check 方法
SaRouter.match("/**").notMatch("*.html", "*.css", "*.js").check( /* 要执行的校验函数 */ );
// 根据请求类型匹配
SaRouter.match(SaHttpMethod.GET).check( /* 要执行的校验函数 */ );
// 根据一个 boolean 条件进行匹配
SaRouter.match( StpUtil.isLogin() ).check( /* 要执行的校验函数 */ );
// 根据一个返回 boolean 结果的lambda表达式匹配
SaRouter.match( r -> StpUtil.isLogin() ).check( /* 要执行的校验函数 */ );
// 多个条件一起使用
// 功能说明: 必须是 Get 请求 并且 请求路径以 `/user/` 开头
SaRouter.match(SaHttpMethod.GET).match("/user/**").check( /* 要执行的校验函数 */ );
// 可以无限连缀下去
// 功能说明: 同时满足 Get 方式请求, 且路由以 /admin 开头, 路由中间带有 /send/ 字符串, 路由结尾不能是 .js 和 .css
SaRouter
.match(SaHttpMethod.GET)
.match("/admin/**")
.match("/**/send/**")
.notMatch("/**/*.js")
.notMatch("/**/*.css")
// ....
.check( /* 只有上述所有条件都匹配成功,才会执行最后的check校验函数 */ );我在项目里用得最多的是 路径匹配 + 排除静态资源 这个组合。因为前后端没有完全分离的老项目,静态资源请求也会走拦截器,不排除的话页面直接白屏。
👇 这个写法基本是我每个项目的标配:
SaRouter.match("/**").notMatch("*.html", "*.css", "*.js").check(r -> StpUtil.checkLogin());当你写了很多条 match 规则的时候,有时候想 在某个节点提前结束整个匹配流程。
SaRouter.stop() 就是干这个的:
registry.addInterceptor(new SaInterceptor(handler -> {
SaRouter.match("/**").check(r -> System.out.println("进入1"));
SaRouter.match("/**").check(r -> System.out.println("进入2")).stop();
SaRouter.match("/**").check(r -> System.out.println("进入3"));
SaRouter.match("/**").check(r -> System.out.println("进入4"));
SaRouter.match("/**").check(r -> System.out.println("进入5"));
})).addPathPatterns("/**");运行到第 2 条时,stop() 会 直接跳出整个匹配函数,3、4、5 全部被忽略。但请求还是会正常进入 Controller。
还有一个 back() 函数,更狠 —— 连 Controller 都不进了,直接返回结果给前端:
// 执行back函数后将停止匹配,也不会进入Controller,而是直接将 back参数 作为返回值输出到前端
SaRouter.match("/user/back").back("要返回到前端的内容");💡 划重点,这两个的区别:
stop() —— 停止匹配,但 继续进入 Controllerback() —— 停止匹配,直接把结果甩给前端,Controller 都不走我在项目里用 back() 做过一个简单的维护模式开关:系统维护时直接在拦截器里 back("系统维护中"),连 Controller 都不用改。虽然不算优雅,但胜在简单粗暴,上线快。
用 stop() 有个问题 —— 它会跳出整个认证函数。
如果你只想跳出一组规则,而不影响后面的规则怎么办?
free() 就是干这个的,它会开辟一个 独立作用域,内部的 stop() 只在这个作用域里生效:
// 进入 free 独立作用域
SaRouter.match("/**").free(r -> {
SaRouter.match("/a/**").check(/* --- */);
SaRouter.match("/b/**").check(/* --- */).stop();
SaRouter.match("/c/**").check(/* --- */);
});
// 执行 stop() 函数跳出 free 后继续执行下面的 match 匹配
SaRouter.match("/**").check(/* --- */);上面代码里,/b/** 的 stop() 只会跳出 free 作用域,外面的 match 不受影响,照常执行。
说实话这个功能我用得不多,但在规则特别复杂的大型项目里,确实能避免 stop() 误伤的问题。
有时候你配好了全局拦截,但个别接口就是需要放行。比如健康检查接口、第三方回调接口。
这时候在 excludePathPatterns 里加太多路径也不现实,用 @SaIgnore 注解更干净:
先确保你有全局拦截配置:
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handler -> {
// 根据路由划分模块,不同模块不同鉴权
SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
// ...
})).addPathPatterns("/**");
}然后在需要放行的接口上贴注解:
@SaIgnore
@RequestMapping("/user/getList")
public SaResult getList() {
System.out.println("------------ 访问进来方法");
return SaResult.ok();
}贴了 @SaIgnore,这个接口就会 跳过拦截器校验,直接进入方法体。
⚠️ 但这里有个大坑要注意:
@SaIgnore 只对 SaInterceptor 拦截器 和 AOP 注解鉴权 生效。如果你项目里还有自定义拦截器或者 Filter,@SaIgnore 管不到它们。
我之前就因为这个问题排查了半天 —— 以为加了 @SaIgnore 就万事大吉了,结果请求还是被另一个自定义 Filter 拦住了。
SaInterceptor 注册后,默认会自动开启注解校验(就是 @SaCheckLogin、@SaCheckRole 这些注解会自动生效)。
如果你只想用路由拦截,不想要注解校验,可以手动关掉:
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(
new SaInterceptor(handle -> {
SaRouter.match("/**").check(r -> StpUtil.checkLogin());
}).isAnnotation(false) // 指定关闭掉注解鉴权能力,这样框架就只会做路由拦截校验了
).addPathPatterns("/**");
}另外还有一个 setBeforeAuth,可以注册 认证前置函数:
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handle -> {
System.out.println(1);
})
.setBeforeAuth(handle -> {
System.out.println(2);
})
).addPathPatterns("/**");
}执行顺序是:先 beforeAuth → 再注解鉴权 → 最后 auth 函数。
如果 beforeAuth 里调了 SaRouter.stop(),后面的注解鉴权和 auth 认证 全部跳过。
这个功能在做灰度发布或者临时绕过鉴权调试的时候挺好用的。
鉴权这件事,能用机制保障的,就别靠人肉记忆。
路由拦截的核心价值不是"少写几行代码",而是 把鉴权从"每个开发者都要记住"变成"框架默认就有"。
一句话总结我的实践经验:
全局拦截兜底 +
@SaIgnore开白名单 +SaRouter做细粒度控制,这三板斧基本能覆盖 90% 的鉴权场景。
本章源码(GitHub):Sa-Token 路由拦截示例 —— sa-token-demo-interceptor[2]
相关文章推荐:
[1] 代码迁移示例: https://blog.csdn.net/shengzhang_/article/details/126458949
[2] 本章源码(GitHub):Sa-Token 路由拦截示例 —— sa-token-demo-interceptor: https://github.com/BNTang/Sa-Token-Demo/tree/main/sa-token-demo-interceptor
如果这篇文章帮到了你,不妨点个分享给同样需要的朋友吧! 你的每一次支持,都是我持续创作的动力!💪