首页
/ 10分钟上手!Sa-Token自定义注解实现权限校验的优雅实践

10分钟上手!Sa-Token自定义注解实现权限校验的优雅实践

2026-02-04 04:58:27作者:裴麒琰

一、为什么需要自定义注解?

你是否还在这样编写权限校验代码?

@GetMapping("/user/info")
public String getUserInfo() {
    // 1. 检查是否登录
    if(!StpUtil.isLogin()) {
        throw new NotLoginException("请先登录");
    }
    // 2. 检查是否有管理员角色
    if(!StpUtil.hasRole("admin")) {
        throw new NotRoleException("无管理员权限");
    }
    // 3. 检查是否有用户查询权限
    if(!StpUtil.hasPermission("user:query")) {
        throw new NotPermissionException("无用户查询权限");
    }
    // 4. 执行业务逻辑
    return "用户信息...";
}

这种硬编码方式不仅充斥着重复代码,还严重污染了业务逻辑。更优雅的解决方案是使用注解式鉴权

@GetMapping("/user/info")
@SaCheckLogin  // 登录校验
@SaCheckRole("admin")  // 角色校验
@SaCheckPermission("user:query")  // 权限校验
public String getUserInfo() {
    // 直接执行业务逻辑
    return "用户信息...";
}

Sa-Token作为轻量级Java权限认证框架,提供了丰富的注解式鉴权能力。本文将深入探讨如何基于Sa-Token实现自定义注解的优雅实践,让你的权限管理代码更加简洁、可维护。

二、Sa-Token内置注解解析

Sa-Token框架在cn.dev33.satoken.annotation包下提供了多个开箱即用的权限注解,我们先通过源码分析其设计原理。

2.1 @SaCheckLogin:登录认证注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface SaCheckLogin {
    /**
     * 多账号体系下所属的账号体系标识
     */
    String type() default "";
}

核心特性

  • 作用范围:方法或类级别
  • 运行时保留:@Retention(RUNTIME)确保可以通过反射获取注解信息
  • 账号体系支持:通过type参数适配多账号体系场景

2.2 @SaCheckRole:角色认证注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface SaCheckRole {
    String type() default "";
    
    /** 需要校验的角色标识数组 */
    String [] value() default {};
    
    /** 验证模式:AND | OR,默认AND */
    SaMode mode() default SaMode.AND;
}

核心特性

  • 多角色校验:支持同时校验多个角色
  • 灵活验证模式:AND(需全部拥有)和OR(只需一个拥有)两种模式

2.3 @SaCheckPermission:权限认证注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface SaCheckPermission {
    String type() default "";
    String [] value() default {};
    SaMode mode() default SaMode.AND;
    
    /** 权限校验不通过时的角色备选方案 */
    String[] orRole() default {};
}

创新特性

  • orRole参数:提供权限与角色的"或"关系校验,例如:
    @SaCheckPermission(value="user-add", orRole="admin")
    
    代表:具有user-add权限admin角色其一即可通过校验

三、自定义注解实现原理

3.1 注解解析流程

Sa-Token通过AOP(面向切面编程)技术实现注解解析,核心流程如下:

sequenceDiagram
    participant 客户端
    participant Sa-Token拦截器
    participant 业务方法
    participant 鉴权逻辑
    
    客户端->>Sa-Token拦截器: 请求目标方法
    Sa-Token拦截器->>Sa-Token拦截器: 扫描方法/类上的注解
    alt 存在权限注解
        Sa-Token拦截器->>鉴权逻辑: 执行注解对应的鉴权规则
        alt 鉴权通过
            Sa-Token拦截器->>业务方法: 执行目标方法
            业务方法-->>客户端: 返回结果
        else 鉴权失败
            鉴权逻辑-->>客户端: 抛出异常(未登录/无权限)
        end
    else 不存在权限注解
        Sa-Token拦截器->>业务方法: 直接执行目标方法
        业务方法-->>客户端: 返回结果
    end

3.2 核心实现机制

Sa-Token通过拦截器/切面实现注解解析,关键步骤包括:

  1. 注解扫描:在方法执行前扫描目标方法及所属类上的所有注解
  2. 注解分类:识别出@SaCheckLogin@SaCheckRole等权限注解
  3. 规则匹配:根据注解类型执行对应的鉴权逻辑
  4. 结果处理:鉴权通过则继续执行,失败则抛出相应异常

四、自定义注解实战:数据权限控制

假设我们需要实现数据权限控制:不同部门的用户只能访问自己部门的数据。下面通过自定义注解@SaDataScope实现这一需求。

4.1 定义注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SaDataScope {
    /** 
     * 数据权限范围 
     * all: 所有数据
     * dept: 本部门数据
     * self: 个人数据
     */
    String scope() default "dept";
    
    /** 数据权限字段名 */
    String deptIdField() default "dept_id";
}

4.2 实现注解解析器

@Component
@Aspect
public class SaDataScopeAspect {

