Sa-Token完全学习指南

目录

1. Sa-Token简介

1.1 什么是Sa-Token?

1.2 Sa-Token架构图

1.3 Sa-Token vs 其他框架

1.4 适用场景

2. 环境搭建与快速开始

2.1 Maven依赖

SpringBoot环境

WebFlux环境

2.2 基础配置

application.yml配置

2.3 创建启动类

2.4 第一个登录接口

2.5 统一响应类

2.6 全局异常处理

3. 核心API详解

3.1 StpUtil核心方法

登录相关API

Token相关API

会话查询API

3.2 Session操作API

4. 登录认证机制

4.1 登录流程详解

4.2 多端登录控制

4.3 Remember Me功能

4.4 登录扩展参数

5. 权限验证详解

5.1 权限验证基础

5.2 权限验证API

5.3 权限通配符

5.4 自定义权限验证器

5.5 数据权限控制

6. Session会话管理

6.1 Session基本操作

6.2 Session超时管理

6.3 自定义Session

6.4 Session监听器

7. 注解鉴权

7.1 基础鉴权注解

7.2 复合条件注解

7.3 安全认证注解

7.4 禁用验证注解

7.5 自定义注解

7.6 类级别注解

8. 路由拦截鉴权

8.1 基于拦截器的鉴权

8.2 SaRouter路由匹配

8.3 高级路由配置

8.4 基于Filter的全局鉴权

8.5 WebFlux环境下的路由拦截

9. 多账户体系

9.1 多账户配置

9.2 管理员账户体系

9.3 多账户权限接口实现

9.4 多账户登录接口

9.5 多账户拦截器配置

9.6 多账户注解使用

10. 集成Redis

10.1 Redis依赖配置

10.2 Redis配置

10.3 Redis序列化配置

10.4 Redis存储测试

10.5 自定义Redis操作

10.6 Redis集群配置

10.7 Redis哨兵配置

10.8 性能监控和优化

11. 集成JWT

11.1 JWT依赖配置

11.2 JWT配置

11.3 JWT配置类

11.4 JWT登录接口

11.5 JWT工具类

11.6 JWT拦截器

11.7 JWT微服务鉴权

11.8 JWT与Redis混合模式

12. 微服务使用

12.1 微服务网关鉴权

Spring Cloud Gateway集成

网关配置

网关鉴权配置

网关自定义过滤器

12.2 服务间认证

Feign客户端集成

服务间Token传递

12.3 分布式Session同步

12.4 微服务配置中心集成

12.5 服务链路追踪

13. 单点登录SSO

13.1 SSO基础配置

13.2 SSO认证服务端

13.3 SSO客户端

13.4 SSO配置详解

13.5 自定义SSO处理器

13.6 跨域SSO配置

13.7 SSO统一用户中心

14. OAuth2.0

14.1 OAuth2.0服务端配置

14.2 OAuth2.0服务端接口

14.3 OAuth2.0客户端

14.4 OAuth2.0客户端管理

14.5 OAuth2.0授权记录

14.6 自定义OAuth2.0扩展

15. 配置详解

15.1 完整配置示例

15.2 多环境配置

15.3 自定义配置类

15.4 动态配置更新

15.5 配置校验

15.6 配置加密

16. 插件扩展

16.1 自定义Token生成器

16.2 自定义持久层

16.3 自定义权限验证器

16.4 自定义Session存储

16.5 自定义Context处理器


1. Sa-Token简介

1.1 什么是Sa-Token?

Sa-Token是一个轻量级Java权限认证框架,主要解决:登录认证、权限认证、Session会话、单点登录、OAuth2.0、微服务网关鉴权等一系列权限相关问题。

核心特点:

  • 简单易用:API设计简洁,上手极快
  • 功能强大:登录认证、权限认证、踢人下线、自动续签等功能一应俱全
  • 高度集成:完美集成SpringBoot、WebFlux、Solon等主流框架
  • 多端支持:同时支持多种前端框架:Vue、React、uniapp、小程序等
  • 分布式:完美支持分布式系统,微服务架构
  • 多账户认证:比如一个系统同时有User表和Admin表,两套账号分开鉴权

1.2 Sa-Token架构图

                    Sa-Token架构
                         |
        +----------------+----------------+
        |                |                |
    登录认证          权限认证          会话管理
        |                |                |
   +----+----+      +----+----+      +----+----+
   |         |      |         |      |         |
  账号     密码    角色     权限    Session   踢人
  验证     加密    验证     验证     会话     下线

1.3 Sa-Token vs 其他框架

特性 Sa-Token Spring Security Shiro
学习难度 简单 复杂 中等
代码量 中等
扩展性 中等
社区活跃度
文档质量 优秀 良好 一般

1.4 适用场景

  • 中小型项目:快速开发,简单易用
  • 微服务架构:分布式鉴权,网关鉴权
  • 多端应用:APP、小程序、Web等多端统一鉴权
  • 单点登录:多个系统统一登录
  • OAuth2.0:第三方登录集成

2. 环境搭建与快速开始

2.1 Maven依赖

