1、何为鉴权?为何鉴权?
鉴权,也被称为权限控制,是一种在计算机网络中保护用户对特定资源的使用,防止未经授权的访问的技术手段。
它是在信息系统、网络或应用中,确定用户的身份是否合法,并根据用户的身份和权限,决定其在系统中能够访问哪些资源或执行哪些操作的过程。这个过程通常包括身份验证和权限验证两个阶段。
- 身份验证是确定用户是否是系统中的合法用户,常见的鉴权方式包括用户名密码认证、数字证书认证、生物特征认证等。一旦身份验证通过,用户就被视为已经获得了对某些资源的访问权限。
- 授权阶段系统会根据用户的身份和权限,授予其访问和操作特定资源的能力。这种授权通常是由系统管理员或第三方服务进行的,以确保资源的合理分配和使用。
鉴权是一种重要的安全机制,通过合理的身份验证和授权策略,以及采用多种安全措施,可以有效地防止网络攻击和滥用行为,保护网络中的资源不被未经授权的用户使用。
2、权限框架
在Java开发环境非常成熟的当下,框架的产生可谓是日新月异,在近十年的Java开发中,有如下权限框架非常火爆。
2.1、Shior
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。
2.1.1、基本概念
三个核心组件:Subject、 SecurityManager 、 Realms。
- Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。
- SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
- Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
2.1.2、Shior架构图
2.2、Spring Security
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。
Spring Security本质上就是一系列的过滤器。
它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
Spring Security对Web安全性的支持大量地依赖于Servlet过滤器。这些过滤器拦截进入请求,并且在应用程序处理该请求之前进行某些安全处理。 Spring Security提供有若干个过滤器,它们能够拦截Servlet请求,并将这些请求转给认证和访问决策管理器处理,从而增强安全性。根据自己的需要,可以使用适当的过滤器来保护自己的应用程序。
2.2.1、基本概念
1·AuthenticationManager
用于处理用户认证。它是一个身份验证管理器,负责协调和执行身份验证过程。AuthenticationManager接受一个Authentication对象作为输入,并返回一个已经填充了身份验证信息的完整的Authentication对象。
2.DaoAuthenticationProvider
是AuthenticationProvider接口的一个实现类。DaoAuthenticationProvider使用一个UserDetailsService来获取用户的详细信息,并使用PasswordEncoder对用户提供的凭据进行验证。它的主要作用是从数据库或其 他持久化存储中获取用户信息,并进行身份验证。
3.AuthenticationProvider
AuthenticationProvider是一个接口,定义了身份验证提供者的通用行为。它是一个抽象的身份验证机制,可以有多个实现类。DaoAuthenticationProvider就是AuthenticationProvider接口的一个实现类之一。通过实现AuthenticationProvider接口,可以自定义身份验证逻辑。
2.2.2、三者的关系
- AuthenticationManager是一个身份验证管理器,负责协调和执行身份验证过程。
- DaoAuthenticationProvider是AuthenticationProvider接口的一个实现类,用于从数据库或其他持久化存储中获取用户信息并进行身份验证。
- AuthenticationProvider是一个接口,定义了身份验证提供者的通用行为,可以有多个实现类。
2.2.3、SpringSecurity架构图
2.3、Sa-Token
Sa-Token官网:https://sa-token.cc/doc.html#/
Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权 等一系列权限相关问题。
2.3.1、集成方式
SpringBoot 2.x
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.37.0</version>
</dependency>
SpringBoot 3.x
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.37.0</version>
</dependency>
分布式
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot-starter</artifactId>
<version>1.37.0</version>
</dependency>
2.3.2、基本配置
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: satoken
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
is-share: true
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
token-style: uuid
# 是否输出操作日志
is-log: true
2.3.3、分布式下的鉴权方案
本文主要记录Sa-Token在分布式Gateway环境下的鉴权方式,想进一步了解Sa-Token请看官网。
2.3.3.1、数据库的设计
用户表
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码',
`phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '手机号',
`sex` int NOT NULL DEFAULT '0' COMMENT '0男,1女,2其他',
`avatar` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '头像',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '签名',
`identify_card` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '身份证号',
`status` bit(1) DEFAULT NULL COMMENT '当前用户是否被冻结',
`travel_official` bit(1) DEFAULT b'0' COMMENT '是否进行了官方认证',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `PHONE_INDEX` (`phone`) USING BTREE,
UNIQUE KEY `IDENTIFY_CARD_INDEX` (`identify_card`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=91706 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC;
角色表
CREATE TABLE `roles` (
`id` int NOT NULL AUTO_INCREMENT,
`role_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`description` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
角色用户映射表
CREATE TABLE `user_role_relationship` (
`user_id` int NOT NULL,
`role_id` int NOT NULL,
PRIMARY KEY (`user_id`,`role_id`) USING BTREE,
UNIQUE KEY `USER_ROLE_INDEX` (`user_id`,`role_id`) USING BTREE,
KEY `ROLE_ID` (`role_id`) USING BTREE,
CONSTRAINT `ROLE_ID` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `USER_ID` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
菜单表
CREATE TABLE `menu` (
`id` int NOT NULL AUTO_INCREMENT,
`menu_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`menu_path` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`need_login` bit(1) NOT NULL DEFAULT b'0',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
菜单角色映射表
CREATE TABLE `role_menu_relationship` (
`roles_id` int NOT NULL,
`menu_id` int NOT NULL,
PRIMARY KEY (`roles_id`,`menu_id`) USING BTREE,
UNIQUE KEY `ROLE_MENU_INDEX` (`roles_id`,`menu_id`) USING BTREE,
KEY `MENU_INDEX` (`menu_id`) USING BTREE,
CONSTRAINT `MENU_INDEX` FOREIGN KEY (`menu_id`) REFERENCES `menu` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `ROLE_INDEX` FOREIGN KEY (`roles_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
2.3.3.2、Gateway配置
在系统中需要Sa-Token整合Redis实现登录持久化记录,所以需要引入:sa-token-redis-jackson
、commons-pool2
两个依赖。
官网解释:https://sa-token.cc/doc.html#/up/integ-redis
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.37.0</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
2.3.3.3、Yaml配置
spring:
redis:
database: 0
host: 127.0.0.1
port: 6379
timeout: 10s
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
sa-token:
token-name: authentication
timeout: 604800
token-style: simple-uuid
is-share: true
is-concurrent: true
is-log: false
is-read-cookie: false
is-read-head: true
2.3.3.4、内置的Token生成规则
- uuid:uuid风格 (默认风格)
623368f0-ae5e-4475-a53f-93e4225f16ae
- simple-uuid:同上,uuid风格, 只不过去掉了中划线
6fd4221395024b5f87edd34bc3258ee8
- random-32 :随机32位字符串
qEjyPsEA1Bkc9dr8YP6okFr5umCZNR6W
- random-64:随机64位字符串
v4ueNLEpPwMtmOPMBtOOeIQsvP8z9gkMgIVibTUVjkrNrlfra5CGwQkViDjO8jcc
- random-128:随机128位字符串
nojYPmcEtrFEaN0Otpssa8I8jpk8FO53UcMZkCP9qyoHaDbKS6dxoRPky9c6QlftQ0pdzxRGXsKZmUSrPeZBOD6kJFfmfgiRyUmYWcj4WU4SSP2ilakWN1HYnIuX0Olj
- tik:tik风格
gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__
2.3.3.5、自定义SaReactorFilter
@Configuration
@RequiredArgsConstructor
public class SaTokenConfig {
@Lazy
@Resource
private UserFeign userFeign;
private final RedisTemplate<String, Object> redisTemplate;
@Bean
public SaReactorFilter getSaReactorFilter() {
return new SaReactorFilter()
.addInclude("/**")
.setAuth(obj -> {
String travelId = SaHolder.getRequest().getHeader("authentication");
Object cache = redisTemplate.opsForValue()
.get(SystemProperties.USER_INFO_PREFIX + StpUtil.getLoginIdByToken(travelId));
if (travelId != null && cache == null) {
throw new RuntimeException();
}
executorMenuPath();
})
.setError(e -> {
e.printStackTrace();
ServerWebExchange exchange = SaReactorSyncHolder.getContext();
exchange.getResponse().getHeaders().set("Content-Type", "application/json; charset=utf-8");
return SaResult.get(HttpStatus.BAD_REQUEST.value(), "请先进行登录", null);
})
// 跨域信息
.setBeforeAuth(obj -> {
SaHolder.getResponse()
.setHeader("Access-Control-Allow-Origin", "*")
.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
.setHeader("Access-Control-Max-Age", "3600")
.setHeader("Access-Control-Allow-Headers", "*");
SaRouter.match(SaHttpMethod.OPTIONS).back();
});
}
// 进行权限判断
private void executorMenuPath(){
Result<Map<String, List<Menu>>> systemMenu = userFeign.allMenu();
if(systemMenu.getCode() != HttpStatus.OK.value()) return;
Map<String, Menu> path = new HashMap<>();
systemMenu.getBody().forEach((key, value) -> {
value.forEach(menu -> path.put(menu.getMenuPath(), menu));
});
Menu menu = path.get(SaHolder.getRequest().getRequestPath());
if(menu.isNeedLogin()) StpUtil.checkLogin();
}
}
2.3.3.6、实现StpInterface接口
StpInterface接口是用来获取用户所拥有的权限和可访问路径的接口。
Sa-Token的鉴权方式就是会调用这个接口的实现类。
@Component
public class StpInterceptorImpl implements StpInterface {
@Resource
@Lazy
private UserFeign userFeign;
// 获取当前用户可以访问的路径
@Override
public List<String> getPermissionList(Object o, String s) {
Result<List<Menu>> response = userFeign.userAccessMenuByRoles();
if(response.getCode() == HttpStatus.OK.value()){
return response.getBody().stream().map(Menu::getMenuPath).toList();
}
return Collections.emptyList();
}
// 获取当前用户的所有角色
@Override
public List<String> getRoleList(Object o, String s) {
Result<List<Role>> response = userFeign.roleList();
if(response.getCode() == HttpStatus.OK.value()){
return response.getBody().stream().map(Role::getRoleName).toList();
}
return Collections.emptyList();
}
}
2.3.3.7、注意事项
- 其实Sa-Token和Security很像,本质上都是一系列过滤器和调用链,如果在Gateway上使用Sa-Token,那么一定要配置跨域,因为Sa-Token会在Gateway之前对请求进行拦截,放行后才会到达Gateway。
StpUtil.checkLogin()
和StpUtil.checkRole()
方法都是有顺序的,不能直接对系统中的所有路径进行配置,而是需要对当前请求的路径做配置。- 在Gateway启动的时候不能做阻塞的任务,不然Gateway无法启动。
3、三大框架的比较
3.1、Shior
Shior正在逐步退出国内市场,原因有如下几个:
- Shior的登录地址默认是login.jsp,这在前后端分离的模式下需要重写许多类,这在分布式项目中可能会带来一些麻烦。
- 对于保持用户登录状态等需求,Shior也并不适合处理。
- 在分布式项目中,如电商项目,通常不需要明确的权限划分,使用Shior进行权限管理可能会过于复杂。同时,Shior对于SpringCloud等各种分布式框架来说,可能也是一场“灾难”,每个子系统可能都需要编写一些Shior的东西,这会逐渐变得繁琐并增加开发难度。
3.2、SpringSecurity
Spring Security 并没有逐渐退出国内市场,在国内外市场,Spring Security 的使用仍然非常广泛。许多大型企业和关键基础设施都在使用 Spring Security 来保护其应用程序和数据。同时,Spring Security 也在不断进行更新和改进,以适应不断变化的安全威胁和需求。
3.3、Sa-Token
阿里开源的轻量级权限框架,伴随简便的开发模式在国内越来越流行。
使用Sa-Token的理由主要有以下几点:
- 轻量级框架:Sa-Token是一个轻量级的Java权限认证框架,相对于其他框架来说,它更加简洁、易用,并且可以快速地实现用户身份验证、权限控制、会话管理等功能。
- 集成简单:Sa-Token的集成非常简单,只需要在项目中添加相关的依赖,就可以快速地实现权限认证功能。
- API设计优雅:Sa-Token的API设计非常优雅,它提供了简单易用的接口,使得开发者可以非常方便地实现权限认证功能。
- 丰富的功能:Sa-Token不仅提供了基本的权限认证功能,还支持单点登录、OAuth2.0、微服务网关鉴权等一系列权限相关问题。
- 高性能:Sa-Token采用了高效的认证和授权机制,可以快速地处理大量的用户认证和授权请求,提高了系统的性能。
- 广泛的应用场景:Sa-Token可以应用于各种需要权限认证的场景,如企业应用、Web应用、移动应用等。
最为后端开发者我个人是非常喜欢Sa-Token的简单开发模式。😘😁💖