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

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

文章目录

在这里插入图片描述

引言

在现代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();// 获取当前用户IDObject userId =StpUtil.getLoginId();// 注销登录StpUtil.logout();
1.1.2 SA-Token的设计理念

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

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

1.2 SA-Token核心特性详解

在这里插入图片描述
1.2.1 登录认证特性

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

/** * 登录认证核心特性演示 */@ServicepublicclassAuthService{/** * 基础登录功能 */publicvoidbasicLogin(Long userId){// 基础登录StpUtil.login(userId);// 指定设备登录StpUtil.login(userId,"PC");// 登录并指定Token有效期(单位:秒)StpUtil.login(userId,3600);// 登录时携带扩展信息StpUtil.login(userId,newSaLoginModel().setDevice("mobile").setTimeout(7200).setIsLastingCookie(true));}/** * 会话查询功能 */publicvoidsessionQuery(){// 获取当前登录用户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());}/** * 登录状态检查 */publicvoidloginCheck(){// 检查当前是否登录,如未登录则抛出异常StpUtil.checkLogin();// 检查当前是否登录,返回boolean值boolean isLogin =StpUtil.isLogin();// 检查指定用户是否登录boolean userLogin =StpUtil.isLogin(10001);// 检查当前Token是否有效boolean isValid =StpUtil.getTokenInfo().getIsLogin();}/** * 注销登录功能 */publicvoidlogout(){// 注销当前用户登录StpUtil.logout();// 注销指定用户登录StpUtil.logout(10001);// 注销指定用户在指定设备的登录StpUtil.logout(10001,"PC");// 踢掉指定用户下线StpUtil.kickout(10001);// 踢掉指定用户在指定设备下线StpUtil.kickout(10001,"mobile");}}
1.2.2 权限认证特性

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

/** * 权限认证特性演示 */@ServicepublicclassPermissionService{/** * 权限校验 */publicvoidpermissionCheck(){// 检查当前用户是否拥有指定权限StpUtil.checkPermission("user:add");// 检查当前用户是否拥有指定权限,返回booleanboolean hasPermission =StpUtil.hasPermission("user:delete");// 检查当前用户是否拥有指定权限列表中的任意一个StpUtil.checkPermissionOr("user:add","user:edit","user:delete");// 检查当前用户是否拥有指定权限列表中的所有权限StpUtil.checkPermissionAnd("user:add","role:add");}/** * 角色校验 */publicvoidroleCheck(){// 检查当前用户是否拥有指定角色StpUtil.checkRole("admin");// 检查当前用户是否拥有指定角色,返回booleanboolean hasRole =StpUtil.hasRole("admin");// 检查当前用户是否拥有指定角色列表中的任意一个StpUtil.checkRoleOr("admin","manager","operator");// 检查当前用户是否拥有指定角色列表中的所有角色StpUtil.checkRoleAnd("admin","manager");}/** * 获取权限和角色信息 */publicvoidgetPermissionInfo(){// 获取当前用户的权限列表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提供了强大的会话管理功能,支持会话存储、会话共享、会话监听等特性。

/** * 会话管理特性演示 */@ServicepublicclassSessionService{/** * Session存储操作 */publicvoidsessionStorage(){// 获取当前用户的Session对象SaSession session =StpUtil.getSession();// 在Session中存储数据 session.set("username","张三"); session.set("email","[email protected]"); 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");// 清空Session session.clear();// 获取Session的剩余存活时间long timeout = session.getTimeout();// 修改Session的存活时间 session.updateTimeout(3600);}/** * Token-Session双Token模式 */publicvoidtokenSessionMode(){// 获取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操作 */publicvoidcustomSession(){// 获取自定义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核心组件架构演示 */publicclassSaTokenArchitecture{/** * 1. StpLogic - 权限认证逻辑核心 */publicvoidstpLogicDemo(){// StpLogic是SA-Token的核心逻辑类// 所有的登录、权限校验等操作都通过StpLogic实现// 获取默认的StpLogic实例StpLogic stpLogic =StpUtil.stpLogic;// 使用StpLogic进行登录 stpLogic.login(10001);// 使用StpLogic进行权限校验 stpLogic.checkPermission("user:add");// 自定义StpLogic实现多账户体系StpLogic adminLogic =newStpLogic("admin");StpLogic userLogic =newStpLogic("user");// 管理员登录 adminLogic.login(1001);// 普通用户登录 userLogic.login(2001);}/** * 2. SaTokenDao - 数据持久化接口 */publicvoidsaTokenDaoDemo(){// SaTokenDao负责Token和Session的持久化// 默认实现:SaTokenDaoDefaultImpl(基于内存)// 获取当前使用的Dao实例SaTokenDao dao =SaManager.getSaTokenDao();// 存储Token dao.set("token:abc123","user:10001",3600);// 获取Token对应的值String value = dao.get("token:abc123");// 删除Token dao.delete("token:abc123");// 获取Token剩余存活时间long timeout = dao.getTimeout("token:abc123");// 修改Token存活时间 dao.updateTimeout("token:abc123",7200);}/** * 3. SaTokenConfig - 全局配置类 */publicvoidsaTokenConfigDemo(){// 获取全局配置对象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 - 策略模式接口 */publicvoidsaStrategyDemo(){// SA-Token使用策略模式来处理各种业务逻辑// 自定义Token生成策略SaStrategy.me.createToken =(loginId, loginType)->{return"custom-token-"+ loginId +"-"+System.currentTimeMillis();};// 自定义Session生成策略SaStrategy.me.createSession =(sessionId)->{returnnewSaSession(sessionId);};// 自定义权限验证失败处理策略SaStrategy.me.notPermission =(loginType, permission)->{thrownewSaTokenException("权限不足:"+ permission);};// 自定义角色验证失败处理策略SaStrategy.me.notRole =(loginType, role)->{thrownewSaTokenException("角色不足:"+ role);};}}
1.3.2 扩展机制设计

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

/** * SA-Token扩展机制演示 */publicclassSaTokenExtension{/** * 自定义权限数据源 */@ComponentpublicclassCustomStpInterfaceimplementsStpInterface{@AutowiredprivateUserService userService;@AutowiredprivateRoleService roleService;@AutowiredprivatePermissionService permissionService;/** * 返回指定用户的权限列表 */@OverridepublicList<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());}/** * 返回指定用户的角色列表 */@OverridepublicList<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持久化实现 */@ComponentpublicclassCustomSaTokenDaoimplementsSaTokenDao{@AutowiredprivateRedisTemplate<String,Object> redisTemplate;@OverridepublicStringget(String key){return(String) redisTemplate.opsForValue().get(key);}@Overridepublicvoidset(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);}}@Overridepublicvoidupdate(String key,String value){long expire =getTimeout(key);if(expire ==SaTokenDao.NOT_VALUE_EXPIRE){return;}this.set(key, value, expire);}@Overridepublicvoiddelete(String key){ redisTemplate.delete(key);}@OverridepubliclonggetTimeout(String key){Long expire = redisTemplate.getExpire(key);return expire ==null?SaTokenDao.NOT_VALUE_EXPIRE : expire;}@OverridepublicvoidupdateTimeout(String key,long timeout){ redisTemplate.expire(key, timeout,TimeUnit.SECONDS);}@OverridepublicObjectgetObject(String key){return redisTemplate.opsForValue().get(key);}@OverridepublicvoidsetObject(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);}}@OverridepublicvoidupdateObject(String key,Object object){long expire =getObjectTimeout(key);if(expire ==SaTokenDao.NOT_VALUE_EXPIRE){return;}this.setObject(key, object, expire);}@OverridepubliclonggetObjectTimeout(String key){Long expire = redisTemplate.getExpire(key);return expire ==null?SaTokenDao.NOT_VALUE_EXPIRE : expire;}@OverridepublicvoidupdateObjectTimeout(String key,long timeout){ redisTemplate.expire(key, timeout,TimeUnit.SECONDS);}@OverridepublicList<String>searchData(String prefix,String keyword,int start,int size,boolean sortType){// 实现数据搜索逻辑Set<String> keys = redisTemplate.keys(prefix +"*"+ keyword +"*");List<String> list =newArrayList<>(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()){returnnewArrayList<>();}return list.subList(fromIndex, toIndex);}}}

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

在这里插入图片描述

2.1 项目环境搭建

2.1.1 Maven依赖配置

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

<?xml version="1.0" encoding="UTF-8"?><projectxmlns="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.Driver url: jdbc:mysql://localhost:3306/satoken_demo?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8username: root password:123456# JPA配置jpa:hibernate:ddl-auto: update show-sql:trueproperties:hibernate:format_sql:true# Redis配置redis:host: localhost port:6379password:database:0timeout: 10000ms lettuce:pool:max-active:8max-wait:-1ms max-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: debug org.springframework.web: debug pattern:console:"%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
2.1.3 数据库表结构设计

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

-- 用户表CREATETABLE`sys_user`(`id`bigintNOTNULLAUTO_INCREMENTCOMMENT'用户ID',`username`varchar(50)NOTNULLCOMMENT'用户名',`password`varchar(100)NOTNULLCOMMENT'密码',`nickname`varchar(50)DEFAULTNULLCOMMENT'昵称',`email`varchar(100)DEFAULTNULLCOMMENT'邮箱',`phone`varchar(20)DEFAULTNULLCOMMENT'手机号',`avatar`varchar(200)DEFAULTNULLCOMMENT'头像',`status`tinyintDEFAULT'1'COMMENT'状态:0-禁用,1-启用',`create_time`datetimeDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',`update_time`datetimeDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',PRIMARYKEY(`id`),UNIQUEKEY`uk_username`(`username`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='用户表';-- 角色表CREATETABLE`sys_role`(`id`bigintNOTNULLAUTO_INCREMENTCOMMENT'角色ID',`role_code`varchar(50)NOTNULLCOMMENT'角色编码',`role_name`varchar(50)NOTNULLCOMMENT'角色名称',`description`varchar(200)DEFAULTNULLCOMMENT'角色描述',`status`tinyintDEFAULT'1'COMMENT'状态:0-禁用,1-启用',`create_time`datetimeDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',`update_time`datetimeDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',PRIMARYKEY(`id`),UNIQUEKEY`uk_role_code`(`role_code`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='角色表';-- 权限表CREATETABLE`sys_permission`(`id`bigintNOTNULLAUTO_INCREMENTCOMMENT'权限ID',`permission_code`varchar(100)NOTNULLCOMMENT'权限编码',`permission_name`varchar(100)NOTNULLCOMMENT'权限名称',`resource_type`varchar(20)DEFAULTNULLCOMMENT'资源类型:menu-菜单,button-按钮',`url`varchar(200)DEFAULTNULLCOMMENT'资源路径',`method`varchar(10)DEFAULTNULLCOMMENT'请求方法',`parent_id`bigintDEFAULT'0'COMMENT'父权限ID',`sort_order`intDEFAULT'0'COMMENT'排序',`status`tinyintDEFAULT'1'COMMENT'状态:0-禁用,1-启用',`create_time`datetimeDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',`update_time`datetimeDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',PRIMARYKEY(`id`),UNIQUEKEY`uk_permission_code`(`permission_code`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='权限表';-- 用户角色关联表CREATETABLE`sys_user_role`(`id`bigintNOTNULLAUTO_INCREMENTCOMMENT'主键ID',`user_id`bigintNOTNULLCOMMENT'用户ID',`role_id`bigintNOTNULLCOMMENT'角色ID',`create_time`datetimeDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',PRIMARYKEY(`id`),UNIQUEKEY`uk_user_role`(`user_id`,`role_id`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='用户角色关联表';-- 角色权限关联表CREATETABLE`sys_role_permission`(`id`bigintNOTNULLAUTO_INCREMENTCOMMENT'主键ID',`role_id`bigintNOTNULLCOMMENT'角色ID',`permission_id`bigintNOTNULLCOMMENT'权限ID',`create_time`datetimeDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',PRIMARYKEY(`id`),UNIQUEKEY`uk_role_permission`(`role_id`,`permission_id`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='角色权限关联表';-- 插入测试数据INSERTINTO`sys_user`(`username`,`password`,`nickname`,`email`,`status`)VALUES('admin','$2a$10$7JB720yubVSOfvVMe6/YqO4wkhWGEn4bJJnNpSn0kfzOLuTOQHHiq','系统管理员','[email protected]',1),('user','$2a$10$7JB720yubVSOfvVMe6/YqO4wkhWGEn4bJJnNpSn0kfzOLuTOQHHiq','普通用户','[email protected]',1);INSERTINTO`sys_role`(`role_code`,`role_name`,`description`,`status`)VALUES('admin','系统管理员','拥有系统所有权限',1),('user','普通用户','拥有基础权限',1);INSERTINTO`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');INSERTINTO`sys_user_role`(`user_id`,`role_id`)VALUES(1,1),(2,2);INSERTINTO`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@EnableConfigurationPropertiespublicclassSaTokenConfig{/** * 获取StpInterface权限认证接口的实现类 */@BeanpublicStpInterfacestpInterface(){returnnewStpInterfaceImpl();}/** * SA-Token全局异常处理 */@BeanpublicGlobalExceptionHandlerglobalExceptionHandler(){returnnewGlobalExceptionHandler();}/** * SA-Token拦截器配置 */@ConfigurationpublicstaticclassSaTokenInterceptorConfigimplementsWebMvcConfigurer{@OverridepublicvoidaddInterceptors(InterceptorRegistry registry){// 注册Sa-Token拦截器,校验规则为StpUtil.checkLogin()登录校验 registry.addInterceptor(newSaInterceptor(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@PrimarypublicSaJsonTemplatesaJsonTemplate(){returnnewSaJsonTemplateForFastjson();}}
2.2.2 权限认证接口实现
/** * 自定义权限验证接口扩展 */@ComponentpublicclassStpInterfaceImplimplementsStpInterface{@AutowiredprivateUserService userService;@AutowiredprivateRoleService roleService;@AutowiredprivatePermissionService permissionService;/** * 返回一个账号所拥有的权限码集合 */@OverridepublicList<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);returnCollections.emptyList();}}/** * 返回一个账号所拥有的角色标识集合 */@OverridepublicList<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);returnCollections.emptyList();}}}
2.2.3 全局异常处理器
/** * 全局异常处理器 */@RestControllerAdvice@Slf4jpublicclassGlobalExceptionHandler{/** * 拦截:未登录异常 */@ExceptionHandler(NotLoginException.class)publicResulthandleNotLoginException(NotLoginException e){String message ="";// 判断场景值,定制化异常信息switch(e.getType()){caseNotLoginException.NOT_TOKEN: message ="未提供Token";break;caseNotLoginException.INVALID_TOKEN: message ="Token无效";break;caseNotLoginException.TOKEN_TIMEOUT: message ="Token已过期";break;caseNotLoginException.BE_REPLACED: message ="Token已被顶下线";break;caseNotLoginException.KICK_OUT: message ="Token已被踢下线";break;default: message ="当前会话未登录";break;} log.warn("用户未登录访问受保护资源: {}", message);returnResult.error(401, message);}/** * 拦截:缺少权限异常 */@ExceptionHandler(NotPermissionException.class)publicResulthandleNotPermissionException(NotPermissionException e){ log.warn("用户权限不足, 缺少权限: {}", e.getPermission());returnResult.error(403,"权限不足,缺少权限:"+ e.getPermission());}/** * 拦截:缺少角色异常 */@ExceptionHandler(NotRoleException.class)publicResulthandleNotRoleException(NotRoleException e){ log.warn("用户角色不足, 缺少角色: {}", e.getRole());returnResult.error(403,"角色不足,缺少角色:"+ e.getRole());}/** * 拦截:禁用账号异常 */@ExceptionHandler(DisableServiceException.class)publicResulthandleDisableServiceException(DisableServiceException e){ log.warn("账号被禁用, 禁用服务: {}, 禁用级别: {}, 禁用时间: {}秒", e.getService(), e.getLevel(), e.getDisableTime());returnResult.error(423,"账号已被禁用:"+ e.getDisableTime()+"秒后解封");}/** * 拦截:二级认证异常 */@ExceptionHandler(NotSafeException.class)publicResulthandleNotSafeException(NotSafeException e){ log.warn("二级认证失败: {}", e.getMessage());returnResult.error(901,"请完成二级认证:"+ e.getMessage());}/** * 拦截:服务封禁异常 */@ExceptionHandler(SaTokenException.class)publicResulthandleSaTokenException(SaTokenException e){ log.error("SA-Token异常: {}", e.getMessage(), e);returnResult.error(500,"系统异常:"+ e.getMessage());}/** * 拦截:其他所有异常 */@ExceptionHandler(Exception.class)publicResulthandleException(Exception e){ log.error("系统异常: {}", e.getMessage(), e);returnResult.error(500,"系统繁忙,请稍后重试");}}

2.3 实体类和数据访问层

2.3.1 实体类定义
/** * 用户实体类 */@Entity@Table(name ="sys_user")@Data@NoArgsConstructor@AllArgsConstructor@BuilderpublicclassUser{@Id@GeneratedValue(strategy =GenerationType.IDENTITY)privateLong id;@Column(unique =true, nullable =false, length =50)privateString username;@Column(nullable =false, length =100)privateString password;@Column(length =50)privateString nickname;@Column(length =100)privateString email;@Column(length =20)privateString phone;@Column(length =200)privateString avatar;@Column(columnDefinition ="TINYINT DEFAULT 1")privateInteger status;@CreationTimestamp@Column(name ="create_time")privateLocalDateTime createTime;@UpdateTimestamp@Column(name ="update_time")privateLocalDateTime updateTime;// 多对多关联角色@ManyToMany(fetch =FetchType.LAZY)@JoinTable( name ="sys_user_role", joinColumns =@JoinColumn(name ="user_id"), inverseJoinColumns =@JoinColumn(name ="role_id"))privateSet<Role> roles =newHashSet<>();}/** * 角色实体类 */@Entity@Table(name ="sys_role")@Data@NoArgsConstructor@AllArgsConstructor@BuilderpublicclassRole{@Id@GeneratedValue(strategy =GenerationType.IDENTITY)privateLong id;@Column(unique =true, nullable =false, length =50)privateString roleCode;@Column(nullable =false, length =50)privateString roleName;@Column(length =200)privateString description;@Column(columnDefinition ="TINYINT DEFAULT 1")privateInteger status;@CreationTimestamp@Column(name ="create_time")privateLocalDateTime createTime;@UpdateTimestamp@Column(name ="update_time")privateLocalDateTime updateTime;// 多对多关联权限@ManyToMany(fetch =FetchType.LAZY)@JoinTable( name ="sys_role_permission", joinColumns =@JoinColumn(name ="role_id"), inverseJoinColumns =@JoinColumn(name ="permission_id"))privateSet<Permission> permissions =newHashSet<>();}/** * 权限实体类 */@Entity@Table(name ="sys_permission")@Data@NoArgsConstructor@AllArgsConstructor@BuilderpublicclassPermission{@Id@GeneratedValue(strategy =GenerationType.IDENTITY)privateLong id;@Column(unique =true, nullable =false, length =100)privateString permissionCode;@Column(nullable =false, length =100)privateString permissionName;@Column(length =20)privateString resourceType;@Column(length =200)privateString url;@Column(length =10)privateString method;@Column(columnDefinition ="BIGINT DEFAULT 0")privateLong parentId;@Column(columnDefinition ="INT DEFAULT 0")privateInteger sortOrder;@Column(columnDefinition ="TINYINT DEFAULT 1")privateInteger status;@CreationTimestamp@Column(name ="create_time")privateLocalDateTime createTime;@UpdateTimestamp@Column(name ="update_time")privateLocalDateTime updateTime;}
2.3.2 数据访问层实现
/** * 用户数据访问接口 */@RepositorypublicinterfaceUserRepositoryextendsJpaRepository<User,Long>{/** * 根据用户名查找用户 */Optional<User>findByUsername(String username);/** * 根据用户名和状态查找用户 */Optional<User>findByUsernameAndStatus(String username,Integer status);/** * 检查用户名是否存在 */booleanexistsByUsername(String username);/** * 检查邮箱是否存在 */booleanexistsByEmail(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);}/** * 角色数据访问接口 */@RepositorypublicinterfaceRoleRepositoryextendsJpaRepository<Role,Long>{/** * 根据角色编码查找角色 */Optional<Role>findByRoleCode(String roleCode);/** * 根据状态查找角色列表 */List<Role>findByStatus(Integer status);/** * 检查角色编码是否存在 */booleanexistsByRoleCode(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);}/** * 权限数据访问接口 */@RepositorypublicinterfacePermissionRepositoryextendsJpaRepository<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@Slf4jpublicclassUserServiceImplimplementsUserService{@AutowiredprivateUserRepository userRepository;@AutowiredprivateRoleRepository roleRepository;@AutowiredprivatePasswordEncoder passwordEncoder;/** * 用户认证 */@OverridepublicUserauthenticate(String username,String password){ log.debug("用户认证开始, username: {}", username);// 查找用户Optional<User> userOpt = userRepository.findByUsernameAndStatus(username,1);if(!userOpt.isPresent()){ log.warn("用户不存在或已被禁用, username: {}", username);returnnull;}User user = userOpt.get();// 验证密码if(!passwordEncoder.matches(password, user.getPassword())){ log.warn("用户密码错误, username: {}", username);returnnull;} log.info("用户认证成功, userId: {}, username: {}", user.getId(), username);return user;}/** * 根据ID获取用户 */@Override@Transactional(readOnly =true)publicUsergetById(Long id){return userRepository.findById(id).orElse(null);}/** * 根据用户名获取用户 */@Override@Transactional(readOnly =true)publicUsergetByUsername(String username){return userRepository.findByUsername(username).orElse(null);}/** * 创建用户 */@OverridepublicUsercreateUser(UserCreateRequest request){ log.info("创建用户开始, username: {}", request.getUsername());// 检查用户名是否已存在if(userRepository.existsByUsername(request.getUsername())){thrownewBusinessException("用户名已存在");}// 检查邮箱是否已存在if(StringUtils.hasText(request.getEmail())&& userRepository.existsByEmail(request.getEmail())){thrownewBusinessException("邮箱已存在");}// 创建用户对象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;}/** * 更新用户信息 */@OverridepublicUserupdateUser(Long userId,UserUpdateRequest request){ log.info("更新用户信息开始, userId: {}", userId);User user = userRepository.findById(userId).orElseThrow(()->newBusinessException("用户不存在"));// 更新基本信息if(StringUtils.hasText(request.getNickname())){ user.setNickname(request.getNickname());}if(StringUtils.hasText(request.getEmail())){// 检查邮箱是否已被其他用户使用if(userRepository.existsByEmail(request.getEmail())&&!request.getEmail().equals(user.getEmail())){thrownewBusinessException("邮箱已被其他用户使用");} 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;}/** * 修改密码 */@OverridepublicvoidchangePassword(Long userId,String oldPassword,String newPassword){ log.info("修改用户密码开始, userId: {}", userId);User user = userRepository.findById(userId).orElseThrow(()->newBusinessException("用户不存在"));// 验证旧密码if(!passwordEncoder.matches(oldPassword, user.getPassword())){thrownewBusinessException("原密码错误");}// 更新密码 user.setPassword(passwordEncoder.encode(newPassword)); userRepository.save(user); log.info("用户密码修改成功, userId: {}", userId);}/** * 分配角色 */@OverridepublicvoidassignRoles(Long userId,List<Long> roleIds){ log.info("分配用户角色开始, userId: {}, roleIds: {}", userId, roleIds);User user = userRepository.findById(userId).orElseThrow(()->newBusinessException("用户不存在"));// 清除现有角色 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());}/** * 启用/禁用用户 */@OverridepublicvoidupdateUserStatus(Long userId,Integer status){ log.info("更新用户状态开始, userId: {}, status: {}", userId, status);User user = userRepository.findById(userId).orElseThrow(()->newBusinessException("用户不存在")); user.setStatus(status); userRepository.save(user);// 如果是禁用用户,则踢下线if(status ==0){StpUtil.kickout(userId); log.info("用户已被踢下线, userId: {}", userId);} log.info("用户状态更新成功, userId: {}, status: {}", userId, status);}/** * 删除用户 */@OverridepublicvoiddeleteUser(Long userId){ log.info("删除用户开始, userId: {}", userId);User user = userRepository.findById(userId).orElseThrow(()->newBusinessException("用户不存在"));// 踢下线StpUtil.kickout(userId);// 删除用户 userRepository.delete(user); log.info("用户删除成功, userId: {}", userId);}/** * 获取用户列表 */@Override@Transactional(readOnly =true)publicList<User>getAllUsers(){return userRepository.findAll();}/** * 分页获取用户列表 */@Override@Transactional(readOnly =true)publicPage<User>getUserPage(Pageable pageable){return userRepository.findAll(pageable);}}
2.4.2 角色服务实现
/** * 角色服务实现类 */@Service@Transactional@Slf4jpublicclassRoleServiceImplimplementsRoleService{@AutowiredprivateRoleRepository roleRepository;@AutowiredprivatePermissionRepository permissionRepository;/** * 根据用户ID获取角色列表 */@Override@Transactional(readOnly =true)publicList<String>getRolesByUserId(Long userId){List<Role> roles = roleRepository.findByUserId(userId);return roles.stream().map(Role::getRoleCode).collect(Collectors.toList());}/** * 创建角色 */@OverridepublicRolecreateRole(RoleCreateRequest request){ log.info("创建角色开始, roleCode: {}", request.getRoleCode());// 检查角色编码是否已存在if(roleRepository.existsByRoleCode(request.getRoleCode())){thrownewBusinessException("角色编码已存在");}// 创建角色对象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;}/** * 分配权限 */@OverridepublicvoidassignPermissions(Long roleId,List<Long> permissionIds){ log.info("分配角色权限开始, roleId: {}, permissionIds: {}", roleId, permissionIds);Role role = roleRepository.findById(roleId).orElseThrow(()->newBusinessException("角色不存在"));// 清除现有权限 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@Slf4jpublicclassPermissionServiceImplimplementsPermissionService{@AutowiredprivatePermissionRepository permissionRepository;/** * 根据用户ID获取权限列表 */@Override@Transactional(readOnly =true)publicList<String>getPermissionsByUserId(Long userId){List<Permission> permissions = permissionRepository.findByUserId(userId);return permissions.stream().map(Permission::getPermissionCode).collect(Collectors.toList());}/** * 获取所有权限 */@Override@Transactional(readOnly =true)publicList<Permission>getAllPermissions(){return permissionRepository.findByStatus(1);}/** * 构建权限树 */@Override@Transactional(readOnly =true)publicList<PermissionTreeNode>buildPermissionTree(){List<Permission> allPermissions =getAllPermissions();// 构建权限树Map<Long,PermissionTreeNode> nodeMap =newHashMap<>();List<PermissionTreeNode> rootNodes =newArrayList<>();// 创建所有节点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(newArrayList<>()).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;}privatevoidsortPermissionTree(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")@Slf4jpublicclassAuthController{@AutowiredprivateUserService userService;@AutowiredprivateCaptchaService captchaService;/** * 用户登录 */@PostMapping("/login")publicResultlogin(@RequestBody@ValidLoginRequest request){ log.info("用户登录请求, username: {}", request.getUsername());// 验证验证码if(!captchaService.verifyCaptcha(request.getCaptchaKey(), request.getCaptchaCode())){returnResult.error("验证码错误");}// 用户认证User user = userService.authenticate(request.getUsername(), request.getPassword());if(user ==null){returnResult.error("用户名或密码错误");}// 检查用户状态if(user.getStatus()!=1){returnResult.error("账号已被禁用");}// 执行登录StpUtil.login(user.getId(),newSaLoginModel().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());returnResult.success(response);}/** * 用户注销 */@PostMapping("/logout")publicResultlogout(){Object loginId =StpUtil.getLoginId();StpUtil.logout(); log.info("用户注销成功, userId: {}", loginId);returnResult.success("注销成功");}/** * 获取当前用户信息 */@GetMapping("/userinfo")publicResultgetUserInfo(){// 检查登录状态StpUtil.checkLogin();// 获取当前用户IDLong userId =StpUtil.getLoginIdAsLong();// 查询用户信息User user = userService.getById(userId);if(user ==null){returnResult.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();returnResult.success(response);}/** * 刷新Token */@PostMapping("/refresh")publicResultrefreshToken(){// 检查登录状态StpUtil.checkLogin();// 续签TokenStpUtil.renewTimeout(7200);// 续签2小时// 获取新的Token信息SaTokenInfo tokenInfo =StpUtil.getTokenInfo();returnResult.success(tokenInfo);}/** * 获取验证码 */@GetMapping("/captcha")publicResultgetCaptcha(){CaptchaResponse captcha = captchaService.generateCaptcha();returnResult.success(captcha);}}
3.1.2 验证码服务实现
/** * 验证码服务实现 */@Service@Slf4jpublicclassCaptchaServiceImplimplementsCaptchaService{@AutowiredprivateRedisTemplate<String,String> redisTemplate;privatestaticfinalString CAPTCHA_PREFIX ="captcha:";privatestaticfinalint CAPTCHA_EXPIRE_TIME =300;// 5分钟privatestaticfinalint CAPTCHA_LENGTH =4;/** * 生成验证码 */@OverridepublicCaptchaResponsegenerateCaptcha(){// 生成验证码keyString captchaKey = UUID.randomUUID().toString();// 生成验证码内容String captchaCode =generateRandomCode(CAPTCHA_LENGTH);// 生成验证码图片String captchaImage =generateCaptchaImage(captchaCode);// 存储到Redis redisTemplate.opsForValue().set( CAPTCHA_PREFIX + captchaKey, captchaCode.toLowerCase(), CAPTCHA_EXPIRE_TIME,TimeUnit.SECONDS ); log.debug("生成验证码, key: {}, code: {}", captchaKey, captchaCode);returnCaptchaResponse.builder().captchaKey(captchaKey).captchaImage(captchaImage).expireTime(CAPTCHA_EXPIRE_TIME).build();}/** * 验证验证码 */@OverridepublicbooleanverifyCaptcha(String captchaKey,String captchaCode){if(!StringUtils.hasText(captchaKey)||!StringUtils.hasText(captchaCode)){returnfalse;}String redisKey = CAPTCHA_PREFIX + captchaKey;String storedCode = redisTemplate.opsForValue().get(redisKey);if(storedCode ==null){ log.warn("验证码已过期或不存在, key: {}", captchaKey);returnfalse;}// 验证后删除验证码 redisTemplate.delete(redisKey);boolean isValid = storedCode.equalsIgnoreCase(captchaCode); log.debug("验证码校验结果, key: {}, code: {}, result: {}", captchaKey, captchaCode, isValid);return isValid;}privateStringgenerateRandomCode(int length){String chars ="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";StringBuilder sb =newStringBuilder();Random random =newRandom();for(int i =0; i < length; i++){ sb.append(chars.charAt(random.nextInt(chars.length())));}return sb.toString();}privateStringgenerateCaptchaImage(String code){// 创建图片int width =120;int height =40;BufferedImage image =newBufferedImage(width, height,BufferedImage.TYPE_INT_RGB);Graphics2D g = image.createGraphics();// 设置背景色 g.setColor(Color.WHITE); g.fillRect(0,0, width, height);// 设置字体 g.setFont(newFont("Arial",Font.BOLD,20));// 绘制验证码Random random =newRandom();for(int i =0; i < code.length(); i++){ g.setColor(newColor(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(newColor(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 =newByteArrayOutputStream();ImageIO.write(image,"png", baos);byte[] imageBytes = baos.toByteArray();return"data:image/png;base64,"+Base64.getEncoder().encodeToString(imageBytes);}catch(IOException e){ log.error("生成验证码图片失败", e);returnnull;}}} ## 第四章:高级特性与扩展应用 ### 4.1 单点登录(SSO)实现 #### 4.1.1 SSO基础配置 SA-Token提供了强大的单点登录功能,支持同域和跨域的SSO实现。 ```java /** * SSO配置类 */@ConfigurationpublicclassSsoConfig{/** * SSO相关配置 */@Bean@ConfigurationProperties(prefix ="sa-token.sso")publicSaSsoConfiggetSaSsoConfig(){returnnewSaSsoConfig()// 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")@Slf4jpublicclassSsoServerController{/** * SSO统一认证页面 */@GetMapping("/auth")publicSaResultauth(String redirect,String mode,HttpServletRequest request){ log.info("SSO统一认证,redirect={}, mode={}", redirect, mode);// 如果已经登录,则直接重定向到Client端if(StpUtil.isLogin()){returnSaSsoUtil.buildRedirectUrl(StpUtil.getLoginId(), redirect);}// 未登录,显示登录页面returnSaResult.ok().setData(buildLoginPage(redirect, mode));}/** * 处理登录请求 */@PostMapping("/doLogin")publicSaResultdoLogin(String username,String password,String redirect){ log.info("SSO登录处理,username={}, redirect={}", username, redirect);// 验证用户名密码if(validateUser(username, password)){// 登录成功,生成ticket并重定向StpUtil.login(username);returnSaSsoUtil.buildRedirectUrl(username, redirect);}returnSaResult.error("用户名或密码错误");}/** * 校验ticket */@GetMapping("/checkTicket")publicSaResultcheckTicket(String ticket,String ssoLogoutCall){ log.info("校验ticket,ticket={}, ssoLogoutCall={}", ticket, ssoLogoutCall);// 校验ticket,获取账号idObject loginId =SaSsoUtil.checkTicket(ticket);if(loginId !=null){// 注册此客户端的单点注销回调URLSaSsoUtil.registerClient(loginId, ssoLogoutCall);returnSaResult.data(loginId);}returnSaResult.error("无效的ticket");}/** * 单点注销 */@GetMapping("/signout")publicSaResultsignout(String loginId,String secretkey){ log.info("SSO单点注销,loginId={}", loginId);// 校验秘钥SaSsoUtil.checkSecretkey(secretkey);// 遍历通知所有Client端注销SaSsoUtil.singleLogout(loginId);returnSaResult.ok("单点注销成功");}/** * 验证用户凭据 */privatebooleanvalidateUser(String username,String password){// 这里应该连接数据库验证用户信息// 为了演示,简单验证return"admin".equals(username)&&"123456".equals(password);}/** * 构建登录页面HTML */privateStringbuildLoginPage(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> <h2>统一认证中心</h2> <form action="/sso/doLogin" method="post"> <input type="hidden" name="redirect" value="%s"> <div> <input type="text" name="username" placeholder="用户名" required> </div> <div> <input type="password" name="password" placeholder="密码" required> </div> <div> <button type="submit">登录</button> </div> </form> </div> </body> </html> """.formatted(redirect !=null? redirect :"");}}
4.1.3 SSO-Client端实现
/** * SSO客户端控制器 */@RestController@RequestMapping("/sso")@Slf4jpublicclassSsoClientController{/** * 首页 */@GetMapping("/")publicSaResultindex(){String loginId =(String)StpUtil.getLoginIdDefaultNull();if(loginId !=null){returnSaResult.ok("欢迎用户:"+ loginId);}returnSaResult.ok("当前未登录").setData("<a href='/sso/login'>点击登录</a>");}/** * 发起登录 */@GetMapping("/login")publicSaResultlogin(String back){// 构建授权地址String authUrl =SaSsoUtil.buildAuthUrl(); log.info("重定向到SSO认证中心:{}", authUrl);returnSaResult.ok().setData("redirect:"+ authUrl);}/** * SSO登录回调 */@GetMapping("/login/callback")publicSaResultloginCallback(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);returnSaResult.ok("登录成功").setData("用户ID:"+ loginId);}returnSaResult.error("登录失败");}/** * 单点注销回调 */@GetMapping("/logoutCall")publicSaResultlogoutCall(String loginId,String secretkey){ log.info("收到单点注销回调,loginId={}", loginId);// 校验秘钥SaSsoUtil.checkSecretkey(secretkey);// 注销当前用户StpUtil.logout(loginId);returnSaResult.ok("注销成功");}/** * 查询登录状态 */@GetMapping("/isLogin")publicSaResultisLogin(){boolean isLogin =StpUtil.isLogin();Object loginId =StpUtil.getLoginIdDefaultNull();returnSaResult.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配置类 */@ConfigurationpublicclassOAuth2Config{/** * OAuth2配置 */@Bean@ConfigurationProperties(prefix ="sa-token.oauth2")publicSaOAuth2Configoauth2Config(){returnnewSaOAuth2Config()// 是否打开模式:授权码(Authorization Code).setIsCode(true)// 是否打开模式:隐藏式(Implicit).setIsImplicit(true)// 是否打开模式:密码式(Password).setIsPassword(true)// 是否打开模式:客户端凭证(Client Credentials).setIsClient(true)// 是否在每次Refresh-Token刷新Access-Token时,产生一个新的Refresh-Token.setIsNewRefresh(true);}/** * OAuth2数据加载器 */@ComponentpublicstaticclassOAuth2DataLoaderimplementsSaOAuth2DataLoader{@AutowiredprivateClientService clientService;@AutowiredprivateUserService userService;/** * 根据 client_id 获取 Client 信息 */@OverridepublicSaClientModelgetClientModel(String clientId){// 从数据库查询客户端信息Client client = clientService.getByClientId(clientId);if(client ==null){returnnull;}returnnewSaClientModel().setClientId(client.getClientId()).setClientSecret(client.getClientSecret()).setAllowUrl(client.getAllowUrl()).setContractScope(client.getContractScope()).setIsAutoMode(client.getIsAutoMode());}/** * 根据 ClientId 和 LoginId 获取openid */@OverridepublicStringgetOpenid(String clientId,Object loginId){// 可以根据 clientId 和 loginId 生成openidreturnDigestUtils.md5Hex(clientId +":"+ loginId);}/** * 校验:指定 LoginId 是否对指定 Client 授权给定 Scope */@OverridepublicbooleanisGrant(Object loginId,String clientId,String scope){// 查询用户是否已授权return userService.hasGranted(String.valueOf(loginId), clientId, scope);}/** * 保存:指定 LoginId 对指定 Client 授权给定 Scope */@OverridepublicvoidsaveGrant(Object loginId,String clientId,String scope){// 保存用户授权信息 userService.saveGrant(String.valueOf(loginId), clientId, scope);}}}

4.3 微服务网关鉴权

4.3.1 网关鉴权配置

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

/** * 网关鉴权配置 */@Configuration@EnableWebFluxSecuritypublicclassGatewayAuthConfig{/** * 注册Sa-Token全局过滤器 */@BeanpublicSaReactorFiltergetSaReactorFilter(){returnnewSaReactorFilter()// 拦截地址.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 ->{returnSaResult.error(e.getMessage());});}/** * 配置跨域 */@BeanpublicCorsWebFiltercorsFilter(){CorsConfiguration config =newCorsConfiguration(); config.addAllowedMethod("*"); config.addAllowedOrigin("*"); config.addAllowedHeader("*");UrlBasedCorsConfigurationSource source =newUrlBasedCorsConfigurationSource(newPathPatternParser()); source.registerCorsConfiguration("/**", config);returnnewCorsWebFilter(source);}}

4.4 多账户体系支持

4.4.1 多账户配置

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

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

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

5.1 性能优化策略

5.1.1 Token存储优化

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

/** * Token存储优化配置 */@ConfigurationpublicclassTokenStorageOptimization{/** * Redis连接池优化 */@BeanpublicLettuceConnectionFactoryredisConnectionFactory(){// 连接池配置GenericObjectPoolConfig<StatefulRedisConnection<String,String>> poolConfig =newGenericObjectPoolConfig<>(); 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 =newRedisStandaloneConfiguration(); serverConfig.setHostName("localhost"); serverConfig.setPort(6379); serverConfig.setDatabase(0);returnnewLettuceConnectionFactory(serverConfig, clientConfig);}/** * 自定义Token存储实现 */@ComponentpublicclassOptimizedSaTokenDaoimplementsSaTokenDao{@AutowiredprivateRedisTemplate<String,Object> redisTemplate;privatefinalString TOKEN_PREFIX ="satoken:";@OverridepublicStringget(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);returnnull;}}@Overridepublicvoidset(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);}}@Overridepublicvoidupdate(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);}}@Overridepublicvoiddelete(String key){try{ redisTemplate.delete(TOKEN_PREFIX + key);}catch(Exception e){ log.error("Redis删除Token失败,key={}", key, e);}}@OverridepubliclonggetTimeout(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;}}@OverridepublicvoidupdateTimeout(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@Slf4jpublicclassPermissionCacheService{@AutowiredprivateRedisTemplate<String,Object> redisTemplate;@AutowiredprivateUserService userService;privatestaticfinalString PERMISSION_CACHE_PREFIX ="permission:";privatestaticfinalString ROLE_CACHE_PREFIX ="role:";privatestaticfinalint CACHE_EXPIRE_SECONDS =3600;// 1小时/** * 获取用户权限列表(带缓存) */publicList<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);}}/** * 获取用户角色列表(带缓存) */publicList<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);}}/** * 清除用户权限缓存 */publicvoidclearUserPermissionCache(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);}}/** * 批量预热权限缓存 */@AsyncpublicvoidpreloadPermissionCache(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安全增强配置 */@ConfigurationpublicclassTokenSecurityConfig{/** * 自定义Token生成策略 */@BeanpublicSaTokenActionsaTokenAction(){returnnewSaTokenAction(){@OverridepublicStringcreateToken(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加密returnAESUtil.encrypt(tokenData,getTokenSecret());}@OverridepublicObjectgetLoginIdByToken(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){thrownewSaTokenException("Token已过期");}return loginId;}}catch(Exception e){ log.error("Token解析失败", e);}returnnull;}};}/** * Token签名密钥 */privateStringgetTokenSecret(){// 从配置文件或环境变量获取密钥return"your-secret-key-here";}/** * IP白名单验证 */@ComponentpublicclassIpWhitelistValidator{privatefinalSet<String> whitelistIps =newHashSet<>();@PostConstructpublicvoidinit(){// 从配置文件加载IP白名单 whitelistIps.add("127.0.0.1"); whitelistIps.add("192.168.1.0/24");}publicbooleanisAllowed(String clientIp){return whitelistIps.contains(clientIp)||isInSubnet(clientIp);}privatebooleanisInSubnet(String clientIp){// 实现子网匹配逻辑for(String subnet : whitelistIps){if(subnet.contains("/")&&matchSubnet(clientIp, subnet)){returntrue;}}returnfalse;}privatebooleanmatchSubnet(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]){returnfalse;}}// 检查剩余位if(bitsToCheck >0&& bytesToCheck < targetBytes.length){int mask =0xFF<<(8- bitsToCheck);return(targetBytes[bytesToCheck]& mask)==(networkBytes[bytesToCheck]& mask);}returntrue;}catch(Exception e){ log.error("子网匹配失败", e);returnfalse;}}}}
5.2.2 防攻击策略
/** * 防攻击策略实现 */@Component@Slf4jpublicclassSecurityDefenseService{@AutowiredprivateRedisTemplate<String,Object> redisTemplate;privatestaticfinalString LOGIN_ATTEMPT_PREFIX ="login_attempt:";privatestaticfinalString RATE_LIMIT_PREFIX ="rate_limit:";privatestaticfinalint MAX_LOGIN_ATTEMPTS =5;privatestaticfinalint LOGIN_LOCK_DURATION =300;// 5分钟privatestaticfinalint RATE_LIMIT_REQUESTS =100;privatestaticfinalint RATE_LIMIT_WINDOW =60;// 1分钟/** * 检查登录尝试次数 */publicbooleancheckLoginAttempts(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);returnfalse;}returntrue;}catch(Exception e){ log.error("检查登录尝试次数失败", e);returntrue;// 异常时允许登录}}/** * 记录登录失败 */publicvoidrecordLoginFailure(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);}}/** * 清除登录失败记录 */publicvoidclearLoginFailures(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);}}/** * 检查请求频率限制 */publicbooleancheckRateLimit(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);returnfalse;}// 增加请求计数if(requests ==null){ redisTemplate.opsForValue().set(key,1,Duration.ofSeconds(RATE_LIMIT_WINDOW));}else{ redisTemplate.opsForValue().increment(key);}returntrue;}catch(Exception e){ log.error("检查请求频率限制失败", e);returntrue;// 异常时允许请求}}/** * SQL注入检测 */publicbooleandetectSqlInjection(String input){if(input ==null|| input.isEmpty()){returnfalse;}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);returntrue;}}returnfalse;}/** * XSS攻击检测 */publicbooleandetectXssAttack(String input){if(input ==null|| input.isEmpty()){returnfalse;}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);returntrue;}}returnfalse;}}

5.3 监控与日志

5.3.1 认证监控
/** * 认证监控服务 */@Service@Slf4jpublicclassAuthMonitorService{@AutowiredprivateMeterRegistry meterRegistry;@AutowiredprivateRedisTemplate<String,Object> redisTemplate;privatefinalCounter loginSuccessCounter;privatefinalCounter loginFailureCounter;privatefinalCounter logoutCounter;privatefinalTimer authenticationTimer;publicAuthMonitorService(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);}/** * 记录登录成功 */publicvoidrecordLoginSuccess(String userId,String clientIp,String userAgent){ loginSuccessCounter.increment();// 记录详细日志 log.info("用户登录成功 - userId={}, clientIp={}, userAgent={}", userId, clientIp, userAgent);// 记录登录历史recordLoginHistory(userId, clientIp, userAgent,true);// 更新在线用户统计updateOnlineUserStats(userId,true);}/** * 记录登录失败 */publicvoidrecordLoginFailure(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);}/** * 记录注销 */publicvoidrecordLogout(String userId,String clientIp){ logoutCounter.increment(); log.info("用户注销 - userId={}, clientIp={}", userId, clientIp);// 更新在线用户统计updateOnlineUserStats(userId,false);}/** * 记录认证耗时 */publicvoidrecordAuthenticationTime(Duration duration){ authenticationTimer.record(duration);}/** * 记录登录历史 */privatevoidrecordLoginHistory(String userId,String clientIp,String userAgent,boolean success){try{LoginHistory history =newLoginHistory(); 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);}}/** * 更新在线用户统计 */privatevoidupdateOnlineUserStats(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);}}/** * 获取认证统计信息 */publicAuthStatisticsgetAuthStatistics(){try{AuthStatistics stats =newAuthStatistics();// 从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);returnnewAuthStatistics();}}/** * 保存登录历史(实际实现) */privatevoidsaveLoginHistory(LoginHistory history){// 实际的数据库保存逻辑 log.debug("保存登录历史:{}", history);}}
5.3.2 审计日志
/** * 审计日志服务 */@Service@Slf4jpublicclassAuditLogService{@AutowiredprivateRedisTemplate<String,Object> redisTemplate;@EventListenerpublicvoidhandleLoginEvent(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);}@EventListenerpublicvoidhandlePermissionCheckEvent(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);}/** * 保存审计日志 */privatevoidsaveAuditLog(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);}}/** * 查询审计日志 */publicList<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);returnCollections.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技术架构核心组件回顾 */publicclassSaTokenArchitectureReview{/** * 1. 核心组件 */// StpUtil - 权限认证工具类// SaTokenDao - Token存储接口// SaTokenConfig - 框架配置类// StpInterface - 权限数据接口/** * 2. 认证流程 */publicvoidauthenticationFlow(){// 用户登录 -> 生成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开发最佳实践 */@ComponentpublicclassSaTokenBestPractices{/** * 1. 统一异常处理 */@ExceptionHandler(NotLoginException.class)publicSaResulthandleNotLoginException(NotLoginException e){returnSaResult.error("请先登录").setCode(401);}/** * 2. 权限注解使用 */@SaCheckPermission("user:list")@GetMapping("/users")publicSaResultgetUsers(){// 业务逻辑returnSaResult.ok();}/** * 3. 会话管理 */publicvoidsessionManagement(){// 设置会话数据StpUtil.getSession().set("userInfo", userInfo);// 获取会话数据UserInfo info =(UserInfo)StpUtil.getSession().get("userInfo");// 清理会话StpUtil.getSession().clear();}/** * 4. 多端登录控制 */publicvoidmultiDeviceLogin(){// 允许多端登录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: satoken timeout: 2592000 is-concurrent: true token-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 零信任安全架构

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

/** * 零信任安全验证 */@ComponentpublicclassZeroTrustValidator{publicbooleanvalidateRequest(HttpServletRequest request){// 1. 身份验证if(!StpUtil.isLogin()){returnfalse;}// 2. 设备验证if(!validateDevice(request)){returnfalse;}// 3. 网络验证if(!validateNetwork(request)){returnfalse;}// 4. 行为验证if(!validateBehavior(request)){returnfalse;}returntrue;}}
6.3.3 AI驱动的智能权限

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

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

6.4 扩展阅读与学习资源

6.4.1 官方文档与社区
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权限认证技术的发展,为构建更安全的软件系统而努力!


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

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

Read more

Flutter 第三方库 spa 的鸿蒙适配实战 - 打造单页应用架构、动态渲染路由状态及鸿蒙大屏多窗体验优化方案

Flutter 第三方库 spa 的鸿蒙适配实战 - 打造单页应用架构、动态渲染路由状态及鸿蒙大屏多窗体验优化方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 第三方库 spa 的鸿蒙适配实战 - 打造单页应用架构、动态渲染路由状态及鸿蒙大屏多窗体验优化方案 前言 随着移动端交互的日益复杂,用户对 App 的流畅度要求已不仅仅停留在“帧率”上,更多的是关于页面切换的“无缝感”。单页应用(Single Page Application, SPA)模式,通过在一个长生命周期的视图内动态替换内容节点,有效地避免了频繁的页面推栈(Push/Pop)带来的布局重绘开销。 spa 库是 Flutter 生态中一个非常独特且高效的路由增强工具。它将路由状态抽象为一套可观察的树状结构,让开发者能像管理 Web 应用一样管理 Flutter 的页面状态。 在鸿蒙系统(OpenHarmony)适配实战中,面对折叠屏的灵活切换和平板的多窗协同,spa 提供了一种天然的“响应式分发”基座。

By Ne0inhk
尚硅谷2025最新SpringCloud速通-操作步骤(详细)

尚硅谷2025最新SpringCloud速通-操作步骤(详细)

说明:本文是基于【雷丰阳老师:尚硅谷2025最新SpringCloud - 快速通关】进行实践操作,并对雷神的笔记做一个更详细的补充,供大家学习参考,一起加油! 视频地址:1、SpringCloud快速通关_教程简介_哔哩哔哩_bilibili 笔记链接:3. SpringCloud - 快速通关 资料:📎资料.zip(代码+课件+逻辑图) 本人代码:📎springcloud-demo.zip 用于测试API接口的工具:Apipost IDEA自动提示代码插件:通义灵码 目录 目录 springcloud简介 前期准备 建springcloud-demo项目 导依赖 建services模块 导入依赖 建service-order/product模块 nacos - 注册/配置中心 基础入门 注册中心

By Ne0inhk

Clawdbot部署Qwen3:32B实操:解决‘gateway token missing’的三种Token注入方式对比

Clawdbot部署Qwen3:32B实操:解决‘gateway token missing’的三种Token注入方式对比 Clawdbot 是一个统一的 AI 代理网关与管理平台,旨在为开发者提供一个直观的界面来构建、部署和监控自主 AI 代理。通过集成的聊天界面、多模型支持和强大的扩展系统,Clawdbot 让 AI 代理的管理变得简单高效。 当你在 ZEEKLOG 星图镜像广场一键部署 Clawdbot 并集成本地运行的 qwen3:32b 模型后,大概率会遇到这样一个提示: disconnected (1008): unauthorized: gateway token missing (open a tokenized dashboard URL or paste token in Control UI settings) 这不是报错,也不是服务没起来—

By Ne0inhk
Flutter 组件 powersync_attachments_helper 的适配 鸿蒙Harmony 实战 - 驾驭分布式附件同步、实现鸿蒙端大文件离线存储与生命周期自动化管理方案

Flutter 组件 powersync_attachments_helper 的适配 鸿蒙Harmony 实战 - 驾驭分布式附件同步、实现鸿蒙端大文件离线存储与生命周期自动化管理方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 powersync_attachments_helper 的适配 鸿蒙Harmony 实战 - 驾驭分布式附件同步、实现鸿蒙端大文件离线存储与生命周期自动化管理方案 前言 在鸿蒙(OpenHarmony)生态的分布式多媒体协作、工业设备故障图片上报以及需要频繁处理大量音频/视频附件的专业级应用开发中,“非结构化数据与 SQL 逻辑的一致性同步”是决定应用能否在大规模复杂场景下存活的技术深水区。面对一条已经同步成功的“设备巡检记录”。如果其关联的“高清故障原图”因为同步时机错位、由于存储空间不足导致的本地缓存被回收,或者是在鸿蒙手机与平板之间由于同步策略不同步导致的文件路径失效。那么不仅会导致用户在查看详情时看到令人沮丧的“附件丢失”占位图,更会严重削弱政务类资产审计的底层严密性。 我们需要一种“逻辑关联、物理对齐”的附件治理艺术。 powersync_attachments_helper 是一套专为 PowerSync 设计的附件同步

By Ne0inhk