SpringBoot环境
<!-- pom.xml -->
<dependencies>
    <!-- Sa-Token权限认证,在线文档:https://sa-token.cc -->
    <dependency>
        <groupId>cn.dev33</groupId>
        <artifactId>sa-token-spring-boot-starter</artifactId>
        <version>1.37.0</version>
    </dependency>
    
    <!-- Sa-Token整合Redis (使用jackson序列化方式) -->
    <dependency>
        <groupId>cn.dev33</groupId>
        <artifactId>sa-token-dao-redis-jackson</artifactId>
        <version>1.37.0</version>
    </dependency>
    
    <!-- 提供Redis连接池 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
    
    <!-- SpringBoot相关 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>
WebFlux环境
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-reactor-spring-boot-starter</artifactId>
    <version>1.37.0</version>
</dependency>

2.2 基础配置

application.yml配置
server:
  port: 8081

# Sa-Token配置
sa-token:
  # token名称 (同时也是cookie名称)
  token-name: satoken
  # token有效期,单位s 默认30天, -1代表永不过期
  timeout: 2592000
  # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
  activity-timeout: -1
  # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
  is-concurrent: true
  # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
  is-share: true
  # token风格
  token-style: uuid
  # 是否输出操作日志
  is-log: false

# Redis配置
spring:
  redis:
    host: localhost
    port: 6379
    password: 
    database: 0
    timeout: 10s
    lettuce:
      pool:
        # 连接池最大连接数
        max-active: 200
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms
        # 连接池中的最大空闲连接
        max-idle: 10
        # 连接池中的最小空闲连接
        min-idle: 0

2.3 创建启动类

@SpringBootApplication
public class SaTokenApplication {
    public static void main(String[] args) {
        SpringApplication.run(SaTokenApplication.class, args);
        System.out.println("启动成功:Sa-Token配置如下:" + SaManager.getConfig());
    }
}

2.4 第一个登录接口

@RestController
@RequestMapping("/user")
public class UserController {

    // 登录接口
    @PostMapping("/login")
    public Result login(@RequestBody LoginDto loginDto) {
        // 1. 验证用户名和密码
        if ("admin".equals(loginDto.getUsername()) && "123456".equals(loginDto.getPassword())) {
            // 2. 登录成功,调用Sa-Token的登录方法
            StpUtil.login(10001);
            
            // 3. 返回token信息
            return Result.success("登录成功")
                    .put("token", StpUtil.getTokenValue())
                    .put("tokenInfo", StpUtil.getTokenInfo());
        }
        
        return Result.error("用户名或密码错误");
    }

    // 查询登录状态
    @GetMapping("/isLogin")
    public Result isLogin() {
        boolean isLogin = StpUtil.isLogin();
        return Result.success("当前登录状态:" + isLogin)
                .put("isLogin", isLogin)
                .put("loginId", StpUtil.getLoginIdDefaultNull());
    }

    // 获取用户信息
    @GetMapping("/info")
    public Result info() {
        // 检查是否登录,如果未登录会抛出异常
        StpUtil.checkLogin();
        
        // 模拟从数据库获取用户信息
        Map<String, Object> userInfo = new HashMap<>();
        userInfo.put("userId", StpUtil.getLoginId());
        userInfo.put("username", "admin");
        userInfo.put("nickname", "管理员");
        
        return Result.success("获取用户信息成功").put("userInfo", userInfo);
    }

    // 退出登录
    @PostMapping("/logout")
    public Result logout() {
        StpUtil.logout();
        return Result.success("退出登录成功");
    }
}

2.5 统一响应类

public class Result {
    private int code;
    private String message;
    private Object data;
    private Map<String, Object> map = new HashMap<>();

    public static Result success(String message) {
        Result result = new Result();
        result.code = 200;
        result.message = message;
        return result;
    }

    public static Result error(String message) {
        Result result = new Result();
        result.code = 500;
        result.message = message;
        return result;
    }

    public Result put(String key, Object value) {
        this.map.put(key, value);
        return this;
    }

    // getter/setter方法...
    public int getCode() { return code; }
    public void setCode(int code) { this.code = code; }
    
    public String getMessage() { return message; }
    public void setMessage(String message) { this.message = message; }
    
    public Object getData() { return data; }
    public void setData(Object data) { this.data = data; }
    
    public Map<String, Object> getMap() { return map; }
    public void setMap(Map<String, Object> map) { this.map = map; }
}

2.6 全局异常处理

@RestControllerAdvice
public class GlobalExceptionHandler {

    // 拦截Sa-Token异常
    @ExceptionHandler(NotLoginException.class)
    public Result handlerNotLoginException(NotLoginException nle) {
        String message = "";
        if (nle.getType().equals(NotLoginException.NOT_TOKEN)) {
            message = "未提供token";
        } else if (nle.getType().equals(NotLoginException.INVALID_TOKEN)) {
            message = "token无效";
        } else if (nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
            message = "token已过期";
        } else if (nle.getType().equals(NotLoginException.BE_REPLACED)) {
            message = "token已被顶下线";
        } else if (nle.getType().equals(NotLoginException.KICK_OUT)) {
            message = "token已被踢下线";
        } else {
            message = "当前会话未登录";
        }
        
        // 返回给前端
        return Result.error(message).put("code", 401);
    }

    // 拦截权限异常
    @ExceptionHandler(NotPermissionException.class)
    public Result handlerNotPermissionException(NotPermissionException e) {
        return Result.error("权限不足:" + e.getPermission()).put("code", 403);
    }

