整合SaToken 实现登录功能
1.整合redis
1.1添加相关依赖
// 省略...<!-- Redis --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- Redis 连接池 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency>// 省略...
1.2添加配置
spring:datasource:// 省略...data:redis:database: 0 # Redis 数据库索引(默认为 0)host: 127.0.0.1 # Redis 服务器地址port: 6379 # Redis 服务器连接端口password: # Redis 服务器连接密码(默认为空)timeout: 5s # 读超时时间connect-timeout: 5s # 链接超时时间lettuce:pool:max-active: 200 # 连接池最大连接数max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)min-idle: 0 # 连接池中的最小空闲连接max-idle: 10 # 连接池中的最大空闲连接
1.3自定义 RedisTemplate
@Configuration
public class RedisTemplateConfig {@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();// 设置 RedisTemplate 的连接工厂redisTemplate.setConnectionFactory(connectionFactory);// 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值,确保 key 是可读的字符串redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setHashKeySerializer(new StringRedisSerializer());// 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值, 确保存储的是 JSON 格式Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);redisTemplate.setValueSerializer(serializer);redisTemplate.setHashValueSerializer(serializer);redisTemplate.afterPropertiesSet();return redisTemplate;}
}
2.鉴权设计:RBAC 权限模型
什么是 RBAC 模型?
RBAC(Role-Based Access Control)是一种基于角色的访问控制。它通过角色来管理用户的权限。RBAC 的核心思想是将用户与角色进行关联,并将权限分配给角色,而不是直接分配给用户。这样,通过改变用户的角色,就可以灵活地控制用户的权限。
RBAC 的主要组成部分包括:
- 用户(User):系统的使用者。
- 角色(Role):权限的集合,一个角色可以包含多个权限。
- 权限(Permission):对系统资源的访问操作,如读取、写入、删除等。
RBAC 1:基于角色的层次模型(Role Hierarchies)
RBAC 1 在 RBAC 0 的基础上增加了角色层次结构(Role Hierarchies)。角色层次结构允许角色之间存在继承关系,一个角色可以继承另一个角色的权限。
主要特点
- 角色继承:一个角色可以继承另一个角色的所有权限。比如,角色B继承角色 A 的权限,那么角色 B 不仅拥有自己定义的权限,还拥有角色 A 的所有权限。
- 权限传递:继承关系是传递的,如果角色 C 继承角色 B,而角色 B 继承角色 A,那么角色 C 将拥有角色 A 和角色 B 的所有权限。
优点
- 简化权限管理:通过角色继承,可以减少重复定义权限的工作。
- 提高灵活性:可以方便地对角色进行分层管理,满足不同层次用户的权限需求。
场景举例
在一个企业系统中,高级经理(Senior Manager)角色继承经理(Manager)角色的权限,经理角色继承员工(Employee)角色的权限。这样,高级经理角色不仅拥有自己的权限,还拥有经理和员工的所有权限。
RBAC 2:基于约束的 RBAC 模型(Constraints)
RBAC 2 同样建立在 RBAC 0 基础之上,但是增加了约束(Constraints)。约束是用于加强访问控制策略的规则或条件,可以限制用户、角色和权限的关联方式。
主要特点
- 互斥角色:某些角色不能同时赋予同一个用户。例如,审计员和财务员角色不能同时赋予同一个用户,以避免暗黑交易。
- 先决条件:用户要获得某个角色,必须先拥有另一个角色。例如,公司研发人员要成为高级程序员,必须先成为中级程序员。
- 基数约束:限制某个角色可以被赋予的用户数量。例如,某个项目的经理角色只能赋予一个用户,以确保项目的唯一责任人。
优点:
- 加强安全性:通过约束规则,可以避免权限滥用和利益冲突。
- 精细化管理:可以更精细地控制用户的角色分配和权限管理。
场景举例
在一个金融系统中,为了避免利益冲突,定义了互斥角色规则:审计员和财务员角色不能同时赋予同一个用户。这样可以确保审计员和财务员的职责分离,增强系统的安全性。
RBAC 3:统一模型(Consolidated Model)
RBAC 3 是最全面的 RBAC 模型,它结合了 RBAC1 的角色层次结构和 RBAC2 的约束,形成一个统一的模型,提供了最大程度的灵活性和安全性。
主要特点
- 包含RBAC 1的所有功能:角色层次结构,角色继承和权限传递。
- 包含RBAC 2的所有功能:互斥角色、先决条件角色和角色卡数限制等约束规则。
- 综合管理:可以同时利用角色继承和约束规则,提供最全面的权限管理解决方案。
优点
- 高灵活性:可以满足各种复杂的权限管理需求。
- 高安全性:通过约束规则,进一步加强权限管理的安全性。
场景举例
在一个大型企业系统中,需要复杂的权限管理策略。RBAC 3 模型可以通过角色层次结构定义不同层级的员工权限,通过约束规则确保权限分配的安全性。例如,高级经理角色继承经理角色的权限,但为了避免利益冲突,财务员和审计员角色互斥,不能同时赋予同一个用户。
基于 RBAC 的延展:用户组
在实际业务场景中,举个栗子,比如销售部门,分配到此部门的员工都是销售员,拥有同一类角色。如果要为每一个员工手动分配角色,就显得非常繁琐了,而且容易出错。于是乎,在系统设计上,引入了用户组的概念,我们可以把销售部看成一个用户组,对用户组提前分配好角色,这样后续只需将员工拉入该部门,即可拥有该部门已分配的权限。
RBAC 模型是为了更加灵活的控制权限。那么问题来了,需要控制的权限通常都有哪些?
在系统设计时,通常你需要考虑以下几类权限:
- 菜单权限:控制用户在管理后台中,可以看到的菜单项与页面。
- 操作权限:控制用户可以执行的具体操作。比如新增、删除、修改按钮的权限。
- 数据权限:控制用户可以访问的数据范围。比如只能看到本部门的数据,其他部门的员工登录则无法查看。
- 字段权限:控制用户可以查看或编辑的字段。
- 等等…
具体还得结合你的业务来,没有绝对,毕竟技术服务于业务。
3.RBAC 权限表设计、微服务鉴权架构设计
- 角色表;
- 权限表;
- 角色权限关联表;
- 用户角色关联表;
CREATE TABLE `t_role` (`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',`role_name` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色名',`role_key` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色唯一标识',`status` tinyint NOT NULL DEFAULT '0' COMMENT '状态(0:启用 1:禁用)',`sort` int unsigned NOT NULL DEFAULT 0 COMMENT '管理系统中的显示顺序',`remark` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后一次更新时间',`is_deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '逻辑删除(0:未删除 1:已删除)',PRIMARY KEY (`id`) USING BTREE,UNIQUE KEY `uk_role_key` (`role_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表';
CREATE TABLE `t_permission` (`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',`parent_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '父ID',`name` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '权限名称',`type` tinyint unsigned NOT NULL COMMENT '类型(1:目录 2:菜单 3:按钮)',`menu_url` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '菜单路由',`menu_icon` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '菜单图标',`sort` int unsigned NOT NULL DEFAULT 0 COMMENT '管理系统中的显示顺序',`permission_key` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '权限标识',`status` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '状态(0:启用;1:禁用)',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',`is_deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '逻辑删除(0:未删除 1:已删除)',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='权限表';
CREATE TABLE `t_user_role_rel` (`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',`user_id` bigint unsigned NOT NULL COMMENT '用户ID',`role_id` bigint unsigned NOT NULL COMMENT '角色ID',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',`is_deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '逻辑删除(0:未删除 1:已删除)',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户角色表';
CREATE TABLE `t_role_permission_rel` (`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',`role_id` bigint unsigned NOT NULL COMMENT '角色ID',`permission_id` bigint unsigned NOT NULL COMMENT '权限ID',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',`is_deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '逻辑删除(0:未删除 1:已删除)',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户权限表';
鉴权放哪里合适?
关于用户认证(登录),我们已经知道是通过认证服务来处理。那么问题来了,鉴权在哪一层处理呢?通常来说,有以下 3 种方案:
- 每个微服务各自鉴权;
- 网关统一鉴权;
- 混合策略;
鉴权方案 | 优点 | 缺点 |
---|---|---|
1. 每个微服务各自鉴权 | - 每个微服务独立处理鉴权,权限控制更细粒度。 | - 鉴权逻辑重复,增加了维护成本。 - 不利于全局权限管理,可能存在权限漏斗。 - 每个服务需要单独处理认证逻辑,可能影响性能。 |
2. 网关统一鉴权 | - 简化了每个微服务的鉴权逻辑,避免重复实现。 - 可以集中管理认证和授权,减少安全风险。 - 更易于实现权限的统一管理和更新。 | - 网关成了单点故障,性能瓶颈容易产生。 - 如果网关配置出现问题,所有服务的访问都将受限。 - 可能增加网关的复杂度,影响响应速度。 |
3. 混合策略 | - 结合了两者的优点,在网关层进行基础鉴权,细粒度的权限控制在微服务内部处理。 - 灵活性高,可以针对不同场景选择不同策略。 | - 配置和管理较复杂,可能导致维护困难。 - 需要较强的服务协作和协调,增加了系统的复杂度。 |
权限数据获取方案?
权限数据获取方案 | 优点 | 缺点 |
---|---|---|
1. 权限数据存储在数据库中,按需查询 | - 权限数据存储持久化,数据持久性强,修改权限时只需更新数据库。 - 权限数据查询灵活,可以支持复杂的查询条件。 | - 每次请求都需要查询数据库,增加了数据库压力,可能影响性能。 - 权限变化时需要及时同步到微服务或缓存。 |
2. 权限数据缓存到 Redis 中 | - 查询速度更快,减少数据库压力,适用于高并发场景。 - 可以在缓存中存储用户的权限数据,快速获取。 - 缓存过期机制可以保证数据的新鲜度。 | - 数据一致性问题:缓存和数据库之间可能会出现数据不同步的情况。 - 需要额外的缓存管理机制(如缓存失效、更新等)。 |
3. 权限数据静态配置(硬编码到应用中) | - 访问权限数据速度最快,直接嵌入到应用代码中,无需查询数据库或缓存。 - 配置简单,适合权限变化不频繁的场景。 | - 权限管理不灵活,变动时需要重新部署应用。 - 随着权限管理的复杂性增加,代码难以维护。 |
4.SaToken 整合 Redis
添加依赖
// 省略...<!-- 统一依赖管理 --><dependencyManagement><dependencies>// 省略...<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) --><dependency><groupId>cn.dev33</groupId><artifactId>sa-token-redis-jackson</artifactId><version>${sa-token.version}</version></dependency>// 省略...</dependencies></dependencyManagement>// 省略...<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) --><dependency><groupId>cn.dev33</groupId><artifactId>sa-token-redis-jackson</artifactId></dependency><!-- Redis 连接池 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency>// 省略...// 省略...
为 SaToken 权限框架整合了 Redis , 让会话数据存储在了缓存中间件中,以保证项目重启后,登录状态不会失效。
5.同步【角色-权限集合】数据到 Redis 中
在 Spring Boot 项目中,可以通过多种方式在项目启动时执行初始化工作。以下是一些常见的方法:
1. 使用 @PostConstruct
注解
@PostConstruct
注解可以用于在 Spring 容器初始化 bean 之后立即执行特定的方法。
import org.springframework.stereotype.Component;import javax.annotation.PostConstruct;@Component
public class MyInitializer {@PostConstructpublic void init() {// 初始化工作System.out.println("初始化工作完成");}
}
2. 实现 ApplicationRunner
接口
ApplicationRunner
接口提供了一种在 Spring Boot 应用启动后执行特定代码的方式。
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;@Component
public class MyApplicationRunner implements ApplicationRunner {@Overridepublic void run(ApplicationArguments args) throws Exception {// 初始化工作System.out.println("初始化工作完成");}
}
3. 实现 CommandLineRunner
接口
CommandLineRunner
接口类似于 ApplicationRunner
,可以在应用启动后执行代码。
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;@Component
public class MyCommandLineRunner implements CommandLineRunner {@Overridepublic void run(String... args) throws Exception {// 初始化工作System.out.println("初始化工作完成");}
}
4. 使用 @EventListener
注解监听 ApplicationReadyEvent
通过监听 ApplicationReadyEvent
事件,可以在 Spring Boot 应用完全启动并准备好服务请求时执行初始化工作。
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.boot.context.event.ApplicationReadyEvent;@Component
public class MyApplicationReadyListener {@EventListener(ApplicationReadyEvent.class)public void onApplicationReady() {// 初始化工作System.out.println("初始化工作完成");}
}
5. 使用 SmartInitializingSingleton
接口
SmartInitializingSingleton
接口提供了一种在所有单例 bean 初始化完成后执行代码的方式。
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.stereotype.Component;@Component
public class MySmartInitializingSingleton implements SmartInitializingSingleton {@Overridepublic void afterSingletonsInstantiated() {// 初始化工作System.out.println("初始化工作完成");}
}
6. 使用 Spring Boot 的 InitializingBean
接口
通过实现 InitializingBean
接口的 afterPropertiesSet
方法,可以在 bean 的属性设置完成后执行初始化工作。
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;@Component
public class MyInitializingBean implements InitializingBean {@Overridepublic void afterPropertiesSet() throws Exception {// 初始化工作System.out.println("初始化工作完成");}
}
7. 总结
以上这些方法各有优缺点,可以根据具体的初始化需求选择合适的方法。
- @PostConstruct:适合简单的初始化逻辑,执行时机较早。
- ApplicationRunner 和 CommandLineRunner:适合需要访问命令行参数的初始化逻辑,执行时机在 Spring Boot 应用启动完成后。
- ApplicationReadyEvent 监听器:适合在整个应用准备好后执行的初始化逻辑。
- SmartInitializingSingleton:适合需要在所有单例 bean 初始化完成后执行的初始化逻辑。
- InitializingBean:适合需要在 bean 属性设置完成后执行的初始化逻辑。