当前位置: 首页 > news >正文

【JAVA 进阶】SpringBoot集成Sa-Token权限校验框架深度解析

文章目录

    • 引言
    • 第一章:SA-Token框架概述与核心特性
      • 1.1 SA-Token简介与设计理念
        • 1.1.1 什么是SA-Token
        • 1.1.2 SA-Token的设计理念
      • 1.2 SA-Token核心特性详解
        • 1.2.1 登录认证特性
        • 1.2.2 权限认证特性
        • 1.2.3 会话管理特性
      • 1.3 SA-Token架构设计
        • 1.3.1 核心组件架构
        • 1.3.2 扩展机制设计
    • 第二章:SpringBoot集成SA-Token基础配置
      • 2.1 项目环境搭建
        • 2.1.1 Maven依赖配置
        • 2.1.2 应用配置文件
        • 2.1.3 数据库表结构设计
      • 2.2 核心配置类实现
        • 2.2.1 SA-Token配置类
        • 2.2.2 权限认证接口实现
        • 2.2.3 全局异常处理器
      • 2.3 实体类和数据访问层
        • 2.3.1 实体类定义
        • 2.3.2 数据访问层实现
      • 2.4 业务服务层实现
        • 2.4.1 用户服务实现
        • 2.4.2 角色服务实现
        • 2.4.3 权限服务实现
    • 第三章:权限认证与授权实战
      • 3.1 登录认证实现
        • 3.1.1 登录控制器实现
        • 3.1.2 验证码服务实现
        • 4.1.2 SSO-Server端实现
        • 4.1.3 SSO-Client端实现
      • 4.2 OAuth2.0集成
        • 4.2.1 OAuth2配置
      • 4.3 微服务网关鉴权
        • 4.3.1 网关鉴权配置
      • 4.4 多账户体系支持
        • 4.4.1 多账户配置
    • 第五章:生产环境最佳实践
      • 5.1 性能优化策略
        • 5.1.1 Token存储优化
        • 5.1.2 权限缓存优化
      • 5.2 安全加固措施
        • 5.2.1 Token安全增强
        • 5.2.2 防攻击策略
      • 5.3 监控与日志
        • 5.3.1 认证监控
        • 5.3.2 审计日志
    • 第六章:总结与展望
      • 6.1 知识点回顾
        • 6.1.1 核心概念与特性
        • 6.1.2 技术架构要点
        • 6.1.3 实战应用总结
      • 6.2 最佳实践总结
        • 6.2.1 开发规范
        • 6.2.2 性能优化建议
        • 6.2.3 安全防护要点
      • 6.3 技术发展趋势
        • 6.3.1 云原生权限管理
        • 6.3.2 零信任安全架构
        • 6.3.3 AI驱动的智能权限
      • 6.4 扩展阅读与学习资源
        • 6.4.1 官方文档与社区
        • 6.4.2 相关技术书籍
        • 6.4.3 在线学习资源
      • 6.5 实践练习与思考
        • 6.5.1 动手实践项目
        • 6.5.2 思考讨论题
        • 6.5.3 开源贡献
      • 6.6 结语

在这里插入图片描述

引言

在现代Web应用开发中,权限管理是一个不可或缺的核心功能。传统的权限框架如Spring Security虽然功能强大,但配置复杂、学习成本高,对于中小型项目来说往往显得过于臃肿。SA-Token作为一个轻量级的Java权限认证框架,以其简洁的API设计、丰富的功能特性和极低的学习成本,正在成为越来越多开发者的首选。

SA-Token(Simple And Token)是一个轻量级Java权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权等一系列权限相关问题。它以简单、强大、优雅为设计理念,让权限认证变得简单而不失灵活。

本文将从SA-Token的基础概念出发,深入探讨其在SpringBoot项目中的集成方案,通过丰富的代码示例和实战案例,帮助读者全面掌握SA-Token的使用技巧和最佳实践。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的技术洞察和实践指导。

第一章:SA-Token框架概述与核心特性

在这里插入图片描述

1.1 SA-Token简介与设计理念

1.1.1 什么是SA-Token

SA-Token是一个轻量级Java权限认证框架,专注于解决Web应用中的权限认证问题。与传统的重量级框架不同,SA-Token采用了更加简洁直观的API设计,让开发者能够快速上手并高效开发。

// SA-Token的核心理念:简单即是美
// 登录用户
StpUtil.login(userId);// 检查登录状态
StpUtil.checkLogin();// 获取当前用户ID
Object userId = StpUtil.getLoginId();// 注销登录
StpUtil.logout();
1.1.2 SA-Token的设计理念

SA-Token的设计遵循以下核心理念:

  1. 简单性:API设计简洁明了,学习成本低
  2. 灵活性:支持多种认证模式和扩展机制
  3. 高性能:轻量级设计,运行效率高
  4. 易集成:与主流框架无缝集成