    // 拦截角色异常
    @ExceptionHandler(NotRoleException.class)
    public Result handlerNotRoleException(NotRoleException e) {
        return Result.error("角色不足:" + e.getRole()).put("code", 403);
    }
}

3. 核心API详解

3.1 StpUtil核心方法

Sa-Token的所有功能都通过StpUtil类来实现,这是最重要的工具类。

登录相关API
public class LoginController {
    
    // 登录
    @PostMapping("/login")
    public Result login(@RequestParam String username, @RequestParam String password) {
        // 验证用户名密码(省略具体验证逻辑)
        if (checkUser(username, password)) {
            // 登录,参数为用户id
            StpUtil.login(userId);
            
            // 其他登录方式
            // StpUtil.login(userId, "PC");           // 指定设备类型登录
            // StpUtil.login(userId, true);           // 是否为持久Cookie(记住我)
            // StpUtil.login(userId, new SaLoginModel()
            //     .setDevice("PC")                    // 设备类型
            //     .setTimeout(60 * 60 * 24 * 7)      // 设置token有效期为7天
            //     .setIsLastingCookie(true));         // 是否为持久Cookie
            
            return Result.success("登录成功");
        }
        return Result.error("用户名或密码错误");
    }
    
    // 检查登录状态
    @GetMapping("/isLogin")
    public Result isLogin() {
        return Result.success("登录状态:" + StpUtil.isLogin());
    }
    
    // 检查登录,如果未登录则抛出异常
    @GetMapping("/checkLogin")
    public Result checkLogin() {
        StpUtil.checkLogin();
        return Result.success("当前会话已登录");
    }
    
    // 退出登录
    @PostMapping("/logout")
    public Result logout() {
        StpUtil.logout();
        return Result.success("退出登录成功");
    }
    
    // 踢人下线
    @PostMapping("/kickout")
    public Result kickout(@RequestParam Object userId) {
        StpUtil.kickout(userId);
        return Result.success("踢人下线成功");
    }
    
    // 顶人下线
    @PostMapping("/replaced")
    public Result replaced(@RequestParam Object userId) {
        StpUtil.replaced(userId);
        return Result.success("顶人下线成功");
    }
}
Token相关API
public class TokenController {
    
    // 获取当前Token值
    @GetMapping("/getToken")
    public Result getToken() {
        String token = StpUtil.getTokenValue();
        return Result.success("当前token:" + token);
    }
    
    // 获取当前Token信息
    @GetMapping("/getTokenInfo")
    public Result getTokenInfo() {
        SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
        return Result.success("token信息").put("tokenInfo", tokenInfo);
    }
    
    // 获取当前Token剩余有效期(单位:秒)
    @GetMapping("/getTokenTimeout")
    public Result getTokenTimeout() {
        long timeout = StpUtil.getTokenTimeout();
        return Result.success("token剩余有效期:" + timeout + "秒");
    }
    
    // 获取当前Token活跃剩余有效期(单位:秒)
    @GetMapping("/getTokenActivityTimeout")
    public Result getTokenActivityTimeout() {
        long timeout = StpUtil.getTokenActivityTimeout();
        return Result.success("token活跃剩余有效期:" + timeout + "秒");
    }
    
    // 续签Token(延长其有效期)
    @PostMapping("/renewTimeout")
    public Result renewTimeout(@RequestParam long timeout) {
        StpUtil.renewTimeout(timeout);
        return Result.success("续签成功,新的有效期:" + timeout + "秒");
    }
}
会话查询API
public class SessionController {
    
    // 获取当前登录用户id
    @GetMapping("/getLoginId")
    public Result getLoginId() {
        Object loginId = StpUtil.getLoginId();
        return Result.success("当前登录用户id:" + loginId);
    }
    
    // 获取当前登录用户id,如果未登录则返回默认值
    @GetMapping("/getLoginIdDefaultNull")
    public Result getLoginIdDefaultNull() {
        Object loginId = StpUtil.getLoginIdDefaultNull();
        return Result.success("当前登录用户id:" + loginId);
    }
    
    // 获取当前登录用户id,转为String类型
    @GetMapping("/getLoginIdAsString")
    public Result getLoginIdAsString() {
        String loginId = StpUtil.getLoginIdAsString();
        return Result.success("当前登录用户id:" + loginId);
    }
    
    // 获取当前登录用户id,转为int类型
    @GetMapping("/getLoginIdAsInt")
    public Result getLoginIdAsInt() {
        int loginId = StpUtil.getLoginIdAsInt();
        return Result.success("当前登录用户id:" + loginId);
    }
    
    // 获取当前登录用户id,转为long类型
    @GetMapping("/getLoginIdAsLong")
    public Result getLoginIdAsLong() {
        long loginId = StpUtil.getLoginIdAsLong();
        return Result.success("当前登录用户id:" + loginId);
    }
}

3.2 Session操作API

public class SessionOperateController {
    
    // 获取当前用户的Session对象
    @GetMapping("/getSession")
    public Result getSession() {
        SaSession session = StpUtil.getSession();
        return Result.success("session对象").put("sessionId", session.getId());
    }
    
    // 获取当前用户的Session对象,如果session尚未创建,是否新建并返回
    @GetMapping("/getSession2")
    public Result getSession2() {
        SaSession session = StpUtil.getSession(false); // false表示不新建
        return Result.success("session对象").put("session", session);
    }
    