    @Around("@annotation(saDataScope)")
    public Object around(ProceedingJoinPoint joinPoint, SaDataScope saDataScope) throws Throwable {
        // 1. 获取当前登录用户
        StpUserLoginModel loginUser = StpUtil.getLoginModel();
        Long userId = loginUser.getUserId();
        Long deptId = loginUser.getDeptId();
        
        // 2. 根据注解参数确定数据权限范围
        String scope = saDataScope.scope();
        String deptIdField = saDataScope.deptIdField();
        
        // 3. 构建数据权限条件
        StringBuilder sqlCondition = new StringBuilder();
        if ("all".equals(scope)) {
            // 管理员可查看所有数据
            if(!StpUtil.hasRole("admin")) {
                throw new NotRoleException("无查看所有数据权限");
            }
        } else if ("dept".equals(scope)) {
            // 普通用户只能查看本部门数据
            sqlCondition.append(deptIdField).append(" = ").append(deptId);
        } else if ("self".equals(scope)) {
            // 只能查看个人数据
            sqlCondition.append("create_user_id = ").append(userId);
        }
        
        // 4. 将数据权限条件注入到业务方法参数中
        Object[] args = joinPoint.getArgs();
        // ... 此处省略参数处理逻辑 ...
        
        // 5. 执行目标方法
        return joinPoint.proceed(args);
    }
}

4.3 使用自定义注解

@Service
public class UserServiceImpl implements UserService {

    @Override
    @SaCheckLogin  // 登录校验
    @SaCheckPermission("user:list")  // 权限校验
    @SaDataScope(scope = "dept", deptIdField = "department_id")  // 数据权限控制
    public PageInfo<User> getUserList(int pageNum, int pageSize) {
        // 业务逻辑实现...
    }
}

五、高级特性与最佳实践

5.1 注解组合使用

Sa-Token注解支持多注解组合使用,实现复杂鉴权逻辑:

// 登录校验 + 角色校验(admin或manager) + 权限校验(user:edit)
@SaCheckLogin
@SaCheckRole(value = {"admin", "manager"}, mode = SaMode.OR)
@SaCheckPermission("user:edit")
public Result<User> updateUser(User user) {
    // ...
}

5.2 类级别注解

将注解标注在类上,可对类中所有方法生效:

@RestController
@RequestMapping("/admin")
@SaCheckRole("admin")  // 整个控制器都需要admin角色
public class AdminController {
    
    @GetMapping("/user/list")
    // 继承类上的@SaCheckRole("admin")注解
    public List<User> getUserList() { ... }
    
    @GetMapping("/log/list")
    // 继承类上的@SaCheckRole("admin")注解
    public List<Log> getLogList() { ... }
}

5.3 注解优先级

当类级别和方法级别都标注注解时,方法级别注解优先:

@RestController
@RequestMapping("/user")
@SaCheckLogin  // 类级别:需要登录
public class UserController {
    
    @GetMapping("/info")
    // 继承类级别@SaCheckLogin注解
    public User getUserInfo() { ... }
    
    @GetMapping("/public")
    @SaIgnore  // 方法级别:忽略登录校验,更高优先级
    public String getPublicInfo() { ... }
}

六、性能优化建议

6.1 注解解析缓存

对于频繁访问的接口,建议对注解解析结果进行缓存:

// 使用Caffeine缓存注解解析结果
private final LoadingCache<Method, List<SaAnnotation>> annotationCache = Caffeine.newBuilder()
    .maximumSize(1000)
    .expireAfterWrite(30, TimeUnit.MINUTES)
    .build(method -> parseAnnotations(method));

6.2 批量鉴权处理

多个注解同时存在时,合并鉴权逻辑减少重复操作:

// 优化前:多次调用StpUtil
if(hasLoginAnnotation) StpUtil.checkLogin();
if(hasRoleAnnotation) StpUtil.checkRole(roles);
if(hasPermissionAnnotation) StpUtil.checkPermission(permissions);

// 优化后:一次调用完成多维度鉴权
StpUtil.checkMulti(loginCheck, roleChecks, permissionChecks);

七、常见问题解决方案

7.1 注解不生效问题排查

flowchart TD
    A[注解不生效] --> B{是否配置AOP?}
    B -->|否| C[添加@EnableAspectJAutoProxy注解]
    B -->|是| D{注解是否标注在接口上?}
    D -->|是| E[移至实现类或配置cglib代理]
    D -->|否| F{是否在拦截器排除列表?}
    F -->|是| G[从排除列表移除]
    F -->|否| H{是否有异常被全局捕获?}
    H -->|是| I[检查异常处理逻辑]
    H -->|否| J[查看Sa-Token日志定位问题]

7.2 多模块项目注解扫描

在SpringBoot应用中,确保注解解析器所在包被扫描到:

@SpringBootApplication
@ComponentScan(basePackages = {
    "cn.dev33.satoken",  // Sa-Token组件
    "com.yourproject.annotation"  // 自定义注解解析器所在包
})
public class YourApplication { ... }

八、总结与展望

Sa-Token的注解式鉴权为Java权限控制带来了优雅解决方案,其核心优势包括:

  1. 代码解耦:将权限逻辑从业务代码中剥离
  2. 使用便捷:一行注解即可完成复杂的权限控制
  3. 扩展性强:支持自定义注解实现业务特定的权限规则

随着项目复杂度提升,建议进一步探索:

  • 注解的组合使用模式
  • 动态权限注解(注解参数从数据库动态获取)
  • 注解与分布式权限的结合

通过Sa-Token注解,让权限控制代码更简洁、更优雅、更易于维护!

登录后查看全文
热门项目推荐
相关项目推荐