1. Sa-Token与Spring Boot的权限控制基础
在Java生态中,权限管理一直是系统开发的核心需求。传统的Shiro和Spring Security虽然功能强大,但配置复杂度常常让开发者望而生畏。Sa-Token作为一款轻量级Java权限认证框架,以其简洁的API和灵活的配置赢得了越来越多开发者的青睐。
我初次接触Sa-Token是在一个需要快速上线的后台管理系统项目中。当时项目已经采用Spring Boot作为基础框架,但团队对Spring Security的学习曲线感到担忧。Sa-Token的"开箱即用"特性让我们在半天内就完成了基础权限体系的搭建,这种效率让我印象深刻。
Sa-Token的核心优势在于:
- 注解式权限控制:通过
@SaCheckLogin、@SaCheckRole等注解实现方法级别的权限校验 - 路由拦截器:可灵活配置需要拦截的路径规则
- 会话管理:支持分布式环境下的会话持久化
- 踢人下线:动态权限变更时的实时生效能力
在Spring Boot中集成Sa-Token通常只需要三个基础步骤:
- 引入starter依赖
- 配置拦截规则
- 添加权限注解
但实际企业级应用中,我们往往需要更精细化的控制——比如在全局拦截的同时,放行特定的API接口。这种需求在开放平台、混合鉴权等场景中尤为常见。接下来,我将分享在Spring Boot项目中实现这种精细化控制的完整方案。
2. 环境准备与基础配置
2.1 项目初始化与依赖引入
首先创建一个标准的Spring Boot项目(2.7.x版本为宜),在pom.xml中添加Sa-Token的核心依赖:
<dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot-starter</artifactId> <version>1.34.0</version> </dependency>对于需要web支持的项目,还需要添加spring-boot-starter-web:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>注意:Sa-Token从1.30.0版本开始支持Spring Boot 3.x,但考虑到企业环境的稳定性,本文仍以Spring Boot 2.7.x为例。若使用Spring Boot 3.x,需要将sa-token版本升级至1.30.0+
2.2 基础配置项
在application.yml中添加Sa-Token的基础配置:
sa-token: # token名称(同时也是cookie名称) token-name: satoken # token有效期(单位:秒)默认30天 timeout: 2592000 # token临时有效期(指定时间内无操作就视为token过期)单位:秒 activity-timeout: -1 # 是否允许同一账号并发登录(为true时允许一起登录,为false时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个token(为true时所有登录共用一个token,为false时每次登录新建一个token) is-share: true这些配置项决定了Sa-Token的基础行为模式。其中is-concurrent和is-share的配合特别值得关注:
- 当
is-concurrent=true且is-share=false时:允许多终端登录,且每个终端有独立token - 当
is-concurrent=false时:新登录会挤掉旧登录(适合敏感系统)
3. 路径拦截的三种实现方式
3.1 注解式拦截
Sa-Token提供了一系列权限注解,可以直接在Controller方法上使用:
@RestController @RequestMapping("/user") public class UserController { // 登录校验:只有登录后才能进入该方法 @SaCheckLogin @GetMapping("/info") public String info() { return "用户信息"; } // 角色校验:必须具有指定角色才能进入该方法 @SaCheckRole("admin") @GetMapping("/delete") public String delete() { return "删除用户"; } // 权限校验:必须具有指定权限才能进入该方法 @SaCheckPermission("user:add") @PostMapping("/add") public String add() { return "添加用户"; } }这种方式的优点是直观简洁,但缺点是需要为每个方法添加注解,在大型项目中可能显得繁琐。
3.2 注册式拦截
通过实现SaInterceptor接口,我们可以自定义拦截逻辑:
@Configuration public class SaTokenConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { // 注册Sa-Token拦截器 registry.addInterceptor(new SaInterceptor(handler -> { // 指定拦截匹配规则 SaRouter.match("/**") .notMatch("/user/login", "/user/register") .check(r -> StpUtil.checkLogin()); })).addPathPatterns("/**"); } }这段代码实现了:
- 拦截所有路径(/**)
- 排除/login和/register路径
- 对其他所有路径检查登录状态
3.3 配置文件拦截
对于更复杂的路径匹配需求,可以在application.yml中配置:
sa-token: # 拦截路径配置 interceptor: # 需要拦截的路径 include: /** # 需要排除的路径 exclude: /user/login,/user/register,/swagger-ui/**,/v3/api-docs/**这种方式特别适合需要动态调整拦截规则的场景,修改配置后无需重新编译代码。
4. 特定接口放行的进阶技巧
4.1 基于注解的免鉴权
Sa-Token提供了@SaCheckSafe和@SaIgnore注解来实现免鉴权:
@SaIgnore @GetMapping("/public/data") public String publicData() { return "公开数据"; }@SaIgnore会完全绕过Sa-Token的拦截器,适用于完全公开的接口。
4.2 动态路径放行
有时我们需要根据运行时条件决定是否放行。可以通过自定义SaRouter逻辑实现:
new SaInterceptor(handler -> { SaRouter.match("/**") // 动态放行条件 .notMatch(path -> { String uri = path.getRequest().getRequestURI(); return uri.startsWith("/public/") || uri.endsWith(".html"); }) .check(r -> StpUtil.checkLogin()); })4.3 混合鉴权模式
在实际项目中,我们可能需要同时支持多种认证方式(如JWT+Session)。可以通过自定义StpLogic实现:
// 定义特殊token类型的StpLogic @Bean @Primary public StpLogic getStpLogicJwt() { return new StpLogic("jwt"); } // 在控制器中混合使用 @SaCheckLogin(type = "jwt") // 校验jwt token @SaCheckRole("admin") // 校验session token @GetMapping("/hybrid/api") public String hybridApi() { return "混合鉴权接口"; }5. 实战中的常见问题与解决方案
5.1 拦截器顺序问题
当项目中存在多个拦截器时,执行顺序可能影响鉴权结果。建议的优先级顺序为:
- CORS跨域拦截器
- Sa-Token拦截器
- 业务拦截器
可以通过order属性明确指定:
registry.addInterceptor(new SaInterceptor(...)) .addPathPatterns("/**") .order(2); // 设置优先级为25.2 静态资源被拦截
前端项目中常见的静态资源路径需要放行:
// 放行常见静态资源 .notMatch("/static/**", "/assets/**", "/**.js", "/**.css")或者使用更精确的正则匹配:
.notMatch(path -> { String uri = path.getRequest().getRequestURI(); return uri.matches(".*\\.(js|css|png|jpg|ico)$"); })5.3 微服务环境下的特殊处理
在Spring Cloud微服务架构中,内部服务调用需要特殊处理:
// 网关层拦截外部请求 .match("/api/**") .notMatch("/api/auth/**") .check(r -> StpUtil.checkLogin()); // 内部服务间调用添加特殊header SaRouter.match("/internal/**") .check(r -> { String secret = r.getRequest().getHeader("X-Internal-Secret"); if(!"service-secret-key".equals(secret)){ throw new ApiException("非法内部调用"); } });5.4 权限缓存一致性问题
当权限数据变更时,需要及时清除缓存:
// 修改角色权限后清除缓存 @Transactional public void updateRolePermissions(Long roleId, List<String> permissions) { // 更新数据库 rolePermissionDao.updateByRoleId(roleId, permissions); // 清除Sa-Token缓存 StpUtil.getSessionByLoginId(roleId, false)?.logout(); // 或者清除所有相关会话 SaSessionCustomUtil.searchSession(session -> { return session.get("role") == roleId; }, false).forEach(SaSession::logout); }6. 性能优化与安全加固
6.1 拦截器性能调优
对于高并发系统,可以优化拦截器逻辑:
// 使用缓存优化频繁访问的路径 private static final ConcurrentHashMap<String, Boolean> PATH_CACHE = new ConcurrentHashMap<>(); .notMatch(path -> { String uri = path.getRequest().getRequestURI(); return PATH_CACHE.computeIfAbsent(uri, k -> { return k.startsWith("/public/") || k.endsWith(".html"); }); })6.2 敏感操作二次验证
对于关键操作,可以强制要求重新验证身份:
@SaCheckSafe("password") // 需要验证密码 @PostMapping("/user/changePassword") public String changePassword(String newPwd) { // 业务逻辑 return "密码修改成功"; }6.3 防重放攻击
通过nonce机制防止请求被重复利用:
@PostMapping("/pay") public String pay(@RequestHeader("X-Nonce") String nonce, @RequestBody PayRequest request) { // 检查nonce是否已使用过 if(StpUtil.getSessionByLoginId(StpUtil.getLoginId()) .get("nonce_"+nonce) != null) { throw new ApiException("请求已处理"); } // 记录nonce StpUtil.getSession().set("nonce_"+nonce, true); // 处理支付逻辑 return "支付成功"; }6.4 日志审计集成
将权限操作与审计日志结合:
@SaCheckLogin @PostMapping("/delete") public String deleteUser(Long userId) { // 记录审计日志 AuditLog log = new AuditLog(); log.setUserId(StpUtil.getLoginId()); log.setAction("DELETE_USER"); log.setTargetId(userId); auditLogService.save(log); // 业务逻辑 return "删除成功"; }7. 测试策略与验证方法
7.1 单元测试方案
使用MockMvc测试拦截逻辑:
@SpringBootTest @AutoConfigureMockMvc class AuthTest { @Autowired private MockMvc mockMvc; @Test void testPublicApi() throws Exception { mockMvc.perform(get("/public/data")) .andExpect(status().isOk()); } @Test void testProtectedApiWithoutToken() throws Exception { mockMvc.perform(get("/user/info")) .andExpect(status().isUnauthorized()); } @Test void testProtectedApiWithToken() throws Exception { // 先登录获取token String token = mockMvc.perform(post("/user/login") .contentType(MediaType.APPLICATION_JSON) .content("{\"username\":\"admin\",\"password\":\"123456\"}")) .andReturn().getResponse().getHeader("satoken"); // 使用token访问受保护接口 mockMvc.perform(get("/user/info") .header("satoken", token)) .andExpect(status().isOk()); } }7.2 集成测试要点
重点测试场景包括:
- 已登录状态访问受保护接口
- 未登录状态访问受保护接口
- 携带过期token访问
- 权限变更后的实时生效测试
- 并发登录测试
7.3 压力测试建议
使用JMeter等工具模拟:
- 高频率的权限校验请求
- 大量并发登录/登出操作
- 长时间运行的会话保持测试
观察指标:
- 平均响应时间
- 错误率
- 内存占用变化
8. 生产环境部署建议
8.1 多环境配置策略
不同环境使用不同配置:
# application-dev.yml sa-token: timeout: 86400 # 开发环境1天过期 # application-prod.yml sa-token: timeout: 1800 # 生产环境30分钟过期 is-concurrent: false # 生产环境禁止并发登录8.2 集群部署方案
在集群环境中,需要配置分布式会话存储:
sa-token: # 使用Redis作为会话存储 token-dao: redis spring: redis: host: redis-cluster.example.com port: 63798.3 监控与告警
通过Spring Boot Actuator暴露健康检查:
management: endpoints: web: exposure: include: health,saToken endpoint: saToken: enabled: true自定义健康指标:
@Component public class SaTokenHealthIndicator implements HealthIndicator { @Override public Health health() { try { // 检查Redis连接状态 long count = SaManager.getSaTokenDao().searchData("*", 1).size(); return Health.up().build(); } catch (Exception e) { return Health.down().withDetail("error", e.getMessage()).build(); } } }在实际项目部署中,我通常会额外配置一个定时任务,定期检查token存储的健康状态,并在异常时发送告警通知运维团队。这种主动监控机制可以帮助我们在用户反馈前发现问题。