    // 获取指定用户的Session对象
    @GetMapping("/getSessionByLoginId")
    public Result getSessionByLoginId(@RequestParam Object loginId) {
        SaSession session = StpUtil.getSessionByLoginId(loginId);
        return Result.success("指定用户的session对象").put("sessionId", session.getId());
    }
    
    // 获取指定Token对应的Session对象
    @GetMapping("/getSessionByToken")
    public Result getSessionByToken(@RequestParam String token) {
        SaSession session = StpUtil.getSessionByToken(token);
        return Result.success("指定Token的session对象").put("sessionId", session.getId());
    }
    
    // Session读写操作示例
    @PostMapping("/sessionOperation")
    public Result sessionOperation() {
        SaSession session = StpUtil.getSession();
        
        // 写值
        session.set("name", "张三");
        session.set("age", 25);
        
        // 读值
        String name = session.getString("name");
        int age = session.getInt("age");
        
        // 获取所有key
        Set<String> keys = session.keys();
        
        return Result.success("Session操作成功")
                .put("name", name)
                .put("age", age)
                .put("keys", keys);
    }
}

4. 登录认证机制

4.1 登录流程详解

Sa-Token的登录流程分为以下几个步骤:

  1. 用户提交登录信息
  2. 验证用户名密码
  3. 调用StpUtil.login(id)登录
  4. Sa-Token生成Token
  5. 将Token返回给前端
  6. 前端保存Token
  7. 后续请求携带Token
@RestController
@RequestMapping("/auth")
public class AuthController {
    
    @Autowired
    private UserService userService;
    
    // 完整的登录流程
    @PostMapping("/login")
    public Result login(@RequestBody LoginRequest loginRequest) {
        try {
            // 1. 参数校验
            if (StrUtil.isBlank(loginRequest.getUsername()) || 
                StrUtil.isBlank(loginRequest.getPassword())) {
                return Result.error("用户名或密码不能为空");
            }
            
            // 2. 根据用户名查询用户
            User user = userService.getUserByUsername(loginRequest.getUsername());
            if (user == null) {
                return Result.error("用户不存在");
            }
            
            // 3. 验证密码
            if (!passwordEncoder.matches(loginRequest.getPassword(), user.getPassword())) {
                return Result.error("密码错误");
            }
            
            // 4. 检查用户状态
            if (user.getStatus() == 0) {
                return Result.error("账户已被禁用");
            }
            
            // 5. 执行登录
            SaLoginModel loginModel = new SaLoginModel()
                    .setDevice(loginRequest.getDevice())          // 设备类型
                    .setTimeout(60 * 60 * 24 * 7)                // 7天有效期
                    .setIsLastingCookie(loginRequest.getRememberMe()); // 记住我
            
            StpUtil.login(user.getId(), loginModel);
            
            // 6. 保存用户信息到Session
            SaSession session = StpUtil.getSession();
            session.set("user", user);
            session.set("loginTime", new Date());
            session.set("loginIp", getClientIP());
            
            // 7. 构造返回信息
            Map<String, Object> tokenInfo = new HashMap<>();
            tokenInfo.put("token", StpUtil.getTokenValue());
            tokenInfo.put("tokenName", StpUtil.getTokenName());
            tokenInfo.put("tokenTimeout", StpUtil.getTokenTimeout());
            tokenInfo.put("sessionTimeout", StpUtil.getSessionTimeout());
            tokenInfo.put("tokenSessionTimeout", StpUtil.getTokenSessionTimeout());
            tokenInfo.put("tokenActivityTimeout", StpUtil.getTokenActivityTimeout());
            tokenInfo.put("loginDevice", StpUtil.getLoginDevice());
            
            return Result.success("登录成功").put("tokenInfo", tokenInfo);
            
        } catch (Exception e) {
            log.error("登录异常:", e);
            return Result.error("登录失败");
        }
    }
    
    // 登录请求对象
    public static class LoginRequest {
        private String username;
        private String password;
        private String device = "default";
        private Boolean rememberMe = false;
        
        // getter/setter...
    }
}

4.2 多端登录控制

@RestController
@RequestMapping("/device")
public class DeviceController {
    
    // PC端登录
    @PostMapping("/loginPC")
    public Result loginPC(@RequestBody LoginRequest request) {
        if (validateUser(request)) {
            StpUtil.login(request.getUserId(), "PC");
            return Result.success("PC登录成功");
        }
        return Result.error("登录失败");
    }
    
    // 移动端登录
    @PostMapping("/loginMobile")
    public Result loginMobile(@RequestBody LoginRequest request) {
        if (validateUser(request)) {
            StpUtil.login(request.getUserId(), "Mobile");
            return Result.success("移动端登录成功");
        }
        return Result.error("登录失败");
    }
    
    // 获取当前用户所有登录设备
    @GetMapping("/getTokenValueListByLoginId")
    public Result getTokenValueListByLoginId(@RequestParam Object loginId) {
        List<String> tokenList = StpUtil.getTokenValueListByLoginId(loginId);
        return Result.success("用户所有token").put("tokenList", tokenList);
    }
    
    // 踢掉指定用户在指定设备上的登录
    @PostMapping("/kickoutByLoginId")
    public Result kickoutByLoginId(@RequestParam Object loginId, @RequestParam String device) {
        StpUtil.kickoutByLoginId(loginId, device);
        return Result.success("踢掉用户在" + device + "设备上的登录");
    }
    