/*** SA-Token设计理念体现*/
@RestController
public class AuthController {/*** 用户登录 - 体现简单性*/@PostMapping("/login")public Result login(@RequestBody LoginRequest request) {// 验证用户名密码(省略具体实现)User user = userService.authenticate(request.getUsername(), request.getPassword());if (user != null) {// 一行代码完成登录StpUtil.login(user.getId());// 获取Token信息SaTokenInfo tokenInfo = StpUtil.getTokenInfo();return Result.success(tokenInfo);}return Result.error("用户名或密码错误");}/*** 获取用户信息 - 体现灵活性*/@GetMapping("/userinfo")public Result getUserInfo() {// 检查登录状态,未登录会抛出异常StpUtil.checkLogin();// 获取当前登录用户IDObject loginId = StpUtil.getLoginId();// 获取用户详细信息User user = userService.getById(loginId);return Result.success(user);}/*** 需要特定权限的接口 - 体现权限控制的简洁性*/@GetMapping("/admin/users")@SaCheckPermission("user:list") // 注解方式权限校验public Result getUserList() {List<User> users = userService.getAllUsers();return Result.success(users);}
}

1.2 SA-Token核心特性详解

在这里插入图片描述

1.2.1 登录认证特性

SA-Token提供了完整的登录认证解决方案,支持多种登录模式和会话管理策略。

/*** 登录认证核心特性演示*/
@Service
public class AuthService {/*** 基础登录功能*/public void basicLogin(Long userId) {// 基础登录StpUtil.login(userId);// 指定设备登录StpUtil.login(userId, "PC");// 登录并指定Token有效期(单位:秒)StpUtil.login(userId, 3600);// 登录时携带扩展信息StpUtil.login(userId, new SaLoginModel().setDevice("mobile").setTimeout(7200).setIsLastingCookie(true));}/*** 会话查询功能*/public void sessionQuery() {// 获取当前登录用户IDObject loginId = StpUtil.getLoginId();// 获取当前登录用户ID,并转换为指定类型Long userId = StpUtil.getLoginIdAsLong();String userIdStr = StpUtil.getLoginIdAsString();// 获取当前登录设备String device = StpUtil.getLoginDevice();// 获取Token剩余有效时间(单位:秒)long timeout = StpUtil.getTokenTimeout();// 获取Token信息SaTokenInfo tokenInfo = StpUtil.getTokenInfo();System.out.println("Token名称:" + tokenInfo.getTokenName());System.out.println("Token值:" + tokenInfo.getTokenValue());System.out.println("是否登录:" + tokenInfo.getIsLogin());System.out.println("登录ID:" + tokenInfo.getLoginId());System.out.println("登录类型:" + tokenInfo.getLoginType());System.out.println("Token超时时间:" + tokenInfo.getTokenTimeout());}/*** 登录状态检查*/public void loginCheck() {// 检查当前是否登录,如未登录则抛出异常StpUtil.checkLogin();// 检查当前是否登录,返回boolean值boolean isLogin = StpUtil.isLogin();// 检查指定用户是否登录boolean userLogin = StpUtil.isLogin(10001);// 检查当前Token是否有效boolean isValid = StpUtil.getTokenInfo().getIsLogin();}/*** 注销登录功能*/public void logout() {// 注销当前用户登录StpUtil.logout();// 注销指定用户登录StpUtil.logout(10001);// 注销指定用户在指定设备的登录StpUtil.logout(10001, "PC");// 踢掉指定用户下线StpUtil.kickout(10001);// 踢掉指定用户在指定设备下线StpUtil.kickout(10001, "mobile");}
}
1.2.2 权限认证特性

SA-Token提供了灵活的权限认证机制,支持基于角色和权限的访问控制。

/*** 权限认证特性演示*/
@Service
public class PermissionService {/*** 权限校验*/public void permissionCheck() {// 检查当前用户是否拥有指定权限StpUtil.checkPermission("user:add");// 检查当前用户是否拥有指定权限,返回booleanboolean hasPermission = StpUtil.hasPermission("user:delete");// 检查当前用户是否拥有指定权限列表中的任意一个StpUtil.checkPermissionOr("user:add", "user:edit", "user:delete");// 检查当前用户是否拥有指定权限列表中的所有权限StpUtil.checkPermissionAnd("user:add", "role:add");}/*** 角色校验*/public void roleCheck() {// 检查当前用户是否拥有指定角色StpUtil.checkRole("admin");// 检查当前用户是否拥有指定角色,返回booleanboolean hasRole = StpUtil.hasRole("admin");// 检查当前用户是否拥有指定角色列表中的任意一个StpUtil.checkRoleOr("admin", "manager", "operator");// 检查当前用户是否拥有指定角色列表中的所有角色StpUtil.checkRoleAnd("admin", "manager");}/*** 获取权限和角色信息*/public void getPermissionInfo() {// 获取当前用户的权限列表List<String> permissions = StpUtil.getPermissionList();// 获取当前用户的角色列表List<String> roles = StpUtil.getRoleList();// 获取指定用户的权限列表List<String> userPermissions = StpUtil.getPermissionList(10001);// 获取指定用户的角色列表List<String> userRoles = StpUtil.getRoleList(10001);System.out.println("当前用户权限:" + permissions);System.out.println("当前用户角色:" + roles);}
}
1.2.3 会话管理特性

SA-Token提供了强大的会话管理功能,支持会话存储、会话共享、会话监听等特性。

/*** 会话管理特性演示*/
@Service
public class SessionService {/*** Session存储操作*/public void sessionStorage() {// 获取当前用户的Session对象SaSession session = StpUtil.getSession();// 在Session中存储数据session.set("username", "张三");session.set("email", "zhangsan@example.com");session.set("loginTime", System.currentTimeMillis());// 从Session中获取数据String username = session.get("username", String.class);String email = (String) session.get("email");// 获取Session中的所有keySet<String> keys = session.keys();// 删除Session中的指定数据session.delete("email");// 清空Sessionsession.clear();// 获取Session的剩余存活时间long timeout = session.getTimeout();// 修改Session的存活时间session.updateTimeout(3600);}/*** Token-Session双Token模式*/public void tokenSessionMode() {// 获取Token-Session(专门存储业务数据的Session)SaSession tokenSession = StpUtil.getTokenSession();// 在Token-Session中存储数据tokenSession.set("currentProject", "SA-Token集成项目");tokenSession.set("theme", "dark");// Token-Session与User-Session的区别:// User-Session: 以用户为单位,同一用户的多次登录共享同一个Session// Token-Session: 以Token为单位,每个Token都有自己独立的Session// 获取User-SessionSaSession userSession = StpUtil.getSession();userSession.set("userInfo", "这是用户级别的数据");// 获取指定用户的SessionSaSession specificUserSession = StpUtil.getSessionByLoginId(10001);specificUserSession.set("lastLoginTime", System.currentTimeMillis());}/*** 自定义Session操作*/public void customSession() {// 获取自定义SessionSaSession customSession = SaSessionCustomUtil.getSessionById("custom-session-001");// 在自定义Session中存储数据customSession.set("customData", "这是自定义Session数据");// 设置自定义Session的存活时间customSession.updateTimeout(1800);// 删除自定义SessionSaSessionCustomUtil.deleteSessionById("custom-session-001");// 获取所有自定义Session的ID列表List<String> sessionIds = SaSessionCustomUtil.searchSessionId("custom-*", 0, 100, true);}
}

1.3 SA-Token架构设计

1.3.1 核心组件架构

SA-Token采用模块化设计,核心组件包括:

/*** SA-Token核心组件架构演示*/
public class SaTokenArchitecture {/*** 1. StpLogic - 权限认证逻辑核心*/public void stpLogicDemo() {// StpLogic是SA-Token的核心逻辑类// 所有的登录、权限校验等操作都通过StpLogic实现// 获取默认的StpLogic实例StpLogic stpLogic = StpUtil.stpLogic;// 使用StpLogic进行登录stpLogic.login(10001);// 使用StpLogic进行权限校验stpLogic.checkPermission("user:add");// 自定义StpLogic实现多账户体系StpLogic adminLogic = new StpLogic("admin");StpLogic userLogic = new StpLogic("user");// 管理员登录adminLogic.login(1001);// 普通用户登录userLogic.login(2001);}/*** 2. SaTokenDao - 数据持久化接口*/public void saTokenDaoDemo() {// SaTokenDao负责Token和Session的持久化// 默认实现:SaTokenDaoDefaultImpl(基于内存)// 获取当前使用的Dao实例SaTokenDao dao = SaManager.getSaTokenDao();// 存储Tokendao.set("token:abc123", "user:10001", 3600);// 获取Token对应的值String value = dao.get("token:abc123");// 删除Tokendao.delete("token:abc123");// 获取Token剩余存活时间long timeout = dao.getTimeout("token:abc123");// 修改Token存活时间dao.updateTimeout("token:abc123", 7200);}/*** 3. SaTokenConfig - 全局配置类*/public void saTokenConfigDemo() {// 获取全局配置对象SaTokenConfig config = SaManager.getConfig();// 查看配置信息System.out.println("Token名称:" + config.getTokenName());System.out.println("Token超时时间:" + config.getTimeout());System.out.println("是否允许同一账号并发登录:" + config.getIsConcurrent());System.out.println("是否共享Token:" + config.getIsShare());System.out.println("Token风格:" + config.getTokenStyle());// 动态修改配置(不推荐在生产环境使用)config.setTokenName("Authorization");config.setTimeout(7200);}/*** 4. SaStrategy - 策略模式接口*/public void saStrategyDemo() {// SA-Token使用策略模式来处理各种业务逻辑// 自定义Token生成策略SaStrategy.me.createToken = (loginId, loginType) -> {return "custom-token-" + loginId + "-" + System.currentTimeMillis();};// 自定义Session生成策略SaStrategy.me.createSession = (sessionId) -> {return new SaSession(sessionId);};// 自定义权限验证失败处理策略SaStrategy.me.notPermission = (loginType, permission) -> {throw new SaTokenException("权限不足:" + permission);};// 自定义角色验证失败处理策略SaStrategy.me.notRole = (loginType, role) -> {throw new SaTokenException("角色不足:" + role);};}
}
1.3.2 扩展机制设计

SA-Token提供了丰富的扩展机制,支持自定义实现各种组件:

/*** SA-Token扩展机制演示*/
public class SaTokenExtension {/*** 自定义权限数据源*/@Componentpublic class CustomStpInterface implements StpInterface {@Autowiredprivate UserService userService;@Autowiredprivate RoleService roleService;@Autowiredprivate PermissionService permissionService;/*** 返回指定用户的权限列表*/@Overridepublic List<String> getPermissionList(Object loginId, String loginType) {Long userId = Long.valueOf(loginId.toString());// 从数据库查询用户权限List<Permission> permissions = permissionService.getPermissionsByUserId(userId);return permissions.stream().map(Permission::getPermissionCode).collect(Collectors.toList());}/*** 返回指定用户的角色列表*/@Overridepublic List<String> getRoleList(Object loginId, String loginType) {Long userId = Long.valueOf(loginId.toString());// 从数据库查询用户角色List<Role> roles = roleService.getRolesByUserId(userId);return roles.stream().map(Role::getRoleCode).collect(Collectors.toList());}}/*** 自定义Token持久化实现*/@Componentpublic class CustomSaTokenDao implements SaTokenDao {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Overridepublic String get(String key) {return (String) redisTemplate.opsForValue().get(key);}@Overridepublic void set(String key, String value, long timeout) {if (timeout == SaTokenDao.NEVER_EXPIRE) {redisTemplate.opsForValue().set(key, value);} else {redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);}}@Overridepublic void update(String key, String value) {long expire = getTimeout(key);if (expire == SaTokenDao.NOT_VALUE_EXPIRE) {return;}this.set(key, value, expire);}@Overridepublic void delete(String key) {redisTemplate.delete(key);}@Overridepublic long getTimeout(String key) {Long expire = redisTemplate.getExpire(key);return expire == null ? SaTokenDao.NOT_VALUE_EXPIRE : expire;}@Overridepublic void updateTimeout(String key, long timeout) {redisTemplate.expire(key, timeout, TimeUnit.SECONDS);}@Overridepublic Object getObject(String key) {return redisTemplate.opsForValue().get(key);}@Overridepublic void setObject(String key, Object object, long timeout) {if (timeout == SaTokenDao.NEVER_EXPIRE) {redisTemplate.opsForValue().set(key, object);} else {redisTemplate.opsForValue().set(key, object, timeout, TimeUnit.SECONDS);}}@Overridepublic void updateObject(String key, Object object) {long expire = getObjectTimeout(key);if (expire == SaTokenDao.NOT_VALUE_EXPIRE) {return;}this.setObject(key, object, expire);}@Overridepublic long getObjectTimeout(String key) {Long expire = redisTemplate.getExpire(key);return expire == null ? SaTokenDao.NOT_VALUE_EXPIRE : expire;}@Overridepublic void updateObjectTimeout(String key, long timeout) {redisTemplate.expire(key, timeout, TimeUnit.SECONDS);}@Overridepublic List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {// 实现数据搜索逻辑Set<String> keys = redisTemplate.keys(prefix + "*" + keyword + "*");List<String> list = new ArrayList<>(keys);// 排序if (sortType) {Collections.sort(list);} else {Collections.sort(list, Collections.reverseOrder());}// 分页int fromIndex = start;int toIndex = Math.min(start + size, list.size());if (fromIndex >= list.size()) {return new ArrayList<>();}return list.subList(fromIndex, toIndex);}}
}

第二章:SpringBoot集成SA-Token基础配置

在这里插入图片描述

2.1 项目环境搭建

2.1.1 Maven依赖配置

首先,我们需要在SpringBoot项目中添加SA-Token的相关依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.0</version><relativePath/></parent><groupId>com.example</groupId><artifactId>satoken-demo</artifactId><version>1.0.0</version><name>SA-Token集成示例</name><description>SpringBoot集成SA-Token权限校验框架示例项目</description><properties><java.version>8</java.version><sa-token.version>1.34.0</sa-token.version></properties><dependencies><!-- SpringBoot Web启动器 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- SA-Token 权限认证,在线文档:https://sa-token.cc --><dependency><groupId>cn.dev33</groupId><artifactId>sa-token-spring-boot-starter</artifactId><version>${sa-token.version}</version></dependency><!-- SA-Token 整合 Redis (使用 jackson 序列化方式) --><dependency><groupId>cn.dev33</groupId><artifactId>sa-token-dao-redis-jackson</artifactId><version>${sa-token.version}</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-data-jpa</artifactId></dependency><!-- MySQL数据库驱动 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!-- JSON处理 --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.83</version></dependency><!-- Lombok简化代码 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!-- SpringBoot测试启动器 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build>
</project>
2.1.2 应用配置文件

application.yml中配置SA-Token和相关组件:

# 服务器配置
server:port: 8080servlet:context-path: /api# 数据源配置
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/satoken_demo?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8username: rootpassword: 123456# JPA配置jpa:hibernate:ddl-auto: updateshow-sql: trueproperties:hibernate:format_sql: true# Redis配置redis:host: localhostport: 6379password: database: 0timeout: 10000mslettuce:pool:max-active: 8max-wait: -1msmax-idle: 8min-idle: 0# SA-Token配置
sa-token:# token名称 (同时也是cookie名称)token-name: Authorization# 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# 是否从cookie中读取tokenis-read-cookie: true# 是否从header中读取tokenis-read-header: true# 是否从body中读取tokenis-read-body: false# token前缀token-prefix: "Bearer"# jwt秘钥jwt-secret-key: abcdefghijklmnopqrstuvwxyz# 日志配置
logging:level:com.example: debugorg.springframework.web: debugpattern:console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
2.1.3 数据库表结构设计

创建用户权限相关的数据库表:

-- 用户表
CREATE TABLE `sys_user` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',`username` varchar(50) NOT NULL COMMENT '用户名',`password` varchar(100) NOT NULL COMMENT '密码',`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',`email` varchar(100) DEFAULT NULL COMMENT '邮箱',`phone` varchar(20) DEFAULT NULL COMMENT '手机号',`avatar` varchar(200) DEFAULT NULL COMMENT '头像',`status` tinyint DEFAULT '1' COMMENT '状态:0-禁用,1-启用',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';-- 角色表
CREATE TABLE `sys_role` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',`role_code` varchar(50) NOT NULL COMMENT '角色编码',`role_name` varchar(50) NOT NULL COMMENT '角色名称',`description` varchar(200) DEFAULT NULL COMMENT '角色描述',`status` tinyint DEFAULT '1' COMMENT '状态:0-禁用,1-启用',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_role_code` (`role_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';-- 权限表
CREATE TABLE `sys_permission` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '权限ID',`permission_code` varchar(100) NOT NULL COMMENT '权限编码',`permission_name` varchar(100) NOT NULL COMMENT '权限名称',`resource_type` varchar(20) DEFAULT NULL COMMENT '资源类型:menu-菜单,button-按钮',`url` varchar(200) DEFAULT NULL COMMENT '资源路径',`method` varchar(10) DEFAULT NULL COMMENT '请求方法',`parent_id` bigint DEFAULT '0' COMMENT '父权限ID',`sort_order` int DEFAULT '0' COMMENT '排序',`status` tinyint DEFAULT '1' COMMENT '状态:0-禁用,1-启用',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_permission_code` (`permission_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限表';-- 用户角色关联表
CREATE TABLE `sys_user_role` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',`user_id` bigint NOT NULL COMMENT '用户ID',`role_id` bigint NOT NULL COMMENT '角色ID',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_user_role` (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';-- 角色权限关联表
CREATE TABLE `sys_role_permission` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',`role_id` bigint NOT NULL COMMENT '角色ID',`permission_id` bigint NOT NULL COMMENT '权限ID',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_role_permission` (`role_id`,`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色权限关联表';-- 插入测试数据
INSERT INTO `sys_user` (`username`, `password`, `nickname`, `email`, `status`) VALUES
('admin', '$2a$10$7JB720yubVSOfvVMe6/YqO4wkhWGEn4bJJnNpSn0kfzOLuTOQHHiq', '系统管理员', 'admin@example.com', 1),
('user', '$2a$10$7JB720yubVSOfvVMe6/YqO4wkhWGEn4bJJnNpSn0kfzOLuTOQHHiq', '普通用户', 'user@example.com', 1);INSERT INTO `sys_role` (`role_code`, `role_name`, `description`, `status`) VALUES
('admin', '系统管理员', '拥有系统所有权限', 1),
('user', '普通用户', '拥有基础权限', 1);INSERT INTO `sys_permission` (`permission_code`, `permission_name`, `resource_type`, `url`, `method`) VALUES
('system:user:list', '用户列表', 'menu', '/system/user/list', 'GET'),
('system:user:add', '添加用户', 'button', '/system/user/add', 'POST'),
('system:user:edit', '编辑用户', 'button', '/system/user/edit', 'PUT'),
('system:user:delete', '删除用户', 'button', '/system/user/delete', 'DELETE'),
('system:role:list', '角色列表', 'menu', '/system/role/list', 'GET'),
('system:role:add', '添加角色', 'button', '/system/role/add', 'POST');INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES
(1, 1),
(2, 2);INSERT INTO `sys_role_permission` (`role_id`, `permission_id`) VALUES
(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6),
(2, 1), (2, 5);

2.2 核心配置类实现

2.2.1 SA-Token配置类
/*** SA-Token配置类*/
@Configuration
@EnableConfigurationProperties
public class SaTokenConfig {/*** 获取StpInterface权限认证接口的实现类*/@Beanpublic StpInterface stpInterface() {return new StpInterfaceImpl();}/*** SA-Token全局异常处理*/@Beanpublic GlobalExceptionHandler globalExceptionHandler() {return new GlobalExceptionHandler();}/*** SA-Token拦截器配置*/@Configurationpublic static class SaTokenInterceptorConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注册Sa-Token拦截器,校验规则为StpUtil.checkLogin()登录校验registry.addInterceptor(new SaInterceptor(handle -> {// 指定一条match规则SaRouter.match("/**")    // 拦截所有路由.notMatch("/auth/login")        // 排除登录接口.notMatch("/auth/register")     // 排除注册接口.notMatch("/auth/captcha")      // 排除验证码接口.notMatch("/doc.html")          // 排除swagger文档.notMatch("/swagger-ui/**")     // 排除swagger资源.notMatch("/swagger-resources/**") // 排除swagger资源.notMatch("/v2/api-docs")       // 排除swagger接口.notMatch("/v3/api-docs")       // 排除swagger接口.notMatch("/webjars/**")        // 排除swagger资源.notMatch("/favicon.ico")       // 排除网站图标.notMatch("/actuator/**")       // 排除监控端点.check(r -> StpUtil.checkLogin()); // 登录校验})).addPathPatterns("/**");}}/*** 自定义JSON序列化方式*/@Bean@Primarypublic SaJsonTemplate saJsonTemplate() {return new SaJsonTemplateForFastjson();}
}
2.2.2 权限认证接口实现
/*** 自定义权限验证接口扩展*/
@Component
public class StpInterfaceImpl implements StpInterface {@Autowiredprivate UserService userService;@Autowiredprivate RoleService roleService;@Autowiredprivate PermissionService permissionService;/*** 返回一个账号所拥有的权限码集合*/@Overridepublic List<String> getPermissionList(Object loginId, String loginType) {try {Long userId = Long.valueOf(loginId.toString());// 查询用户权限列表List<String> permissions = permissionService.getPermissionsByUserId(userId);log.debug("用户[{}]拥有权限: {}", userId, permissions);return permissions;} catch (Exception e) {log.error("获取用户权限列表失败, loginId: {}", loginId, e);return Collections.emptyList();}}/*** 返回一个账号所拥有的角色标识集合*/@Overridepublic List<String> getRoleList(Object loginId, String loginType) {try {Long userId = Long.valueOf(loginId.toString());// 查询用户角色列表List<String> roles = roleService.getRolesByUserId(userId);log.debug("用户[{}]拥有角色: {}", userId, roles);return roles;} catch (Exception e) {log.error("获取用户角色列表失败, loginId: {}", loginId, e);return Collections.emptyList();}}
}
2.2.3 全局异常处理器
/*** 全局异常处理器*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {/*** 拦截:未登录异常*/@ExceptionHandler(NotLoginException.class)public Result handleNotLoginException(NotLoginException e) {String message = "";// 判断场景值,定制化异常信息switch (e.getType()) {case NotLoginException.NOT_TOKEN:message = "未提供Token";break;case NotLoginException.INVALID_TOKEN:message = "Token无效";break;case NotLoginException.TOKEN_TIMEOUT:message = "Token已过期";break;case NotLoginException.BE_REPLACED:message = "Token已被顶下线";break;case NotLoginException.KICK_OUT:message = "Token已被踢下线";break;default:message = "当前会话未登录";break;}log.warn("用户未登录访问受保护资源: {}", message);return Result.error(401, message);}/*** 拦截:缺少权限异常*/@ExceptionHandler(NotPermissionException.class)public Result handleNotPermissionException(NotPermissionException e) {log.warn("用户权限不足, 缺少权限: {}", e.getPermission());return Result.error(403, "权限不足,缺少权限:" + e.getPermission());}/*** 拦截:缺少角色异常*/@ExceptionHandler(NotRoleException.class)public Result handleNotRoleException(NotRoleException e) {log.warn("用户角色不足, 缺少角色: {}", e.getRole());return Result.error(403, "角色不足,缺少角色:" + e.getRole());}/*** 拦截:禁用账号异常*/@ExceptionHandler(DisableServiceException.class)public Result handleDisableServiceException(DisableServiceException e) {log.warn("账号被禁用, 禁用服务: {}, 禁用级别: {}, 禁用时间: {}秒", e.getService(), e.getLevel(), e.getDisableTime());return Result.error(423, "账号已被禁用:" + e.getDisableTime() + "秒后解封");}/*** 拦截:二级认证异常*/@ExceptionHandler(NotSafeException.class)public Result handleNotSafeException(NotSafeException e) {log.warn("二级认证失败: {}", e.getMessage());return Result.error(901, "请完成二级认证:" + e.getMessage());}/*** 拦截:服务封禁异常*/@ExceptionHandler(SaTokenException.class)public Result handleSaTokenException(SaTokenException e) {log.error("SA-Token异常: {}", e.getMessage(), e);return Result.error(500, "系统异常:" + e.getMessage());}/*** 拦截:其他所有异常*/@ExceptionHandler(Exception.class)public Result handleException(Exception e) {log.error("系统异常: {}", e.getMessage(), e);return Result.error(500, "系统繁忙,请稍后重试");}
}

2.3 实体类和数据访问层

2.3.1 实体类定义
/*** 用户实体类*/
@Entity
@Table(name = "sys_user")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(unique = true, nullable = false, length = 50)private String username;@Column(nullable = false, length = 100)private String password;@Column(length = 50)private String nickname;@Column(length = 100)private String email;@Column(length = 20)private String phone;@Column(length = 200)private String avatar;@Column(columnDefinition = "TINYINT DEFAULT 1")private Integer status;@CreationTimestamp@Column(name = "create_time")private LocalDateTime createTime;@UpdateTimestamp@Column(name = "update_time")private LocalDateTime updateTime;// 多对多关联角色@ManyToMany(fetch = FetchType.LAZY)@JoinTable(name = "sys_user_role",joinColumns = @JoinColumn(name = "user_id"),inverseJoinColumns = @JoinColumn(name = "role_id"))private Set<Role> roles = new HashSet<>();
}/*** 角色实体类*/
@Entity
@Table(name = "sys_role")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Role {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(unique = true, nullable = false, length = 50)private String roleCode;@Column(nullable = false, length = 50)private String roleName;@Column(length = 200)private String description;@Column(columnDefinition = "TINYINT DEFAULT 1")private Integer status;@CreationTimestamp@Column(name = "create_time")private LocalDateTime createTime;@UpdateTimestamp@Column(name = "update_time")private LocalDateTime updateTime;// 多对多关联权限@ManyToMany(fetch = FetchType.LAZY)@JoinTable(name = "sys_role_permission",joinColumns = @JoinColumn(name = "role_id"),inverseJoinColumns = @JoinColumn(name = "permission_id"))private Set<Permission> permissions = new HashSet<>();
}/*** 权限实体类*/
@Entity
@Table(name = "sys_permission")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Permission {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(unique = true, nullable = false, length = 100)private String permissionCode;@Column(nullable = false, length = 100)private String permissionName;@Column(length = 20)private String resourceType;@Column(length = 200)private String url;@Column(length = 10)private String method;@Column(columnDefinition = "BIGINT DEFAULT 0")private Long parentId;@Column(columnDefinition = "INT DEFAULT 0")private Integer sortOrder;@Column(columnDefinition = "TINYINT DEFAULT 1")private Integer status;@CreationTimestamp@Column(name = "create_time")private LocalDateTime createTime;@UpdateTimestamp@Column(name = "update_time")private LocalDateTime updateTime;
}
2.3.2 数据访问层实现
/*** 用户数据访问接口*/
@Repository
public interface UserRepository extends JpaRepository<User, Long> {/*** 根据用户名查找用户*/Optional<User> findByUsername(String username);/*** 根据用户名和状态查找用户*/Optional<User> findByUsernameAndStatus(String username, Integer status);/*** 检查用户名是否存在*/boolean existsByUsername(String username);/*** 检查邮箱是否存在*/boolean existsByEmail(String email);/*** 根据状态查找用户列表*/List<User> findByStatus(Integer status);/*** 根据用户名模糊查询*/@Query("SELECT u FROM User u WHERE u.username LIKE %:username%")List<User> findByUsernameLike(@Param("username") String username);
}/*** 角色数据访问接口*/
@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {/*** 根据角色编码查找角色*/Optional<Role> findByRoleCode(String roleCode);/*** 根据状态查找角色列表*/List<Role> findByStatus(Integer status);/*** 检查角色编码是否存在*/boolean existsByRoleCode(String roleCode);/*** 根据用户ID查找角色列表*/@Query("SELECT r FROM Role r JOIN r.users u WHERE u.id = :userId AND r.status = 1")List<Role> findByUserId(@Param("userId") Long userId);
}/*** 权限数据访问接口*/
@Repository
public interface PermissionRepository extends JpaRepository<Permission, Long> {/*** 根据权限编码查找权限*/Optional<Permission> findByPermissionCode(String permissionCode);/*** 根据状态查找权限列表*/List<Permission> findByStatus(Integer status);/*** 根据资源类型查找权限列表*/List<Permission> findByResourceType(String resourceType);/*** 根据父权限ID查找子权限列表*/List<Permission> findByParentId(Long parentId);/*** 根据用户ID查找权限列表*/@Query("SELECT DISTINCT p FROM Permission p " +"JOIN p.roles r " +"JOIN r.users u " +"WHERE u.id = :userId AND p.status = 1")List<Permission> findByUserId(@Param("userId") Long userId);/*** 根据角色ID查找权限列表*/@Query("SELECT p FROM Permission p JOIN p.roles r WHERE r.id = :roleId AND p.status = 1")List<Permission> findByRoleId(@Param("roleId") Long roleId);
}

2.4 业务服务层实现

2.4.1 用户服务实现
/*** 用户服务实现类*/
@Service
@Transactional
@Slf4j
public class UserServiceImpl implements UserService {@Autowiredprivate UserRepository userRepository;@Autowiredprivate RoleRepository roleRepository;@Autowiredprivate PasswordEncoder passwordEncoder;/*** 用户认证*/@Overridepublic User authenticate(String username, String password) {log.debug("用户认证开始, username: {}", username);// 查找用户Optional<User> userOpt = userRepository.findByUsernameAndStatus(username, 1);if (!userOpt.isPresent()) {log.warn("用户不存在或已被禁用, username: {}", username);return null;}User user = userOpt.get();// 验证密码if (!passwordEncoder.matches(password, user.getPassword())) {log.warn("用户密码错误, username: {}", username);return null;}log.info("用户认证成功, userId: {}, username: {}", user.getId(), username);return user;}/*** 根据ID获取用户*/@Override@Transactional(readOnly = true)public User getById(Long id) {return userRepository.findById(id).orElse(null);}/*** 根据用户名获取用户*/@Override@Transactional(readOnly = true)public User getByUsername(String username) {return userRepository.findByUsername(username).orElse(null);}/*** 创建用户*/@Overridepublic User createUser(UserCreateRequest request) {log.info("创建用户开始, username: {}", request.getUsername());// 检查用户名是否已存在if (userRepository.existsByUsername(request.getUsername())) {throw new BusinessException("用户名已存在");}// 检查邮箱是否已存在if (StringUtils.hasText(request.getEmail()) && userRepository.existsByEmail(request.getEmail())) {throw new BusinessException("邮箱已存在");}// 创建用户对象User user = User.builder().username(request.getUsername()).password(passwordEncoder.encode(request.getPassword())).nickname(request.getNickname()).email(request.getEmail()).phone(request.getPhone()).status(1).build();// 保存用户user = userRepository.save(user);// 分配默认角色if (request.getRoleIds() != null && !request.getRoleIds().isEmpty()) {assignRoles(user.getId(), request.getRoleIds());} else {// 分配默认用户角色Role defaultRole = roleRepository.findByRoleCode("user").orElse(null);if (defaultRole != null) {assignRoles(user.getId(), Collections.singletonList(defaultRole.getId()));}}log.info("用户创建成功, userId: {}, username: {}", user.getId(), user.getUsername());return user;}/*** 更新用户信息*/@Overridepublic User updateUser(Long userId, UserUpdateRequest request) {log.info("更新用户信息开始, userId: {}", userId);User user = userRepository.findById(userId).orElseThrow(() -> new BusinessException("用户不存在"));// 更新基本信息if (StringUtils.hasText(request.getNickname())) {user.setNickname(request.getNickname());}if (StringUtils.hasText(request.getEmail())) {// 检查邮箱是否已被其他用户使用if (userRepository.existsByEmail(request.getEmail()) && !request.getEmail().equals(user.getEmail())) {throw new BusinessException("邮箱已被其他用户使用");}user.setEmail(request.getEmail());}if (StringUtils.hasText(request.getPhone())) {user.setPhone(request.getPhone());}if (StringUtils.hasText(request.getAvatar())) {user.setAvatar(request.getAvatar());}// 保存更新user = userRepository.save(user);log.info("用户信息更新成功, userId: {}", userId);return user;}/*** 修改密码*/@Overridepublic void changePassword(Long userId, String oldPassword, String newPassword) {log.info("修改用户密码开始, userId: {}", userId);User user = userRepository.findById(userId).orElseThrow(() -> new BusinessException("用户不存在"));// 验证旧密码if (!passwordEncoder.matches(oldPassword, user.getPassword())) {throw new BusinessException("原密码错误");}// 更新密码user.setPassword(passwordEncoder.encode(newPassword));userRepository.save(user);log.info("用户密码修改成功, userId: {}", userId);}/*** 分配角色*/@Overridepublic void assignRoles(Long userId, List<Long> roleIds) {log.info("分配用户角色开始, userId: {}, roleIds: {}", userId, roleIds);User user = userRepository.findById(userId).orElseThrow(() -> new BusinessException("用户不存在"));// 清除现有角色user.getRoles().clear();// 分配新角色if (roleIds != null && !roleIds.isEmpty()) {List<Role> roles = roleRepository.findAllById(roleIds);user.getRoles().addAll(roles);}userRepository.save(user);log.info("用户角色分配成功, userId: {}, roleCount: {}", userId, user.getRoles().size());}/*** 启用/禁用用户*/@Overridepublic void updateUserStatus(Long userId, Integer status) {log.info("更新用户状态开始, userId: {}, status: {}", userId, status);User user = userRepository.findById(userId).orElseThrow(() -> new BusinessException("用户不存在"));user.setStatus(status);userRepository.save(user);// 如果是禁用用户,则踢下线if (status == 0) {StpUtil.kickout(userId);log.info("用户已被踢下线, userId: {}", userId);}log.info("用户状态更新成功, userId: {}, status: {}", userId, status);}/*** 删除用户*/@Overridepublic void deleteUser(Long userId) {log.info("删除用户开始, userId: {}", userId);User user = userRepository.findById(userId).orElseThrow(() -> new BusinessException("用户不存在"));// 踢下线StpUtil.kickout(userId);// 删除用户userRepository.delete(user);log.info("用户删除成功, userId: {}", userId);}/*** 获取用户列表*/@Override@Transactional(readOnly = true)public List<User> getAllUsers() {return userRepository.findAll();}/*** 分页获取用户列表*/@Override@Transactional(readOnly = true)public Page<User> getUserPage(Pageable pageable) {return userRepository.findAll(pageable);}
}
2.4.2 角色服务实现
/*** 角色服务实现类*/
@Service
@Transactional
@Slf4j
public class RoleServiceImpl implements RoleService {@Autowiredprivate RoleRepository roleRepository;@Autowiredprivate PermissionRepository permissionRepository;/*** 根据用户ID获取角色列表*/@Override@Transactional(readOnly = true)public List<String> getRolesByUserId(Long userId) {List<Role> roles = roleRepository.findByUserId(userId);return roles.stream().map(Role::getRoleCode).collect(Collectors.toList());}/*** 创建角色*/@Overridepublic Role createRole(RoleCreateRequest request) {log.info("创建角色开始, roleCode: {}", request.getRoleCode());// 检查角色编码是否已存在if (roleRepository.existsByRoleCode(request.getRoleCode())) {throw new BusinessException("角色编码已存在");}// 创建角色对象Role role = Role.builder().roleCode(request.getRoleCode()).roleName(request.getRoleName()).description(request.getDescription()).status(1).build();// 保存角色role = roleRepository.save(role);// 分配权限if (request.getPermissionIds() != null && !request.getPermissionIds().isEmpty()) {assignPermissions(role.getId(), request.getPermissionIds());}log.info("角色创建成功, roleId: {}, roleCode: {}", role.getId(), role.getRoleCode());return role;}/*** 分配权限*/@Overridepublic void assignPermissions(Long roleId, List<Long> permissionIds) {log.info("分配角色权限开始, roleId: {}, permissionIds: {}", roleId, permissionIds);Role role = roleRepository.findById(roleId).orElseThrow(() -> new BusinessException("角色不存在"));// 清除现有权限role.getPermissions().clear();// 分配新权限if (permissionIds != null && !permissionIds.isEmpty()) {List<Permission> permissions = permissionRepository.findAllById(permissionIds);role.getPermissions().addAll(permissions);}roleRepository.save(role);log.info("角色权限分配成功, roleId: {}, permissionCount: {}", roleId, role.getPermissions().size());}
}
2.4.3 权限服务实现
/*** 权限服务实现类*/
@Service
@Transactional
@Slf4j
public class PermissionServiceImpl implements PermissionService {@Autowiredprivate PermissionRepository permissionRepository;/*** 根据用户ID获取权限列表*/@Override@Transactional(readOnly = true)public List<String> getPermissionsByUserId(Long userId) {List<Permission> permissions = permissionRepository.findByUserId(userId);return permissions.stream().map(Permission::getPermissionCode).collect(Collectors.toList());}/*** 获取所有权限*/@Override@Transactional(readOnly = true)public List<Permission> getAllPermissions() {return permissionRepository.findByStatus(1);}/*** 构建权限树*/@Override@Transactional(readOnly = true)public List<PermissionTreeNode> buildPermissionTree() {List<Permission> allPermissions = getAllPermissions();// 构建权限树Map<Long, PermissionTreeNode> nodeMap = new HashMap<>();List<PermissionTreeNode> rootNodes = new ArrayList<>();// 创建所有节点for (Permission permission : allPermissions) {PermissionTreeNode node = PermissionTreeNode.builder().id(permission.getId()).permissionCode(permission.getPermissionCode()).permissionName(permission.getPermissionName()).resourceType(permission.getResourceType()).url(permission.getUrl()).method(permission.getMethod()).parentId(permission.getParentId()).sortOrder(permission.getSortOrder()).children(new ArrayList<>()).build();nodeMap.put(permission.getId(), node);}// 构建树形结构for (PermissionTreeNode node : nodeMap.values()) {if (node.getParentId() == 0) {rootNodes.add(node);} else {PermissionTreeNode parent = nodeMap.get(node.getParentId());if (parent != null) {parent.getChildren().add(node);}}}// 排序sortPermissionTree(rootNodes);return rootNodes;}private void sortPermissionTree(List<PermissionTreeNode> nodes) {nodes.sort(Comparator.comparing(PermissionTreeNode::getSortOrder));for (PermissionTreeNode node : nodes) {if (!node.getChildren().isEmpty()) {sortPermissionTree(node.getChildren());}}}
}

第三章:权限认证与授权实战

3.1 登录认证实现

3.1.1 登录控制器实现
/*** 认证控制器*/
@RestController
@RequestMapping("/auth")
@Slf4j
public class AuthController {@Autowiredprivate UserService userService;@Autowiredprivate CaptchaService captchaService;/*** 用户登录*/@PostMapping("/login")public Result login(@RequestBody @Valid LoginRequest request) {log.info("用户登录请求, username: {}", request.getUsername());// 验证验证码if (!captchaService.verifyCaptcha(request.getCaptchaKey(), request.getCaptchaCode())) {return Result.error("验证码错误");}// 用户认证User user = userService.authenticate(request.getUsername(), request.getPassword());if (user == null) {return Result.error("用户名或密码错误");}// 检查用户状态if (user.getStatus() != 1) {return Result.error("账号已被禁用");}// 执行登录StpUtil.login(user.getId(), new SaLoginModel().setDevice(request.getDevice()).setTimeout(request.getRememberMe() ? 30 * 24 * 3600 : -1) // 记住我30天.setIsLastingCookie(request.getRememberMe()));// 获取Token信息SaTokenInfo tokenInfo = StpUtil.getTokenInfo();// 构建登录响应LoginResponse response = LoginResponse.builder().tokenName(tokenInfo.getTokenName()).tokenValue(tokenInfo.getTokenValue()).isLogin(tokenInfo.getIsLogin()).loginId(tokenInfo.getLoginId()).loginType(tokenInfo.getLoginType()).tokenTimeout(tokenInfo.getTokenTimeout()).sessionTimeout(tokenInfo.getSessionTimeout()).tokenSessionTimeout(tokenInfo.getTokenSessionTimeout()).tokenActivityTimeout(tokenInfo.getTokenActivityTimeout()).loginDevice(tokenInfo.getLoginDevice()).tag(tokenInfo.getTag()).userInfo(UserInfo.builder().id(user.getId()).username(user.getUsername()).nickname(user.getNickname()).email(user.getEmail()).avatar(user.getAvatar()).build()).build();log.info("用户登录成功, userId: {}, username: {}, tokenValue: {}", user.getId(), user.getUsername(), tokenInfo.getTokenValue());return Result.success(response);}/*** 用户注销*/@PostMapping("/logout")public Result logout() {Object loginId = StpUtil.getLoginId();StpUtil.logout();log.info("用户注销成功, userId: {}", loginId);return Result.success("注销成功");}/*** 获取当前用户信息*/@GetMapping("/userinfo")public Result getUserInfo() {// 检查登录状态StpUtil.checkLogin();// 获取当前用户IDLong userId = StpUtil.getLoginIdAsLong();// 查询用户信息User user = userService.getById(userId);if (user == null) {return Result.error("用户不存在");}// 获取用户权限和角色List<String> permissions = StpUtil.getPermissionList();List<String> roles = StpUtil.getRoleList();// 构建用户信息响应UserInfoResponse response = UserInfoResponse.builder().id(user.getId()).username(user.getUsername()).nickname(user.getNickname()).email(user.getEmail()).phone(user.getPhone()).avatar(user.getAvatar()).status(user.getStatus()).createTime(user.getCreateTime()).permissions(permissions).roles(roles).build();return Result.success(response);}/*** 刷新Token*/@PostMapping("/refresh")public Result refreshToken() {// 检查登录状态StpUtil.checkLogin();// 续签TokenStpUtil.renewTimeout(7200); // 续签2小时// 获取新的Token信息SaTokenInfo tokenInfo = StpUtil.getTokenInfo();return Result.success(tokenInfo);}/*** 获取验证码*/@GetMapping("/captcha")public Result getCaptcha() {CaptchaResponse captcha = captchaService.generateCaptcha();return Result.success(captcha);}
}
3.1.2 验证码服务实现
/*** 验证码服务实现*/
@Service
@Slf4j
public class CaptchaServiceImpl implements CaptchaService {@Autowiredprivate RedisTemplate<String, String> redisTemplate;private static final String CAPTCHA_PREFIX = "captcha:";private static final int CAPTCHA_EXPIRE_TIME = 300; // 5分钟private static final int CAPTCHA_LENGTH = 4;/*** 生成验证码*/@Overridepublic CaptchaResponse generateCaptcha() {// 生成验证码keyString captchaKey = UUID.randomUUID().toString();// 生成验证码内容String captchaCode = generateRandomCode(CAPTCHA_LENGTH);// 生成验证码图片String captchaImage = generateCaptchaImage(captchaCode);// 存储到RedisredisTemplate.opsForValue().set(CAPTCHA_PREFIX + captchaKey, captchaCode.toLowerCase(), CAPTCHA_EXPIRE_TIME, TimeUnit.SECONDS);log.debug("生成验证码, key: {}, code: {}", captchaKey, captchaCode);return CaptchaResponse.builder().captchaKey(captchaKey).captchaImage(captchaImage).expireTime(CAPTCHA_EXPIRE_TIME).build();}/*** 验证验证码*/@Overridepublic boolean verifyCaptcha(String captchaKey, String captchaCode) {if (!StringUtils.hasText(captchaKey) || !StringUtils.hasText(captchaCode)) {return false;}String redisKey = CAPTCHA_PREFIX + captchaKey;String storedCode = redisTemplate.opsForValue().get(redisKey);if (storedCode == null) {log.warn("验证码已过期或不存在, key: {}", captchaKey);return false;}// 验证后删除验证码redisTemplate.delete(redisKey);boolean isValid = storedCode.equalsIgnoreCase(captchaCode);log.debug("验证码校验结果, key: {}, code: {}, result: {}", captchaKey, captchaCode, isValid);return isValid;}private String generateRandomCode(int length) {String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";StringBuilder sb = new StringBuilder();Random random = new Random();for (int i = 0; i < length; i++) {sb.append(chars.charAt(random.nextInt(chars.length())));}return sb.toString();}private String generateCaptchaImage(String code) {// 创建图片int width = 120;int height = 40;BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);Graphics2D g = image.createGraphics();// 设置背景色g.setColor(Color.WHITE);g.fillRect(0, 0, width, height);// 设置字体g.setFont(new Font("Arial", Font.BOLD, 20));// 绘制验证码Random random = new Random();for (int i = 0; i < code.length(); i++) {g.setColor(new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255)));g.drawString(String.valueOf(code.charAt(i)), 20 + i * 20, 25);}// 添加干扰线for (int i = 0; i < 5; i++) {g.setColor(new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255)));g.drawLine(random.nextInt(width), random.nextInt(height), random.nextInt(width), random.nextInt(height));}g.dispose();// 转换为Base64try {ByteArrayOutputStream baos = new ByteArrayOutputStream();ImageIO.write(image, "png", baos);byte[] imageBytes = baos.toByteArray();return "data:image/png;base64," + Base64.getEncoder().encodeToString(imageBytes);} catch (IOException e) {log.error("生成验证码图片失败", e);return null;}}
}## 第四章:高级特性与扩展应用### 4.1 单点登录(SSO)实现#### 4.1.1 SSO基础配置SA-Token提供了强大的单点登录功能,支持同域和跨域的SSO实现。```java
/*** SSO配置类*/
@Configuration
public class SsoConfig {/*** SSO相关配置*/@Bean@ConfigurationProperties(prefix = "sa-token.sso")public SaSsoConfig getSaSsoConfig() {return new SaSsoConfig()// SSO-Server端 统一认证地址.setAuthUrl("http://sa-sso-server.com:9000/sso/auth")// SSO-Server端 ticket校验地址.setCheckTicketUrl("http://sa-sso-server.com:9000/sso/checkTicket")// SSO-Server端 单点注销地址.setSloUrl("http://sa-sso-server.com:9000/sso/signout")// 当前Client端 单点注销回调URL.setSsoLogoutCall("http://sa-sso-client1.com:9001/sso/logoutCall")// 是否打开单点注销功能.setIsSlo(true);}
}
4.1.2 SSO-Server端实现
/*** SSO认证服务端控制器*/
@RestController
@RequestMapping("/sso")
@Slf4j
public class SsoServerController {/*** SSO统一认证页面*/@GetMapping("/auth")public SaResult auth(String redirect, String mode, HttpServletRequest request) {log.info("SSO统一认证,redirect={}, mode={}", redirect, mode);// 如果已经登录,则直接重定向到Client端if (StpUtil.isLogin()) {return SaSsoUtil.buildRedirectUrl(StpUtil.getLoginId(), redirect);}// 未登录,显示登录页面return SaResult.ok().setData(buildLoginPage(redirect, mode));}/*** 处理登录请求*/@PostMapping("/doLogin")public SaResult doLogin(String username, String password, String redirect) {log.info("SSO登录处理,username={}, redirect={}", username, redirect);// 验证用户名密码if (validateUser(username, password)) {// 登录成功,生成ticket并重定向StpUtil.login(username);return SaSsoUtil.buildRedirectUrl(username, redirect);}return SaResult.error("用户名或密码错误");}/*** 校验ticket*/@GetMapping("/checkTicket")public SaResult checkTicket(String ticket, String ssoLogoutCall) {log.info("校验ticket,ticket={}, ssoLogoutCall={}", ticket, ssoLogoutCall);// 校验ticket,获取账号idObject loginId = SaSsoUtil.checkTicket(ticket);if (loginId != null) {// 注册此客户端的单点注销回调URLSaSsoUtil.registerClient(loginId, ssoLogoutCall);return SaResult.data(loginId);}return SaResult.error("无效的ticket");}/*** 单点注销*/@GetMapping("/signout")public SaResult signout(String loginId, String secretkey) {log.info("SSO单点注销,loginId={}", loginId);// 校验秘钥SaSsoUtil.checkSecretkey(secretkey);// 遍历通知所有Client端注销SaSsoUtil.singleLogout(loginId);return SaResult.ok("单点注销成功");}/*** 验证用户凭据*/private boolean validateUser(String username, String password) {// 这里应该连接数据库验证用户信息// 为了演示,简单验证return "admin".equals(username) && "123456".equals(password);}/*** 构建登录页面HTML*/private String buildLoginPage(String redirect, String mode) {return """<!DOCTYPE html><html><head><title>SSO统一认证中心</title><meta charset="utf-8"><style>.login-form { width: 300px; margin: 100px auto; padding: 20px; border: 1px solid #ddd; }.form-item { margin: 10px 0; }.form-item input { width: 100%; padding: 8px; }.btn { width: 100%; padding: 10px; background: #007bff; color: white; border: none; cursor: pointer; }</style></head><body><div class="login-form"><h2>统一认证中心</h2><form action="/sso/doLogin" method="post"><input type="hidden" name="redirect" value="%s"><div class="form-item"><input type="text" name="username" placeholder="用户名" required></div><div class="form-item"><input type="password" name="password" placeholder="密码" required></div><div class="form-item"><button type="submit" class="btn">登录</button></div></form></div></body></html>""".formatted(redirect != null ? redirect : "");}
}
4.1.3 SSO-Client端实现
/*** SSO客户端控制器*/
@RestController
@RequestMapping("/sso")
@Slf4j
public class SsoClientController {/*** 首页*/@GetMapping("/")public SaResult index() {String loginId = (String) StpUtil.getLoginIdDefaultNull();if (loginId != null) {return SaResult.ok("欢迎用户:" + loginId);}return SaResult.ok("当前未登录").setData("<a href='/sso/login'>点击登录</a>");}/*** 发起登录*/@GetMapping("/login")public SaResult login(String back) {// 构建授权地址String authUrl = SaSsoUtil.buildAuthUrl();log.info("重定向到SSO认证中心:{}", authUrl);return SaResult.ok().setData("redirect:" + authUrl);}/*** SSO登录回调*/@GetMapping("/login/callback")public SaResult loginCallback(String ticket, String back) {log.info("SSO登录回调,ticket={}, back={}", ticket, back);// 根据ticket进行登录Object loginId = SaSsoUtil.checkTicket(ticket, "/sso/logoutCall");if (loginId != null) {StpUtil.login(loginId);return SaResult.ok("登录成功").setData("用户ID:" + loginId);}return SaResult.error("登录失败");}/*** 单点注销回调*/@GetMapping("/logoutCall")public SaResult logoutCall(String loginId, String secretkey) {log.info("收到单点注销回调,loginId={}", loginId);// 校验秘钥SaSsoUtil.checkSecretkey(secretkey);// 注销当前用户StpUtil.logout(loginId);return SaResult.ok("注销成功");}/*** 查询登录状态*/@GetMapping("/isLogin")public SaResult isLogin() {boolean isLogin = StpUtil.isLogin();Object loginId = StpUtil.getLoginIdDefaultNull();return SaResult.ok().set("isLogin", isLogin).set("loginId", loginId).set("tokenInfo", StpUtil.getTokenInfo());}
}

4.2 OAuth2.0集成

在这里插入图片描述

4.2.1 OAuth2配置

SA-Token提供了完整的OAuth2.0支持,可以快速构建OAuth2认证服务器。

/*** OAuth2配置类*/
@Configuration
public class OAuth2Config {/*** OAuth2配置*/@Bean@ConfigurationProperties(prefix = "sa-token.oauth2")public SaOAuth2Config oauth2Config() {return new SaOAuth2Config()// 是否打开模式:授权码(Authorization Code).setIsCode(true)// 是否打开模式:隐藏式(Implicit).setIsImplicit(true)// 是否打开模式:密码式(Password).setIsPassword(true)// 是否打开模式:客户端凭证(Client Credentials).setIsClient(true)// 是否在每次Refresh-Token刷新Access-Token时,产生一个新的Refresh-Token.setIsNewRefresh(true);}/*** OAuth2数据加载器*/@Componentpublic static class OAuth2DataLoader implements SaOAuth2DataLoader {@Autowiredprivate ClientService clientService;@Autowiredprivate UserService userService;/*** 根据 client_id 获取 Client 信息*/@Overridepublic SaClientModel getClientModel(String clientId) {// 从数据库查询客户端信息Client client = clientService.getByClientId(clientId);if (client == null) {return null;}return new SaClientModel().setClientId(client.getClientId()).setClientSecret(client.getClientSecret()).setAllowUrl(client.getAllowUrl()).setContractScope(client.getContractScope()).setIsAutoMode(client.getIsAutoMode());}/*** 根据 ClientId 和 LoginId 获取openid*/@Overridepublic String getOpenid(String clientId, Object loginId) {// 可以根据 clientId 和 loginId 生成openidreturn DigestUtils.md5Hex(clientId + ":" + loginId);}/*** 校验:指定 LoginId 是否对指定 Client 授权给定 Scope*/@Overridepublic boolean isGrant(Object loginId, String clientId, String scope) {// 查询用户是否已授权return userService.hasGranted(String.valueOf(loginId), clientId, scope);}/*** 保存:指定 LoginId 对指定 Client 授权给定 Scope*/@Overridepublic void saveGrant(Object loginId, String clientId, String scope) {// 保存用户授权信息userService.saveGrant(String.valueOf(loginId), clientId, scope);}}
}

4.3 微服务网关鉴权

4.3.1 网关鉴权配置

在微服务架构中,SA-Token可以作为统一的鉴权组件集成到网关中。

/*** 网关鉴权配置*/
@Configuration
@EnableWebFluxSecurity
public class GatewayAuthConfig {/*** 注册Sa-Token全局过滤器*/@Beanpublic SaReactorFilter getSaReactorFilter() {return new SaReactorFilter()// 拦截地址.addInclude("/**")// 排除地址.addExclude("/favicon.ico", "/actuator/**")// 鉴权方法:每次访问进入.setAuth(obj -> {// 登录校验 -- 拦截所有路由,并排除/user/doLogin用于开放登录SaRouter.match("/**", "/auth/login", r -> StpUtil.checkLogin());// 权限认证 -- 不同模块, 校验不同权限SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));// 角色认证 -- 拦截以 admin 开头的路由,必须具备 admin 角色或者 super-admin 角色才可以通过认证SaRouter.match("/admin/**", r -> StpUtil.checkRoleOr("admin", "super-admin"));})// 异常处理方法:每次setAuth函数出现异常时进入.setError(e -> {return SaResult.error(e.getMessage());});}/*** 配置跨域*/@Beanpublic CorsWebFilter corsFilter() {CorsConfiguration config = new CorsConfiguration();config.addAllowedMethod("*");config.addAllowedOrigin("*");config.addAllowedHeader("*");UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());source.registerCorsConfiguration("/**", config);return new CorsWebFilter(source);}
}

4.4 多账户体系支持

4.4.1 多账户配置

SA-Token支持多账户体系,可以同时管理用户账户和管理员账户。

/*** 多账户体系配置*/
@Configuration
public class MultiAccountConfig {/*** 用户账户StpLogic*/@Bean("userStpLogic")public StpLogic getUserStpLogic() {return new StpLogic("user") {// 重写获取权限列表的方法@Overridepublic List<String> getPermissionList(Object loginId, String loginType) {// 查询用户权限return userService.getPermissionsByUserId(String.valueOf(loginId));}// 重写获取角色列表的方法@Overridepublic List<String> getRoleList(Object loginId, String loginType) {// 查询用户角色return userService.getRolesByUserId(String.valueOf(loginId));}};}/*** 管理员账户StpLogic*/@Bean("adminStpLogic")public StpLogic getAdminStpLogic() {return new StpLogic("admin") {@Overridepublic List<String> getPermissionList(Object loginId, String loginType) {// 查询管理员权限return adminService.getPermissionsByAdminId(String.valueOf(loginId));}@Overridepublic List<String> getRoleList(Object loginId, String loginType) {// 查询管理员角色return adminService.getRolesByAdminId(String.valueOf(loginId));}};}
}

第五章:生产环境最佳实践

5.1 性能优化策略

5.1.1 Token存储优化

在高并发场景下,Token的存储和检索性能至关重要。

/*** Token存储优化配置*/
@Configuration
public class TokenStorageOptimization {/*** Redis连接池优化*/@Beanpublic LettuceConnectionFactory redisConnectionFactory() {// 连接池配置GenericObjectPoolConfig<StatefulRedisConnection<String, String>> poolConfig = new GenericObjectPoolConfig<>();poolConfig.setMaxTotal(200);poolConfig.setMaxIdle(50);poolConfig.setMinIdle(10);poolConfig.setTestOnBorrow(true);poolConfig.setTestOnReturn(true);poolConfig.setTestWhileIdle(true);// Lettuce连接工厂LettucePoolingClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder().poolConfig(poolConfig).commandTimeout(Duration.ofSeconds(5)).shutdownTimeout(Duration.ofSeconds(10)).build();RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration();serverConfig.setHostName("localhost");serverConfig.setPort(6379);serverConfig.setDatabase(0);return new LettuceConnectionFactory(serverConfig, clientConfig);}/*** 自定义Token存储实现*/@Componentpublic class OptimizedSaTokenDao implements SaTokenDao {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;private final String TOKEN_PREFIX = "satoken:";@Overridepublic String get(String key) {try {Object value = redisTemplate.opsForValue().get(TOKEN_PREFIX + key);return value != null ? value.toString() : null;} catch (Exception e) {log.error("Redis获取Token失败,key={}", key, e);return null;}}@Overridepublic void set(String key, String value, long timeout) {try {if (timeout > 0) {redisTemplate.opsForValue().set(TOKEN_PREFIX + key, value, Duration.ofSeconds(timeout));} else {redisTemplate.opsForValue().set(TOKEN_PREFIX + key, value);}} catch (Exception e) {log.error("Redis设置Token失败,key={}, value={}", key, value, e);}}@Overridepublic void update(String key, String value) {try {Long expire = redisTemplate.getExpire(TOKEN_PREFIX + key);if (expire != null && expire > 0) {redisTemplate.opsForValue().set(TOKEN_PREFIX + key, value, Duration.ofSeconds(expire));} else {redisTemplate.opsForValue().set(TOKEN_PREFIX + key, value);}} catch (Exception e) {log.error("Redis更新Token失败,key={}, value={}", key, value, e);}}@Overridepublic void delete(String key) {try {redisTemplate.delete(TOKEN_PREFIX + key);} catch (Exception e) {log.error("Redis删除Token失败,key={}", key, e);}}@Overridepublic long getTimeout(String key) {try {Long expire = redisTemplate.getExpire(TOKEN_PREFIX + key);return expire != null ? expire : -1;} catch (Exception e) {log.error("Redis获取Token过期时间失败,key={}", key, e);return -1;}}@Overridepublic void updateTimeout(String key, long timeout) {try {redisTemplate.expire(TOKEN_PREFIX + key, Duration.ofSeconds(timeout));} catch (Exception e) {log.error("Redis更新Token过期时间失败,key={}, timeout={}", key, timeout, e);}}}
}
5.1.2 权限缓存优化
/*** 权限缓存优化服务*/
@Service
@Slf4j
public class PermissionCacheService {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Autowiredprivate UserService userService;private static final String PERMISSION_CACHE_PREFIX = "permission:";private static final String ROLE_CACHE_PREFIX = "role:";private static final int CACHE_EXPIRE_SECONDS = 3600; // 1小时/*** 获取用户权限列表(带缓存)*/public List<String> getUserPermissions(String userId) {String cacheKey = PERMISSION_CACHE_PREFIX + userId;try {// 先从缓存获取List<String> permissions = (List<String>) redisTemplate.opsForValue().get(cacheKey);if (permissions != null) {log.debug("从缓存获取用户权限,userId={}", userId);return permissions;}// 缓存未命中,从数据库查询permissions = userService.getPermissionsByUserId(userId);// 写入缓存redisTemplate.opsForValue().set(cacheKey, permissions, Duration.ofSeconds(CACHE_EXPIRE_SECONDS));log.debug("从数据库查询用户权限并缓存,userId={}, permissions={}", userId, permissions);return permissions;} catch (Exception e) {log.error("获取用户权限失败,userId={}", userId, e);// 缓存异常时直接查询数据库return userService.getPermissionsByUserId(userId);}}/*** 获取用户角色列表(带缓存)*/public List<String> getUserRoles(String userId) {String cacheKey = ROLE_CACHE_PREFIX + userId;try {List<String> roles = (List<String>) redisTemplate.opsForValue().get(cacheKey);if (roles != null) {log.debug("从缓存获取用户角色,userId={}", userId);return roles;}roles = userService.getRolesByUserId(userId);redisTemplate.opsForValue().set(cacheKey, roles, Duration.ofSeconds(CACHE_EXPIRE_SECONDS));log.debug("从数据库查询用户角色并缓存,userId={}, roles={}", userId, roles);return roles;} catch (Exception e) {log.error("获取用户角色失败,userId={}", userId, e);return userService.getRolesByUserId(userId);}}/*** 清除用户权限缓存*/public void clearUserPermissionCache(String userId) {try {redisTemplate.delete(PERMISSION_CACHE_PREFIX + userId);redisTemplate.delete(ROLE_CACHE_PREFIX + userId);log.info("清除用户权限缓存,userId={}", userId);} catch (Exception e) {log.error("清除用户权限缓存失败,userId={}", userId, e);}}/*** 批量预热权限缓存*/@Asyncpublic void preloadPermissionCache(List<String> userIds) {log.info("开始预热权限缓存,用户数量={}", userIds.size());for (String userId : userIds) {try {getUserPermissions(userId);getUserRoles(userId);Thread.sleep(10); // 避免过快请求} catch (Exception e) {log.error("预热用户权限缓存失败,userId={}", userId, e);}}log.info("权限缓存预热完成");}
}

5.2 安全加固措施

5.2.1 Token安全增强
/*** Token安全增强配置*/
@Configuration
public class TokenSecurityConfig {/*** 自定义Token生成策略*/@Beanpublic SaTokenAction saTokenAction() {return new SaTokenAction() {@Overridepublic String createToken(Object loginId, String loginType) {// 生成更安全的TokenString timestamp = String.valueOf(System.currentTimeMillis());String randomStr = UUID.randomUUID().toString().replace("-", "");String userAgent = SaHolder.getRequest().getHeader("User-Agent");String clientIp = SaFoxUtil.getClientIP();// 组合信息进行加密String tokenData = loginId + ":" + loginType + ":" + timestamp + ":" + randomStr + ":" + DigestUtils.md5Hex(userAgent + clientIp);// 使用AES加密return AESUtil.encrypt(tokenData, getTokenSecret());}@Overridepublic Object getLoginIdByToken(String tokenValue) {try {// 解密TokenString tokenData = AESUtil.decrypt(tokenValue, getTokenSecret());String[] parts = tokenData.split(":");if (parts.length >= 5) {String loginId = parts[0];String timestamp = parts[2];// 检查Token时效性(额外的时间校验)long createTime = Long.parseLong(timestamp);long maxAge = 24 * 60 * 60 * 1000; // 24小时if (System.currentTimeMillis() - createTime > maxAge) {throw new SaTokenException("Token已过期");}return loginId;}} catch (Exception e) {log.error("Token解析失败", e);}return null;}};}/*** Token签名密钥*/private String getTokenSecret() {// 从配置文件或环境变量获取密钥return "your-secret-key-here";}/*** IP白名单验证*/@Componentpublic class IpWhitelistValidator {private final Set<String> whitelistIps = new HashSet<>();@PostConstructpublic void init() {// 从配置文件加载IP白名单whitelistIps.add("127.0.0.1");whitelistIps.add("192.168.1.0/24");}public boolean isAllowed(String clientIp) {return whitelistIps.contains(clientIp) || isInSubnet(clientIp);}private boolean isInSubnet(String clientIp) {// 实现子网匹配逻辑for (String subnet : whitelistIps) {if (subnet.contains("/") && matchSubnet(clientIp, subnet)) {return true;}}return false;}private boolean matchSubnet(String ip, String subnet) {// 子网匹配实现try {String[] subnetParts = subnet.split("/");String networkIp = subnetParts[0];int prefixLength = Integer.parseInt(subnetParts[1]);InetAddress targetAddr = InetAddress.getByName(ip);InetAddress networkAddr = InetAddress.getByName(networkIp);byte[] targetBytes = targetAddr.getAddress();byte[] networkBytes = networkAddr.getAddress();int bytesToCheck = prefixLength / 8;int bitsToCheck = prefixLength % 8;// 检查完整字节for (int i = 0; i < bytesToCheck; i++) {if (targetBytes[i] != networkBytes[i]) {return false;}}// 检查剩余位if (bitsToCheck > 0 && bytesToCheck < targetBytes.length) {int mask = 0xFF << (8 - bitsToCheck);return (targetBytes[bytesToCheck] & mask) == (networkBytes[bytesToCheck] & mask);}return true;} catch (Exception e) {log.error("子网匹配失败", e);return false;}}}
}
5.2.2 防攻击策略
/*** 防攻击策略实现*/
@Component
@Slf4j
public class SecurityDefenseService {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;private static final String LOGIN_ATTEMPT_PREFIX = "login_attempt:";private static final String RATE_LIMIT_PREFIX = "rate_limit:";private static final int MAX_LOGIN_ATTEMPTS = 5;private static final int LOGIN_LOCK_DURATION = 300; // 5分钟private static final int RATE_LIMIT_REQUESTS = 100;private static final int RATE_LIMIT_WINDOW = 60; // 1分钟/*** 检查登录尝试次数*/public boolean checkLoginAttempts(String clientIp, String username) {String key = LOGIN_ATTEMPT_PREFIX + clientIp + ":" + username;try {Integer attempts = (Integer) redisTemplate.opsForValue().get(key);if (attempts != null && attempts >= MAX_LOGIN_ATTEMPTS) {log.warn("登录尝试次数超限,IP={}, username={}, attempts={}", clientIp, username, attempts);return false;}return true;} catch (Exception e) {log.error("检查登录尝试次数失败", e);return true; // 异常时允许登录}}/*** 记录登录失败*/public void recordLoginFailure(String clientIp, String username) {String key = LOGIN_ATTEMPT_PREFIX + clientIp + ":" + username;try {Integer attempts = (Integer) redisTemplate.opsForValue().get(key);attempts = attempts != null ? attempts + 1 : 1;redisTemplate.opsForValue().set(key, attempts, Duration.ofSeconds(LOGIN_LOCK_DURATION));log.info("记录登录失败,IP={}, username={}, attempts={}", clientIp, username, attempts);} catch (Exception e) {log.error("记录登录失败次数异常", e);}}/*** 清除登录失败记录*/public void clearLoginFailures(String clientIp, String username) {String key = LOGIN_ATTEMPT_PREFIX + clientIp + ":" + username;try {redisTemplate.delete(key);log.info("清除登录失败记录,IP={}, username={}", clientIp, username);} catch (Exception e) {log.error("清除登录失败记录异常", e);}}/*** 检查请求频率限制*/public boolean checkRateLimit(String clientIp) {String key = RATE_LIMIT_PREFIX + clientIp;try {Integer requests = (Integer) redisTemplate.opsForValue().get(key);if (requests != null && requests >= RATE_LIMIT_REQUESTS) {log.warn("请求频率超限,IP={}, requests={}", clientIp, requests);return false;}// 增加请求计数if (requests == null) {redisTemplate.opsForValue().set(key, 1, Duration.ofSeconds(RATE_LIMIT_WINDOW));} else {redisTemplate.opsForValue().increment(key);}return true;} catch (Exception e) {log.error("检查请求频率限制失败", e);return true; // 异常时允许请求}}/*** SQL注入检测*/public boolean detectSqlInjection(String input) {if (input == null || input.isEmpty()) {return false;}String[] sqlKeywords = {"select", "insert", "update", "delete", "drop", "create", "alter","union", "exec", "execute", "script", "javascript", "vbscript","onload", "onerror", "onclick", "'", "\"", ";", "--", "/*", "*/"};String lowerInput = input.toLowerCase();for (String keyword : sqlKeywords) {if (lowerInput.contains(keyword)) {log.warn("检测到可疑SQL注入尝试,input={}", input);return true;}}return false;}/*** XSS攻击检测*/public boolean detectXssAttack(String input) {if (input == null || input.isEmpty()) {return false;}String[] xssPatterns = {"<script", "</script>", "javascript:", "vbscript:", "onload=", "onerror=", "onclick=", "onmouseover=", "onfocus=", "onblur=","alert(", "confirm(", "prompt(", "document.cookie", "document.write"};String lowerInput = input.toLowerCase();for (String pattern : xssPatterns) {if (lowerInput.contains(pattern)) {log.warn("检测到可疑XSS攻击尝试,input={}", input);return true;}}return false;}
}

5.3 监控与日志

5.3.1 认证监控
/*** 认证监控服务*/
@Service
@Slf4j
public class AuthMonitorService {@Autowiredprivate MeterRegistry meterRegistry;@Autowiredprivate RedisTemplate<String, Object> redisTemplate;private final Counter loginSuccessCounter;private final Counter loginFailureCounter;private final Counter logoutCounter;private final Timer authenticationTimer;public AuthMonitorService(MeterRegistry meterRegistry) {this.meterRegistry = meterRegistry;this.loginSuccessCounter = Counter.builder("auth.login.success").description("成功登录次数").register(meterRegistry);this.loginFailureCounter = Counter.builder("auth.login.failure").description("登录失败次数").register(meterRegistry);this.logoutCounter = Counter.builder("auth.logout").description("注销次数").register(meterRegistry);this.authenticationTimer = Timer.builder("auth.authentication.duration").description("认证耗时").register(meterRegistry);}/*** 记录登录成功*/public void recordLoginSuccess(String userId, String clientIp, String userAgent) {loginSuccessCounter.increment();// 记录详细日志log.info("用户登录成功 - userId={}, clientIp={}, userAgent={}", userId, clientIp, userAgent);// 记录登录历史recordLoginHistory(userId, clientIp, userAgent, true);// 更新在线用户统计updateOnlineUserStats(userId, true);}/*** 记录登录失败*/public void recordLoginFailure(String username, String clientIp, String reason) {loginFailureCounter.increment(Tags.of("reason", reason));log.warn("用户登录失败 - username={}, clientIp={}, reason={}", username, clientIp, reason);// 记录失败历史recordLoginHistory(username, clientIp, null, false);}/*** 记录注销*/public void recordLogout(String userId, String clientIp) {logoutCounter.increment();log.info("用户注销 - userId={}, clientIp={}", userId, clientIp);// 更新在线用户统计updateOnlineUserStats(userId, false);}/*** 记录认证耗时*/public void recordAuthenticationTime(Duration duration) {authenticationTimer.record(duration);}/*** 记录登录历史*/private void recordLoginHistory(String userId, String clientIp, String userAgent, boolean success) {try {LoginHistory history = new LoginHistory();history.setUserId(userId);history.setClientIp(clientIp);history.setUserAgent(userAgent);history.setSuccess(success);history.setLoginTime(LocalDateTime.now());// 异步保存到数据库CompletableFuture.runAsync(() -> {// 保存登录历史逻辑saveLoginHistory(history);});} catch (Exception e) {log.error("记录登录历史失败", e);}}/*** 更新在线用户统计*/private void updateOnlineUserStats(String userId, boolean online) {try {String key = "online_users";if (online) {redisTemplate.opsForSet().add(key, userId);} else {redisTemplate.opsForSet().remove(key, userId);}// 更新Micrometer指标Long onlineCount = redisTemplate.opsForSet().size(key);Gauge.builder("auth.online.users").description("在线用户数").register(meterRegistry, this, obj -> onlineCount != null ? onlineCount : 0);} catch (Exception e) {log.error("更新在线用户统计失败", e);}}/*** 获取认证统计信息*/public AuthStatistics getAuthStatistics() {try {AuthStatistics stats = new AuthStatistics();// 从Micrometer获取统计数据stats.setLoginSuccessCount((long) loginSuccessCounter.count());stats.setLoginFailureCount((long) loginFailureCounter.count());stats.setLogoutCount((long) logoutCounter.count());stats.setAverageAuthTime(authenticationTimer.mean(TimeUnit.MILLISECONDS));// 获取在线用户数Long onlineUsers = redisTemplate.opsForSet().size("online_users");stats.setOnlineUserCount(onlineUsers != null ? onlineUsers : 0);return stats;} catch (Exception e) {log.error("获取认证统计信息失败", e);return new AuthStatistics();}}/*** 保存登录历史(实际实现)*/private void saveLoginHistory(LoginHistory history) {// 实际的数据库保存逻辑log.debug("保存登录历史:{}", history);}
}
5.3.2 审计日志
/*** 审计日志服务*/
@Service
@Slf4j
public class AuditLogService {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@EventListenerpublic void handleLoginEvent(LoginEvent event) {AuditLog auditLog = AuditLog.builder().userId(event.getUserId()).action("LOGIN").resource("AUTH").clientIp(event.getClientIp()).userAgent(event.getUserAgent()).timestamp(LocalDateTime.now()).success(event.isSuccess()).details(event.getDetails()).build();saveAuditLog(auditLog);}@EventListenerpublic void handlePermissionCheckEvent(PermissionCheckEvent event) {AuditLog auditLog = AuditLog.builder().userId(event.getUserId()).action("PERMISSION_CHECK").resource(event.getResource()).permission(event.getPermission()).clientIp(SaFoxUtil.getClientIP()).timestamp(LocalDateTime.now()).success(event.isSuccess()).details(event.getDetails()).build();saveAuditLog(auditLog);}/*** 保存审计日志*/private void saveAuditLog(AuditLog auditLog) {try {// 异步保存到数据库CompletableFuture.runAsync(() -> {// 实际的数据库保存逻辑log.info("审计日志:{}", auditLog);});// 同时保存到Redis用于实时查询String key = "audit_logs:" + LocalDate.now().toString();redisTemplate.opsForList().leftPush(key, auditLog);redisTemplate.expire(key, Duration.ofDays(7)); // 保留7天} catch (Exception e) {log.error("保存审计日志失败", e);}}/*** 查询审计日志*/public List<AuditLog> queryAuditLogs(String userId, LocalDate date, String action) {try {String key = "audit_logs:" + date.toString();List<Object> logs = redisTemplate.opsForList().range(key, 0, -1);return logs.stream().map(obj -> (AuditLog) obj).filter(log -> userId == null || userId.equals(log.getUserId())).filter(log -> action == null || action.equals(log.getAction())).collect(Collectors.toList());} catch (Exception e) {log.error("查询审计日志失败", e);return Collections.emptyList();}}
}

第六章:总结与展望

6.1 知识点回顾

通过本文的深入学习,我们全面掌握了SA-Token权限认证框架的核心技术和实践应用。让我们回顾一下主要的知识点:

6.1.1 核心概念与特性

SA-Token作为一个轻量级的Java权限认证框架,具有以下核心优势:

  • 简洁的API设计StpUtil.login()StpUtil.checkLogin()等简单易用的API
  • 丰富的功能特性:支持登录认证、权限验证、角色验证、踢人下线、会话管理等
  • 灵活的集成方式:与SpringBoot、SpringCloud等主流框架无缝集成
  • 强大的扩展能力:支持自定义Token生成策略、存储方式、权限验证逻辑等
6.1.2 技术架构要点
/*** SA-Token技术架构核心组件回顾*/
public class SaTokenArchitectureReview {/*** 1. 核心组件*/// StpUtil - 权限认证工具类// SaTokenDao - Token存储接口// SaTokenConfig - 框架配置类// StpInterface - 权限数据接口/*** 2. 认证流程*/public void authenticationFlow() {// 用户登录 -> 生成Token -> 存储会话 -> 返回TokenStpUtil.login(userId);// 请求验证 -> 解析Token -> 校验会话 -> 检查权限StpUtil.checkLogin();StpUtil.checkPermission("user:list");}/*** 3. 扩展机制*/// 自定义Token生成:实现SaTokenAction接口// 自定义存储方式:实现SaTokenDao接口  // 自定义权限验证:实现StpInterface接口// 自定义配置策略:继承SaTokenConfig类
}
6.1.3 实战应用总结

在实际项目中,我们学会了如何:

  1. 基础集成:SpringBoot项目中快速集成SA-Token
  2. 权限设计:构建RBAC权限模型,实现细粒度权限控制
  3. 高级特性:单点登录、OAuth2.0、微服务网关鉴权等企业级应用
  4. 性能优化:Token存储优化、权限缓存、连接池配置等
  5. 安全加固:防暴力破解、SQL注入检测、XSS防护等
  6. 监控运维:认证监控、审计日志、性能指标等

6.2 最佳实践总结

6.2.1 开发规范
/*** SA-Token开发最佳实践*/
@Component
public class SaTokenBestPractices {/*** 1. 统一异常处理*/@ExceptionHandler(NotLoginException.class)public SaResult handleNotLoginException(NotLoginException e) {return SaResult.error("请先登录").setCode(401);}/*** 2. 权限注解使用*/@SaCheckPermission("user:list")@GetMapping("/users")public SaResult getUsers() {// 业务逻辑return SaResult.ok();}/*** 3. 会话管理*/public void sessionManagement() {// 设置会话数据StpUtil.getSession().set("userInfo", userInfo);// 获取会话数据UserInfo info = (UserInfo) StpUtil.getSession().get("userInfo");// 清理会话StpUtil.getSession().clear();}/*** 4. 多端登录控制*/public void multiDeviceLogin() {// 允许多端登录StpUtil.login(userId, "PC");StpUtil.login(userId, "MOBILE");// 踢掉其他端StpUtil.kickout(userId, "PC");}
}
6.2.2 性能优化建议
  1. 合理配置Token过期时间:平衡安全性和用户体验
  2. 使用Redis集群:提高Token存储的可用性和性能
  3. 权限缓存策略:减少数据库查询,提升响应速度
  4. 异步日志记录:避免影响主业务流程性能
  5. 连接池优化:合理配置数据库和Redis连接池参数
6.2.3 安全防护要点
  1. Token安全:使用强加密算法,定期轮换密钥
  2. 传输安全:HTTPS传输,避免Token泄露
  3. 存储安全:Redis密码保护,网络隔离
  4. 访问控制:IP白名单,请求频率限制
  5. 审计监控:完整的操作日志,异常告警机制

6.3 技术发展趋势

6.3.1 云原生权限管理

随着云原生技术的发展,权限管理也在向云原生方向演进:

# Kubernetes RBAC集成示例
apiVersion: v1
kind: ConfigMap
metadata:name: satoken-config
data:application.yml: |sa-token:token-name: satokentimeout: 2592000is-concurrent: truetoken-style: uuid# 云原生配置jwt:secret-key: ${JWT_SECRET:default-secret}redis:host: ${REDIS_HOST:redis-service}port: ${REDIS_PORT:6379}password: ${REDIS_PASSWORD:}
6.3.2 零信任安全架构

零信任安全模型要求对每个请求都进行验证:

/*** 零信任安全验证*/
@Component
public class ZeroTrustValidator {public boolean validateRequest(HttpServletRequest request) {// 1. 身份验证if (!StpUtil.isLogin()) {return false;}// 2. 设备验证if (!validateDevice(request)) {return false;}// 3. 网络验证if (!validateNetwork(request)) {return false;}// 4. 行为验证if (!validateBehavior(request)) {return false;}return true;}
}
6.3.3 AI驱动的智能权限

未来的权限系统将更加智能化:

/*** AI智能权限推荐*/
@Service
public class IntelligentPermissionService {/*** 基于用户行为的权限推荐*/public List<String> recommendPermissions(String userId) {// 分析用户历史行为UserBehavior behavior = analyzeUserBehavior(userId);// 机器学习模型预测List<String> recommendations = mlModel.predict(behavior);return recommendations;}/*** 异常行为检测*/public boolean detectAnomalousAccess(String userId, String resource) {// 获取用户正常访问模式AccessPattern normalPattern = getUserAccessPattern(userId);// 当前访问行为AccessBehavior currentBehavior = getCurrentBehavior(userId, resource);// AI模型检测异常return anomalyDetectionModel.isAnomalous(normalPattern, currentBehavior);}
}

6.4 扩展阅读与学习资源

6.4.1 官方文档与社区
  • SA-Token官方文档
  • GitHub仓库
  • Gitee仓库
6.4.2 相关技术书籍
  1. 《Spring Security实战》- 深入理解Spring Security权限框架
  2. 《OAuth 2.0实战》- 掌握OAuth2.0协议和实现
  3. 《微服务安全架构与实践》- 微服务环境下的安全设计
  4. 《Redis实战》- 深入学习Redis在权限系统中的应用
6.4.3 在线学习资源
  • 慕课网:SA-Token实战课程
  • 极客时间:权限系统设计专栏
  • B站:SA-Token作者孔明老师的视频教程
  • 掘金社区:SA-Token技术文章和实践分享

6.5 实践练习与思考

6.5.1 动手实践项目

为了更好地掌握SA-Token,建议完成以下实践项目:

  1. 基础项目:构建一个简单的用户管理系统

    • 用户注册、登录、注销
    • 基本的权限控制
    • 会话管理
  2. 进阶项目:开发企业级权限管理平台

    • RBAC权限模型
    • 动态权限配置
    • 多租户支持
  3. 高级项目:微服务权限网关

    • 统一认证中心
    • 服务间鉴权
    • 分布式会话管理
6.5.2 思考讨论题
  1. 架构设计:如何在大型分布式系统中设计高可用的权限服务?
  2. 性能优化:面对百万级用户的权限验证,如何优化性能?
  3. 安全防护:如何防范权限系统面临的各种安全威胁?
  4. 技术选型:SA-Token与Spring Security的适用场景对比?
6.5.3 开源贡献

鼓励读者参与SA-Token开源社区建设:

  • 提交Bug报告和功能建议
  • 贡献代码和文档
  • 分享使用经验和最佳实践
  • 帮助其他开发者解决问题

6.6 结语

SA-Token作为一个优秀的权限认证框架,以其简洁、强大、灵活的特性,为Java开发者提供了一个高效的权限管理解决方案。通过本文的深入学习,相信读者已经掌握了SA-Token的核心技术和实践应用。

在实际项目开发中,权限管理不仅仅是技术问题,更是业务安全的重要保障。希望读者能够结合具体的业务场景,灵活运用SA-Token的各种特性,构建安全、高效、易维护的权限系统。

技术在不断发展,权限管理领域也在持续演进。保持学习的热情,关注技术发展趋势,积极参与开源社区,是每个技术人员成长的必经之路。

最后,感谢SA-Token开源团队的辛勤付出,感谢所有为权限管理技术发展做出贡献的开发者们。让我们一起推动Java权限认证技术的发展,为构建更安全的软件系统而努力!


如果这篇文章对你有帮助,请不要忘记点赞👍、收藏⭐、分享📤!你的支持是我创作的最大动力!

有任何问题欢迎在评论区讨论,我会及时回复大家!让我们一起在技术的道路上不断前行! 🚀

http://www.dtcms.com/a/519440.html

相关文章:

  • 【CMakeLists.txt】Qt6 依赖配置详解
  • 用js做网站登录网页成品
  • 数据库安全网关:从“看得见访问”到“控得住风险”的关键一层
  • 对泊松过程的理解
  • 【数论】质数筛(埃氏筛、欧拉筛)
  • 扩展名网站兰州做网站一咨询兰州做网站公司
  • 华为OD-Java面经-21届考研
  • Excel拆分和合并优化版本
  • 智能网联汽车:当汽车遇上“智慧网络”
  • 常规点光源在工业视觉检测上的应用
  • C++新特性——正则表达式
  • 基于卷积神经网络的汽车类型识别系统,resnet50,vgg16,resnet34【pytorch框架,python代码】
  • 设计 企业网站电脑系统网站建设
  • 做网站业务的怎么找资源网站推广名片
  • FPGA强化- HDMI显示器驱动设计与验证
  • 【PPT-ungroup PPT解组合,python无水印】
  • Java 17 环境下 EasyPoi 反射访问异常分析与解决方案(ExcelImportUtil.importExcelMore)
  • SpringBoot+alibaba的easyexcel实现前端使用excel表格批量插入
  • 重大更新,LVGL有UI编辑器用了
  • 多场景 VR 教学编辑器:重构教学流程的场景化实践
  • 公司做网站让我们销售单页面网站模板怎么做
  • 广州微网站建设价位广东网站建设公司
  • 基于 Spring AI Alibaba + Nacos 的分布式 Multi-Agent 构建指南
  • 《与幽灵作战:Python 棘手 Bug 的调试策略与实战技巧》
  • 使用Requests和lxml实现飞卢小说网小说爬取
  • bug 记录 - 路由守卫 beforeRouteLeave 与 confirm 结合,不生效问题
  • 数据库字段类型bit容易被忽视的bug
  • centos 配置网络
  • [人工智能-大模型-55]:模型层技术 - AI的算法、数据结构中算法、逻辑处理的算法异同
  • LeetCode 3461.判断操作后字符串中的数字是否相等 I:简单题简单做的时候到了