Sa-Token 路由拦截鉴权实战:用拦截器统一做登录与权限校验
Sa-Token 路由拦截鉴权实战用拦截器统一做登录与权限校验【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架让鉴权变得简单、优雅—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token本文是 Sa-Token 官方文档《路由拦截鉴权》的深度解读与实战指南面向项目中所有接口均需登录校验、仅登录接口对外开放的典型场景。通过注册SaInterceptor拦截器与SaRouter路由匹配器你可以在不修改任何 Controller 代码的前提下集中完成登录校验、按模块划分权限、按请求方式/路径特征做差异化鉴权并掌握stop()/back()/free()等高级流程控制手段。阅读本文后你将能够用几行配置替换掉散落在每个接口上的注解让鉴权规则变得集中、可维护、易审计。一、为什么需要路由拦截鉴权假设项目有这样一个硬性需求所有接口均需要登录校验只有登录接口本身对外开放。如果给每个接口都手动加上SaCheckLogin注解工作量大且容易遗漏。此时使用拦截器鉴权模式将大大降低代码量——只需在一个配置类中注册一次拦截器框架就会在每次请求进入 Controller 之前先执行一道统一的前置审核决定请求是否放行。Sa-Token 官方为此提供了两个核心组件均可从源码中验证SaInterceptor实现 Spring MVC 的HandlerInterceptor的综合拦截器同时提供注解鉴权与路由拦截鉴权两种能力源码位于 SaInterceptor.javaSaRouter路由匹配操作工具类提供一系列match/notMatch/check链式方法一般用在全局拦截器、过滤器里做路由拦截鉴权源码位于 SaRouter.java。二、注册 Sa-Token 路由拦截器以 SpringBoot 为例新建配置类SaTokenConfigure.java实现WebMvcConfigurer接口并重写addInterceptors方法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以外的所有接口都需要登录才能访问。说明SaInterceptor是新版本提供的拦截器官方文档提示旧版本用户可参考其提供的迁移示例完成升级。另外addPathPatterns(/**)与excludePathPatterns(...)是 Spring MVC 原生拦截器的注册 API与 Sa-Token 无耦合你也可以按需调整拦截范围。从源码看 SaInterceptor 的执行链路打开 SaInterceptor.java 可以看到其核心逻辑在preHandle方法中约 90-120 行public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { try { // 这里必须确保 handler 是 HandlerMethod 类型时才能进行注解鉴权 if(isAnnotation handler instanceof HandlerMethod) { Method method ((HandlerMethod) handler).getMethod(); SaAnnotationStrategy.instance.checkMethodAnnotation.accept(method); } // Auth 校验 auth.run(handler); } catch (StopMatchException e) { // StopMatchException 异常代表停止匹配进入Controller } catch (BackResultException e) { // BackResultException 异常代表停止匹配向前端输出结果 if(response.getContentType() null) { response.setContentType(text/plain; charsetutf-8); } response.getWriter().print(e.getMessage()); return false; } // 通过验证 return true; }从中可以确认几个重要实现事实先执行注解鉴权再执行路由拦截校验只要isAnnotation为 true构造器默认值即为true见源码字段声明public boolean isAnnotation true;且 handler 是HandlerMethod就会先通过SaAnnotationStrategy.instance.checkMethodAnnotation检查方法上的注解stop()与back()的本质是抛异常StopMatchException被拦截器吞掉并放行返回true进入 Controller而BackResultException则把消息直接写给前端并返回false不再进入 Controllerback()的默认响应类型是text/plain如果需要返回 JSON需在 back 前自行设置响应头例如SaHolder.getResponse().setHeader(Content-Type, application/json;charsetUTF-8)。这些行为同样被单元测试覆盖见 SaInterceptorTest.java其中preHandle_stopMatch_pass验证了抛出StopMatchException时拦截器返回truepreHandle_backResult_writeResponse验证了抛出BackResultException时响应内容被写回、返回false且默认 Content-Type 为text/plain; charsetutf-8preHandle_backResult_keepExistingContentType验证了如果响应已设置application/json拦截器不会覆盖。三、校验函数详解new SaInterceptor(handle - StpUtil.checkLogin())是最简单的写法代表只进行登录校验。我们可以往构造函数塞一个完整的 lambda 函数来定义详细的校验规则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()匹配函数有两个参数参数一要匹配的 path 路由参数二要执行的校验函数。关于权限码user、admin等这些权限码由你的业务代码在登录后通过StpInterface接口动态返回具体实现见官方文档权限认证章节。路由拦截这里只负责按路由匹配 → 触发校验并不负责权限数据的定义。在校验函数内不只可以使用StpUtil.checkPermission(xxx)进行权限校验你还可以写任意代码例如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(/**); } }从源码看 match 与 check 的内部机制SaRouter.match(pattern, fun)之所以能匹配成功才执行函数底层由 SaRouterStaff.java 支撑。它的核心是一个isHit命中标记默认truematch(String... patterns)命中时执行isHit SaRouter.isMatchCurrURI(patterns)即用当前请求 URI 去匹配 patternnotMatch(String... patterns)命中时执行isHit !SaRouter.isMatchCurrURI(patterns)即取反check(fun)只有isHit true时才真正执行fun.run()。因此先 match 再 check等价于当前请求满足全部匹配条件时才执行校验函数。而SaRouter.match(pattern, fun)这个便捷重载在源码中就是this.match(pattern).check(fun)的组合写法。四、匹配特征详解除了 path 路由匹配SaRouter还支持根据很多其它特征进行匹配。以下是所有可匹配的特征均来自 SaRouter.java 与 SaRouterStaff.java 的公开 API// 基础写法样例匹配一个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校验函数 */ );各匹配特征说明path 匹配支持*匹配任意非/字符、**匹配任意字符含/以及{id}这种 restful 风格占位符可以传多个 pattern请求方式匹配SaHttpMethod是枚举包含GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE, CONNECT以及代表全部方式的ALL源码见 SaHttpMethod.java。判断时忽略大小写且命中ALL即为匹配成功boolean 匹配直接传入一个 boolean 值如StpUtil.isLogin()lambda 匹配传入一个返回 boolean 的 lambda实现任意自定义条件无限连缀match与notMatch可以反复叠加最终语义为所有条件同时满足其底层就是SaRouterStaff中isHit标记被逐条累加判断if(isHit)保护避免已失败的条件继续消耗性能。五、提前退出匹配链stop() 与 back()使用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函数处提前退出整个匹配函数从而忽略掉剩余的所有 match 匹配进入3/4/5都不会打印。除了stop()函数SaRouter还提供了back()函数用于停止匹配、结束执行、直接向前端返回结果// 执行back函数后将停止匹配也不会进入Controller而是直接将 back参数 作为返回值输出到前端 SaRouter.match(/user/back).back(要返回到前端的内容);stop()与back()函数的区别在于SaRouter.stop()会停止匹配进入 ControllerSaRouter.back()会停止匹配直接返回结果到前端不进入 Controller。底层原理异常驱动的流程控制从源码看这两者的行为差异十分清晰SaRouterStaff.java 中stop()在isHit时throw new StopMatchException()back(result)在isHit时throw new BackResultException(result)SaInterceptor.java 的preHandle捕获StopMatchException后什么都不做返回true放行进入 Controller捕获BackResultException后把消息getMessage()通过response.getWriter().print(...)写给前端并返回false。正是这套异常中断 拦截器兜底的设计让stop/back可以在任意深度的嵌套 lambda 里生效。官方 demo SaTokenConfigure.java 中还有一处经典用法在 CORS 预检请求处理里用SaRouter.match(SaHttpMethod.OPTIONS).free(...).back()直接结束预检请求、不进入业务代码。六、使用 free() 打开一个独立的作用域// 进入 free 独立作用域 SaRouter.match(/**).free(r - { SaRouter.match(/a/**).check(/* --- */); SaRouter.match(/b/**).check(/* --- */).stop(); SaRouter.match(/c/**).check(/* --- */); }); // 执行 stop() 函数跳出 free 后继续执行下面的 match 匹配 SaRouter.match(/**).check(/* --- */);free()的作用是打开一个独立的作用域使内部的stop()不再一次性跳出整个 Auth 函数而是仅仅跳出当前 free 作用域。/c/**与 free 外层的match仍会正常执行判断。这一点在 SaRouterStaff.java 的free方法中体现得很直接它用try { fun.run(this); } catch (StopMatchException e) { /* 跳出 free 自由匹配代码块 */ }把内部抛出的StopMatchException就地吞掉使中断效果限定在 free 块内部。七、使用注解忽略掉路由拦截校验我们可以使用SaIgnore注解忽略掉路由拦截认证。1、先配置好了拦截规则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(/**); }2、然后在Controller里又添加了忽略校验的注解SaIgnore RequestMapping(/user/getList) public SaResult getList() { System.out.println(------------ 访问进来方法); return SaResult.ok(); }请求将会跳过拦截器的校验直接进入 Controller 的方法中。注意点注解SaIgnore的忽略效果只针对SaInterceptor拦截器和 AOP 注解鉴权生效对自定义拦截器与过滤器不生效。从源码看 SaIgnoreSaIgnore.java 的定义非常简单——一个Retention(RUNTIME)、Target({METHOD, TYPE})的标记注解注释里明确写道表示被修饰的方法或类无需进行注解认证和路由拦截认证且同样强调忽略效果只针对 SaInterceptor 拦截器和 AOP 注解鉴权生效对自定义拦截器与过滤器不生效。在 SaAnnotationStrategy.java 的注解检查逻辑中约 98 行会先判断方法上是否标注了SaIgnore命中则直接放行。由于它同时支持TYPE目标你还可以把它标注在 Controller 类上使整个类下的所有接口都跳过拦截校验。八、关闭注解校验与认证前置函数SaInterceptor只要注册到项目中默认就会打开注解校验源码中isAnnotation字段默认值为true。如果要关闭此能力需要指定isAnnotation为 falseOverride 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(/**); }如上代码执行顺序为先执行 2beforeAuth→ 再执行注解鉴权 → 再执行 1auth。如果 beforeAuth 里包含SaRouter.stop()将跳过后续的注解鉴权和 auth 认证环节。补充说明isAnnotation(false)与setBeforeAuth(...)都是返回对象自身的链式方法源码见 SaInterceptor.java可以连缀使用。SaInterceptor还提供了setAuth(...)方法用于在构造后动态替换认证函数这一点在单元测试constructorsAndFluentApi中也有覆盖。九、完整示例与继续深入官方示例工程 sa-token-demo-case 提供了路由拦截鉴权的完整可运行代码配置类 SaTokenConfigure.java注册SaInterceptor演示了登录校验、按模块划分权限/admin/**、/goods/**、/orders/**等、路由打印语句以及基于SaServletFilter的全局过滤器鉴权和 CORS 预检请求处理注解鉴权示例 AtCheckController.java展示了SaCheckLogin、SaCheckPermission含SaMode.AND/OR组合、SaCheckRole等注解的配合使用方式可作为注解鉴权与路由拦截鉴权两种模式的对照参考拦截器单元测试 SaInterceptorTest.java用 Mock 请求验证了构造器、链式配置、注解鉴权开关、StopMatchException/BackResultException处理分支等关键行为。使用建议路由拦截鉴权与注解鉴权并不互斥——SaInterceptor默认同时开启两者。你可以在拦截器里做全局性的登录校验与模块粗粒度权限划分再在个别接口上用注解做细粒度的权限点校验两者叠加使用即可兼顾统一收口与局部灵活。若某个接口需要临时放行如回调接口、静态资源优先使用SaIgnore或notMatch排除而不要修改拦截器全局规则。进一步了解注解鉴权可阅读注解鉴权章节权限数据如何提供可参考权限认证章节。【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架让鉴权变得简单、优雅—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考