    // 切换到指定设备
    @PostMapping("/switchTo")
    public Result switchTo(@RequestParam String device) {
        StpUtil.switchTo(device);
        return Result.success("切换设备成功");
    }
}

4.3 Remember Me功能

@RestController
@RequestMapping("/remember")
public class RememberMeController {
    
    // 记住我登录
    @PostMapping("/login")
    public Result rememberMeLogin(@RequestBody RememberMeRequest request) {
        if (validateUser(request.getUsername(), request.getPassword())) {
            
            SaLoginModel loginModel = new SaLoginModel();
            
            if (request.getRememberMe()) {
                // 记住我:设置较长的有效期,并且设置为持久Cookie
                loginModel.setTimeout(60 * 60 * 24 * 30)    // 30天有效期
                         .setIsLastingCookie(true);          // 设置为持久Cookie
            } else {
                // 不记住我:使用默认配置
                loginModel.setTimeout(60 * 60 * 24)         // 1天有效期
                         .setIsLastingCookie(false);         // 临时Cookie
            }
            
            StpUtil.login(request.getUserId(), loginModel);
            
            return Result.success("登录成功")
                    .put("rememberMe", request.getRememberMe())
                    .put("timeout", StpUtil.getTokenTimeout());
        }
        
        return Result.error("用户名或密码错误");
    }
    
    // 记住我请求对象
    public static class RememberMeRequest {
        private String username;
        private String password;
        private Boolean rememberMe = false;
        private Object userId;
        
        // getter/setter...
    }
}

4.4 登录扩展参数

@RestController
@RequestMapping("/extend")
public class ExtendLoginController {
    
    // 扩展登录信息
    @PostMapping("/loginWithExtend")
    public Result loginWithExtend(@RequestBody ExtendLoginRequest request) {
        if (validateUser(request)) {
            
            // 创建登录模型,设置扩展参数
            SaLoginModel loginModel = new SaLoginModel()
                    .setDevice(request.getDevice())
                    .setTimeout(request.getTimeout())
                    .setToken(request.getCustomToken())         // 自定义Token值
                    .setIsLastingCookie(request.getRememberMe())
                    .setExtra("loginTime", System.currentTimeMillis())  // 扩展参数
                    .setExtra("loginIp", getClientIP())         // 扩展参数
                    .setExtra("userAgent", getUserAgent());     // 扩展参数
            
            StpUtil.login(request.getUserId(), loginModel);
            
            // 获取Token信息,包含扩展参数
            SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
            
            return Result.success("登录成功").put("tokenInfo", tokenInfo);
        }
        
        return Result.error("登录失败");
    }
    
    // 获取Token扩展参数
    @GetMapping("/getExtra")
    public Result getExtra() {
        SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
        
        Map<String, Object> extraData = new HashMap<>();
        extraData.put("loginTime", tokenInfo.getExtra("loginTime"));
        extraData.put("loginIp", tokenInfo.getExtra("loginIp"));
        extraData.put("userAgent", tokenInfo.getExtra("userAgent"));
        
        return Result.success("扩展参数").put("extraData", extraData);
    }
}

5. 权限验证详解

5.1 权限验证基础

Sa-Token中的权限验证需要实现StpInterface接口,定义权限和角色的获取逻辑。

@Component
public class StpInterfaceImpl implements StpInterface {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private RoleService roleService;
    
    @Autowired
    private PermissionService permissionService;
    
