Sa-Token路由拦截鉴权实战与最佳实践 1. 为什么需要路由拦截鉴权在传统的Web开发中我们经常遇到这样的场景90%的接口都需要登录后才能访问只有少数几个登录/注册接口需要对外开放。如果给每个需要鉴权的接口都加上SaCheckLogin注解不仅会产生大量重复代码还容易遗漏某些接口的注解配置。我曾经接手过一个老项目开发者给200多个接口都加上了CheckAuth注解。后来需求变更需要临时开放部分接口给第三方调用结果因为漏改了3个接口的注解导致线上事故。这种注解污染的问题在大型项目中尤为明显。路由拦截鉴权的核心价值在于集中管理所有鉴权规则在一个地方配置避免分散在各个Controller中灵活控制可以基于URL模式、HTTP方法等特征进行细粒度控制减少遗漏通过正向匹配反向排除机制确保不会漏掉任何接口动态调整修改鉴权策略时只需改动一处配置2. Sa-Token基础环境搭建2.1 依赖引入与版本选择在pom.xml中添加Sa-Token依赖时需要注意SpringBoot版本兼容性问题!-- 针对SpringBoot 2.x版本 -- dependency groupIdcn.dev33/groupId artifactIdsa-token-spring-boot-starter/artifactId version1.34.0/version /dependency !-- 针对SpringBoot 3.x版本 -- dependency groupIdcn.dev33/groupId artifactIdsa-token-spring-boot3-starter/artifactId version1.34.0/version /dependency踩坑提醒很多开发者会忽略版本匹配问题导致启动时报ClassNotFoundException。我曾遇到一个团队同时使用SpringBoot 2.7和3.0的子模块结果因为依赖混用导致鉴权完全失效。2.2 最小化配置示例创建一个基础的配置类SaTokenConfig.javaConfiguration public class SaTokenConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle - { // 最简单的登录校验 SaRouter.match(/**).check(r - StpUtil.checkLogin()); })).addPathPatterns(/**); } }这个配置实现了拦截所有请求路径/**对每个请求执行登录检查StpUtil.checkLogin()未登录时会自动抛出NotLoginException3. 进阶路由匹配策略3.1 多路径匹配与排除实际项目中我们通常需要更精细的控制SaRouter .match(/admin/**, /user/**) // 匹配admin和user路径 .notMatch(/user/public/**) // 排除user下的public路径 .check(r - { StpUtil.checkLogin(); StpUtil.checkRole(admin); });这种配置方式特别适合前后端分离项目比如/api/admin/**需要管理员权限/api/user/**需要用户权限/api/public/**完全开放3.2 基于HTTP方法的鉴权RESTful接口通常需要根据请求方法区分权限SaRouter .match(SaHttpMethod.GET) .match(/articles/**) .check(r - StpUtil.checkPermission(article:read)); SaRouter .match(SaHttpMethod.POST) .match(/articles/**) .check(r - StpUtil.checkPermission(article:write));这种配置实现了GET请求需要article:read权限POST请求需要article:write权限3.3 动态条件匹配Sa-Token支持通过Lambda表达式实现动态条件SaRouter .match(r - { String uri r.getRequest().getRequestURI(); return uri.startsWith(/vip/) isWeekend(); }) .check(r - StpUtil.checkPermission(vip.weekend));这个例子实现了周末访问/vip/路径时需要额外权限工作日则不需要4. 鉴权函数深度解析4.1 多条件组合校验复杂业务场景可能需要组合多个校验条件SaRouter.match(/order/**).check(r - { // 必须登录 StpUtil.checkLogin(); // 必须具有order权限 StpUtil.checkPermission(order); // IP白名单校验 if(!ipWhiteList.contains(r.getIp())) { throw new ApiException(非法IP访问); } // 风控校验 riskControlService.check(r); });4.2 异常处理机制默认情况下校验失败会抛出异常。我们可以自定义错误响应SaRouter.match(/**).check(r - { try { StpUtil.checkLogin(); } catch (NotLoginException e) { // 自定义未登录响应 r.getResponse().setHeader(Content-Type, application/json); r.getResponse().getWriter().write( JSON.toJSONString(SaResult.error(请先登录)) ); SaRouter.back(); } });4.3 性能优化建议在大流量场景下鉴权逻辑需要注意缓存权限数据使用SaCache注解缓存权限校验结果避免重复校验同一个请求的相同权限只校验一次异步日志记录审计日志采用异步方式记录SaCache(timeout 5*60) // 缓存5分钟 public boolean hasPermission(String permission) { return permissionService.hasPermission(StpUtil.getLoginId(), permission); }5. 实战中的特殊场景处理5.1 接口免鉴权配置有些接口需要特殊处理// 方法1在拦截器中排除 SaRouter.match(/**) .notMatch(/swagger/**, /doc.html, /webjars/**) .check(r - StpUtil.checkLogin()); // 方法2使用SaIgnore注解 SaIgnore GetMapping(/public/data) public SaResult getPublicData() { // ... }经验分享Swagger等调试接口一定要记得排除我有次排查2小时才发现是Swagger被拦截了。5.2 微服务间的鉴权处理在微服务架构中内部调用也需要鉴权SaRouter.match(/internal/**).check(r - { String serviceToken r.getHeader(Service-Token); if(!SECRET_KEY.equals(serviceToken)) { throw new ApiException(服务间调用认证失败); } });建议方案为每个服务分配唯一Token使用HTTPS保证传输安全定期轮换Token5.3 前后端分离的特殊处理前端可能遇到的一些问题及解决方案OPTIONS请求处理SaRouter.match(SaHttpMethod.OPTIONS).free(r - {});Token自动续期SaRouter.match(/**).check(r - { if(StpUtil.getTokenTimeout() 3600) { StpUtil.renewTimeout(2*3600); } });多端登录处理// 在配置中开启多端登录 Bean public StpLogic getStpLogic() { return new StpLogic(user).setIsConcurrent(true); }6. 安全增强措施6.1 防重放攻击为敏感接口添加时间戳校验SaRouter.match(/pay/**).check(r - { long timestamp Long.parseLong(r.getHeader(Timestamp)); if(Math.abs(System.currentTimeMillis() - timestamp) 5000) { throw new ApiException(请求已过期); } // 签名校验 String sign r.getHeader(Sign); String expectSign buildSign(r, timestamp); if(!expectSign.equals(sign)) { throw new ApiException(签名错误); } });6.2 频率限制防止接口被暴力调用SaRouter.match(/sms/send).check(r - { String key sms_limit: r.getIp(); long count redisTemplate.opsForValue().increment(key); if(count 1) { redisTemplate.expire(key, 1, TimeUnit.MINUTES); } if(count 5) { throw new ApiException(操作太频繁); } });6.3 敏感操作二次验证关键操作需要额外验证SaRouter.match(/account/transfer).check(r - { StpUtil.checkLogin(); // 检查是否已完成二次验证 if(!StpUtil.getExtra(has2FA).equals(true)) { throw new ApiException(请先完成二次验证); } });7. 调试与问题排查7.1 常见问题解决方案拦截不生效检查拦截器注册顺序确认路径匹配规则是否正确查看是否有SaIgnore注解干扰循环重定向// 错误示例登录接口也被拦截了 SaRouter.match(/**).check(r - StpUtil.checkLogin()); // 正确做法 SaRouter.match(/**).notMatch(/login).check(r - StpUtil.checkLogin());权限缓存不一致// 权限变更后清除缓存 StpUtil.logoutByLoginId(userId);7.2 日志监控建议建议记录以下日志鉴权失败记录包含IP、路径、用户信息高频访问监控异常权限变更记录SaRouter.match(/**).check(r - { try { StpUtil.checkLogin(); } catch (Exception e) { log.warn(鉴权失败ip{}, path{}, r.getIp(), r.getPath()); throw e; } });7.3 性能监控指标需要重点监控鉴权平均耗时拦截器执行时间权限缓存命中率SaRouter.match(/**).check(r - { long start System.currentTimeMillis(); try { StpUtil.checkLogin(); } finally { long cost System.currentTimeMillis() - start; metrics.record(auth_time, cost); } });