    /**
     * 返回一个账号所拥有的权限码集合
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        // 根据用户id查询权限列表
        List<String> permissions = permissionService.getPermissionsByUserId(loginId);
        
        // 这里可以添加一些通用权限
        permissions.add("common.read");
        
        return permissions;
    }
    
    /**
     * 返回一个账号所拥有的角色标识集合
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        // 根据用户id查询角色列表
        List<String> roles = roleService.getRolesByUserId(loginId);
        
        return roles;
    }
}

5.2 权限验证API

@RestController
@RequestMapping("/permission")
public class PermissionController {
    
    // 验证用户是否具有指定权限
    @GetMapping("/checkPermission")
    public Result checkPermission(@RequestParam String permission) {
        try {
            StpUtil.checkPermission(permission);
            return Result.success("具有权限:" + permission);
        } catch (NotPermissionException e) {
            return Result.error("权限不足:" + permission);
        }
    }
    
    // 验证用户是否具有指定权限(不抛异常)
    @GetMapping("/hasPermission")
    public Result hasPermission(@RequestParam String permission) {
        boolean hasPermission = StpUtil.hasPermission(permission);
        return Result.success("权限验证结果:" + hasPermission);
    }
    
    // 验证用户是否具有指定权限(任意一个即可)
    @GetMapping("/hasPermissionOr")
    public Result hasPermissionOr(@RequestParam String[] permissions) {
        boolean hasPermission = StpUtil.hasPermissionOr(permissions);
        return Result.success("权限验证结果(OR):" + hasPermission)
                .put("permissions", permissions);
    }
    
    // 验证用户是否具有指定权限(必须全部具有)
    @GetMapping("/hasPermissionAnd")
    public Result hasPermissionAnd(@RequestParam String[] permissions) {
        boolean hasPermission = StpUtil.hasPermissionAnd(permissions);
        return Result.success("权限验证结果(AND):" + hasPermission)
                .put("permissions", permissions);
    }
    
    // 验证用户是否具有指定角色
    @GetMapping("/checkRole")
    public Result checkRole(@RequestParam String role) {
        try {
            StpUtil.checkRole(role);
            return Result.success("具有角色:" + role);
        } catch (NotRoleException e) {
            return Result.error("角色不足:" + role);
        }
    }
    
    // 验证用户是否具有指定角色(不抛异常)
    @GetMapping("/hasRole")
    public Result hasRole(@RequestParam String role) {
        boolean hasRole = StpUtil.hasRole(role);
        return Result.success("角色验证结果:" + hasRole);
    }
    
    // 获取当前用户的权限列表
    @GetMapping("/getPermissionList")
    public Result getPermissionList() {
        List<String> permissions = StpUtil.getPermissionList();
        return Result.success("权限列表").put("permissions", permissions);
    }
    
    // 获取当前用户的角色列表
    @GetMapping("/getRoleList")
    public Result getRoleList() {
        List<String> roles = StpUtil.getRoleList();
        return Result.success("角色列表").put("roles", roles);
    }
}

5.3 权限通配符

Sa-Token支持权限通配符,可以实现更灵活的权限控制。

@RestController
@RequestMapping("/wildcard")
public class WildcardPermissionController {
    
    // 通配符权限示例
    @GetMapping("/testWildcard")
    public Result testWildcard() {
        Map<String, Object> result = new HashMap<>();
        
        // 假设用户具有权限:user.*
        // 以下验证都会通过
        result.put("user.add", StpUtil.hasPermission("user.add"));
        result.put("user.delete", StpUtil.hasPermission("user.delete"));
        result.put("user.update", StpUtil.hasPermission("user.update"));
        result.put("user.select", StpUtil.hasPermission("user.select"));
        
        // 假设用户具有权限:*.delete
        // 以下验证都会通过
        result.put("user.delete", StpUtil.hasPermission("user.delete"));
        result.put("role.delete", StpUtil.hasPermission("role.delete"));
        result.put("permission.delete", StpUtil.hasPermission("permission.delete"));
        
        // 假设用户具有权限:*
        // 所有权限验证都会通过(超级管理员)
        result.put("any.permission", StpUtil.hasPermission("any.permission"));
        
        return Result.success("通配符权限测试").put("results", result);
    }
}

5.4 自定义权限验证器

@Component
public class CustomPermissionValidator {
    
    /**
     * 自定义权限验证逻辑
     */
    public boolean validateCustomPermission(Object loginId, String resource, String action) {
        // 获取用户信息
        User user = userService.getUserById(loginId);
        if (user == null) {
            return false;
        }
        
        // 超级管理员拥有所有权限
        if ("admin".equals(user.getUsername())) {
            return true;
        }
        
        // 检查用户是否有访问特定资源的权限
        String permission = resource + ":" + action;
        List<String> userPermissions = StpUtil.getPermissionList();
        
        // 精确匹配
        if (userPermissions.contains(permission)) {
            return true;
        }
        
        // 通配符匹配
        for (String userPermission : userPermissions) {
            if (isMatch(userPermission, permission)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * 通配符匹配逻辑
     */
    private boolean isMatch(String pattern, String permission) {
        // 实现通配符匹配逻辑
        // 例如:user:* 匹配 user:add, user:delete 等
        if (pattern.endsWith("*")) {
            String prefix = pattern.substring(0, pattern.length() - 1);
            return permission.startsWith(prefix);
        }
        
        if (pattern.startsWith("*")) {
            String suffix = pattern.substring(1);
            return permission.endsWith(suffix);
        }
        
        return pattern.equals(permission);
    }
}

5.5 数据权限控制

@RestController
@RequestMapping("/data")
public class DataPermissionController {
    
    @Autowired
    private UserService userService;
    
    // 根据数据权限获取用户列表
    @GetMapping("/getUserList")
    public Result getUserList() {
        Object loginId = StpUtil.getLoginId();
        User currentUser = userService.getUserById(loginId);
        
        List<User> userList;
        
        // 根据用户角色决定可以查看的数据范围
        if (StpUtil.hasRole("admin")) {
            // 管理员:查看所有用户
            userList = userService.getAllUsers();
        } else if (StpUtil.hasRole("dept_manager")) {
            // 部门经理:查看本部门用户
            userList = userService.getUsersByDeptId(currentUser.getDeptId());
        } else if (StpUtil.hasRole("user")) {
            // 普通用户:只能查看自己
            userList = Arrays.asList(currentUser);
        } else {
            // 无权限
            return Result.error("无权限查看用户列表");
        }
        
        return Result.success("用户列表").put("userList", userList);
    }
    
    // 数据权限装饰器
    @GetMapping("/getOrderList")
    public Result getOrderList(@RequestParam(required = false) String status) {
        Object loginId = StpUtil.getLoginId();
        
        // 构建查询条件
        OrderQuery query = new OrderQuery();
        query.setStatus(status);
        
        // 根据权限添加数据过滤条件
        if (StpUtil.hasPermission("order:viewAll")) {
            // 有查看所有订单权限,不添加额外条件
        } else if (StpUtil.hasPermission("order:viewDept")) {
            // 只能查看本部门订单
            User currentUser = userService.getUserById(loginId);
            query.setDeptId(currentUser.getDeptId());
        } else if (StpUtil.hasPermission("order:viewSelf")) {
            // 只能查看自己的订单
            query.setUserId(loginId);
        } else {
            return Result.error("无权限查看订单");
        }
        
        List<Order> orderList = orderService.getOrderList(query);
        return Result.success("订单列表").put("orderList", orderList);
    }
}

6. Session会话管理

6.1 Session基本操作

Sa-Token的Session是一个功能强大的会话对象,可以存储任意类型的数据。

@RestController
@RequestMapping("/session")
public class SessionController {
    
    // Session基本读写操作
    @PostMapping("/basicOperation")
    public Result basicOperation() {
        SaSession session = StpUtil.getSession();
        
        // 写入数据
        session.set("name", "张三");
        session.set("age", 25);
        session.set("email", "[email protected]");
        session.set("loginTime", new Date());
        
        // 读取数据
        String name = session.getString("name");
        int age = session.getInt("age");
        String email = session.getString("email");
        Date loginTime = session.getModel("loginTime", Date.class);
        
        // 获取所有key
        Set<String> keys = session.keys();
        
        return Result.success("Session基本操作")
                .put("name", name)
                .put("age", age)
                .put("email", email)
                .put("loginTime", loginTime)
                .put("keys", keys);
    }
    
    // Session存储对象
    @PostMapping("/storeObject")
    public Result storeObject() {
        SaSession session = StpUtil.getSession();
        
        // 存储用户对象
        User user = new User();
        user.setId(1L);
        user.setUsername("admin");
        user.setNickname("管理员");
        user.setEmail("[email protected]");
        
        session.set("currentUser", user);
        
        // 存储列表
        List<String> hobbies = Arrays.asList("编程", "阅读", "游戏");
        session.set("hobbies", hobbies);
        
        // 存储Map
        Map<String, Object> settings = new HashMap<>();
        settings.put("theme", "dark");
        settings.put("language", "zh-CN");
        session.set("settings", settings);
        
        return Result.success("对象存储成功");
    }
    
    // Session读取对象
    @GetMapping("/getObject")
    public Result getObject() {
        SaSession session = StpUtil.getSession();
        
        // 读取用户对象
        User user = session.getModel("currentUser", User.class);
        
        // 读取列表
        List<String> hobbies = session.getModel("hobbies", List.class);
        
        // 读取Map
        Map<String, Object> settings = session.getModel("settings", Map.class);
        
        return Result.success("对象读取成功")
                .put("user", user)
                .put("hobbies", hobbies)
                .put("settings", settings);
    }
    
    // Session删除操作
    @DeleteMapping("/delete")
    public Result delete(@RequestParam String key) {
        SaSession session = StpUtil.getSession();
        
        // 删除指定key
        session.delete(key);
        
        return Result.success("删除成功:" + key);
    }
    
    // 清空Session
    @DeleteMapping("/clear")
    public Result clear() {
        SaSession session = StpUtil.getSession();
        session.clear();
        
        return Result.success("Session清空成功");
    }
}

6.2 Session超时管理

@RestController
@RequestMapping("/session/timeout")
public class SessionTimeoutController {
    
    // 获取Session剩余有效期
    @GetMapping("/getTimeout")
    public Result getTimeout() {
        SaSession session = StpUtil.getSession();
        long timeout = session.getTimeout();
        
        return Result.success("Session剩余有效期:" + timeout + "秒");
    }
    
    // 修改Session有效期
    @PostMapping("/setTimeout")
    public Result setTimeout(@RequestParam long timeout) {
        SaSession session = StpUtil.getSession();
        session.updateTimeout(timeout);
        
        return Result.success("Session有效期已设置为:" + timeout + "秒");
    }
    
    // 获取Session最后活跃时间
    @GetMapping("/getLastActiveTime")
    public Result getLastActiveTime() {
        SaSession session = StpUtil.getSession();
        
        return Result.success("Session最后活跃时间")
                .put("createTime", new Date(session.getCreateTime()))
                .put("timeout", session.getTimeout());
    }
}

6.3 自定义Session

@RestController
@RequestMapping("/session/custom")
public class CustomSessionController {
    
    // 创建自定义Session
    @PostMapping("/createCustomSession")
    public Result createCustomSession(@RequestParam String sessionId) {
        // 创建自定义Session
        SaSession session = SaSessionCustomUtil.getSessionById(sessionId);
        
        // 存储数据
        session.set("type", "custom");
        session.set("createTime", new Date());
        session.set("creator", StpUtil.getLoginIdDefaultNull());
        
        return Result.success("自定义Session创建成功")
                .put("sessionId", sessionId);
    }
    
    // 获取自定义Session
    @GetMapping("/getCustomSession")
    public Result getCustomSession(@RequestParam String sessionId) {
        SaSession session = SaSessionCustomUtil.getSessionById(sessionId, false);
        
        if (session == null) {
            return Result.error("Session不存在:" + sessionId);
        }
        
        Map<String, Object> data = new HashMap<>();
        for (String key : session.keys()) {
            data.put(key, session.get(key));
        }
        
        return Result.success("自定义Session数据").put("data", data);
    }
    
    // 删除自定义Session
    @DeleteMapping("/deleteCustomSession")
    public Result deleteCustomSession(@RequestParam String sessionId) {
        SaSessionCustomUtil.deleteSessionById(sessionId);
        return Result.success("自定义Session删除成功:" + sessionId);
    }
}

6.4 Session监听器

@Component
public class SessionListener implements SaTokenListener {
    
    private static final Logger log = LoggerFactory.getLogger(SessionListener.class);
    
    /** 每次登录时触发 */
    @Override
    public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
        log.info("用户登录:loginType={}, loginId={}, tokenValue={}", loginType, loginId, tokenValue);
        
        // 记录登录日志
        recordLoginLog(loginId, tokenValue, "LOGIN");
    }
    
    /** 每次注销时触发 */
    @Override
    public void doLogout(String loginType, Object loginId, String tokenValue) {
        log.info("用户注销:loginType={}, loginId={}, tokenValue={}", loginType, loginId, tokenValue);
        
        // 记录注销日志
        recordLoginLog(loginId, tokenValue, "LOGOUT");
    }
    
    /** 每次被踢下线时触发 */
    @Override
    public void doKickout(String loginType, Object loginId, String tokenValue) {
        log.info("用户被踢下线:loginType={}, loginId={}, tokenValue={}", loginType, loginId, tokenValue);
        
        // 记录踢出日志
        recordLoginLog(loginId, tokenValue, "KICKOUT");
    }
    
    /** 每次被顶下线时触发 */
    @Override
    public void doReplaced(String loginType, Object loginId, String tokenValue) {
        log.info("用户被顶下线:loginType={}, loginId={}, tokenValue={}", loginType, loginId, tokenValue);
        
        // 记录顶替日志
        recordLoginLog(loginId, tokenValue, "REPLACED");
    }
    
    /** 每次被禁用时触发 */
    @Override
    public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {
        log.info("用户被禁用:loginType={}, loginId={}, service={}, level={}, disableTime={}", 
                loginType, loginId, service, level, disableTime);
    }
    
    /** 每次被解封时触发 */
    @Override
    public void doUntieDisable(String loginType, Object loginId, String service) {
        log.info("用户被解封:loginType={}, loginId={}, service={}", loginType, loginId, service);
    }
    
    /** 每次打开二级认证时触发 */
    @Override
    public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {
        log.info("二级认证开启:loginType={}, tokenValue={}, service={}, safeTime={}", 
                loginType, tokenValue, service, safeTime);
    }
    
    /** 每次关闭二级认证时触发 */
    @Override
    public void doCloseSafe(String loginType, String tokenValue, String service) {
        log.info("二级认证关闭:loginType={}, tokenValue={}, service={}", loginType, tokenValue, service);
    }
    
    /** 每次创建Session时触发 */
    @Override
    public void doCreateSession(String id) {
        log.info("Session创建:id={}", id);
    }
    
    /** 每次注销Session时触发 */
    @Override
    public void doLogoutSession(String id) {
        log.info("Session注销:id={}", id);
    }
    
    /** 每次Token续期时触发 */
    @Override
    public void doRenewTimeout(String tokenValue, Object loginId, long timeout) {
        log.info("Token续期:tokenValue={}, loginId={}, timeout={}", tokenValue, loginId, timeout);
    }
    
    private void recordLoginLog(Object loginId, String tokenValue, String action) {
        // 实现登录日志记录逻辑
        // 可以保存到数据库或日志文件
    }
}

7. 注解鉴权

Sa-Token提供了丰富的注解来实现权限控制,可以在方法上直接使用注解进行鉴权。

7.1 基础鉴权注解

@RestController
@RequestMapping("/admin")
public class AdminController {
    
    // 登录验证:只有登录后才能访问
    @SaCheckLogin
    @GetMapping("/info")
    public Result getAdminInfo() {
        return Result.success("管理员信息")
                .put("adminId", StpUtil.getLoginId())
                .put("adminName", "超级管理员");
    }
    
    // 角色验证:必须具有admin角色
    @SaCheckRole("admin")
    @GetMapping("/userList")
    public Result getUserList() {
        return Result.success("用户列表")
                .put("users", getUserListFromDB());
    }
    
    // 权限验证:必须具有user:add权限
    @SaCheckPermission("user:add")
    @PostMapping("/addUser")
    public Result addUser(@RequestBody User user) {
        // 添加用户逻辑
        return Result.success("用户添加成功");
    }
    
    // 权限验证:必须具有user:delete权限
    @SaCheckPermission("user:delete")
    @DeleteMapping("/deleteUser/{userId}")
    public Result deleteUser(@PathVariable Long userId) {
        // 删除用户逻辑
        return Result.success("用户删除成功");
    }
    
    // 权限验证:必须具有user:update权限
    @SaCheckPermission("user:update")
    @PutMapping("/updateUser")
    public Result updateUser(@RequestBody User user) {
        // 更新用户逻辑
        return Result.success("用户更新成功");
    }
    
    // 权限验证:必须具有user:select权限
    @SaCheckPermission("user:select")
    @GetMapping("/getUser/{userId}")
    public Result getUser(@PathVariable Long userId) {
        // 查询用户逻辑
        return Result.success("用户信息").put("user", getUserFromDB(userId));
    }
}

7.2 复合条件注解

@RestCont
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值