1.概述
上文写到了SpringSecurity从入门到放弃之JWT认证登陆(一),该文章介绍了spring security的登陆原理以及整合JWT的登陆案例,本文将基于上文,介绍spring securtity整合RBAC权限模型,实现按权访问接口。
2.Spring Security整合RBAC权限模式
2.1 RBAC模型原理
RBAC(Role-Based Access Control):基于角色的权限访问控制。它的核心在于用户只和角色关联,而角色代表了权限,是一系列权限的集合。RBAC的核心元素包括:用户、角色、权限。
- 用户:系统中所有的账户
- 角色:一系列权限的集合(如:管理员,普通用户)
- 权限:菜单,按钮,数据的增删改查等详细权限。
在RBAC中,权限与角色相关联,用户被分配成为适当角色下的成员而获得对应角色的权限。角色是为了完成不同的工作而被创建,用户则依据它的责任来被分配相应的角色,用户能被关联一个甚至多个角色,且能完成从一个角色向另一个角色的变更。角色可以根据场景或其他元素的变更,来被赋予新的权限,角色的权限亦能根据场景被收回。 用户、角色、权限之间主要联系如下图所示: RBAC模型可以分为:RBAC 0、RBAC 1、RBAC 2、RBAC 3 四个阶段,一般公司使用RBAC0的模型就可以。RBAC 1、RBAC 2、RBAC 3都是基于RBAC 0上的改良版本。本文主要介绍RBAC 0模型:用户与角色、角色与权限都是多对多的关系。如下图所示: RBAC模型的本质就是根据场景设计不同的角色,用角色来关联不同权限(菜单),最后不同的用户根据场景关联对应的角色即可。任何模型都是要根据不同的场景进行设计,不能简单设计,也不能过度设计。
2.2 spring security权限控制的几种方式
在spring security中,对接口的拦截或放行,主要有以下四种权限控制方式:
1.利用Ant表达式实现权限控制; 2.利用授权注解结合SpEL表达式实现权限控制; 3.利用过滤器注解实现权限控制; 4.利用动态权限实现权限控制。
2.2.1 Security Expression Operations
在Spring Security中,SecurityExpressionOperations接口定义了一系列方法用于用户权限的设置,如下所示: 这些方法的作用分别如下:
方法 | 作用 |
---|
hasRole | 用户具备某个角色即可访问 | hasAnyRole | 用户具备多个角色中一个即可访问 | permitAll | 所有请求均允许访问 | denyAll | 所有请求都拒绝访问 | hasPermission | 用户具备某个权限即可访问 | isFullyAuthenticated | 判断是否用户名/密码登陆 | isRememberMe | 判断是否通过记住我功能登陆 | isAnonymous | 判断是否匿名登陆 | isAuthenticated | 允许认证通过用户访问 | hasAuthority | 判断是否拥有权限,类似于hasRole | hasAnyAuthority | 判断是否拥有某一权限,类似于hasAnyRole |
使用方法如下:
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login/user").anonymous()
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated();
http.csrf().disable();
}
2.2.2 利用授权注解结合SpEL表达式实现权限控制
Spring Security提供了方法注解来进行权限控制,常用的授权注解如下:
@PreAuthorize:在方法执行前进行权限检查; @PostAuthorize:在方法执行后进行权限检查; @Secured:类似于 @PreAuthorize。
使用方式如下: 1.首先利用@EnableGlobalMethodSecurity注解开启授权注解功能 2.在方法上开启注解进行权限控制 上述这种方式相对较为灵活,唯一的缺陷是代码耦合度较高。
2.2.3 利用过滤器注解实现权限控制
在Spring Security中还提供了另外的两个注解,即@PreFilter和@PostFilter,这两个注解可以对集合类型的参数或返回值进行过滤。使用@PreFilter和@PostFilter时,Spring Security将移除对应表达式结果为false的元素。 1.@PreFilter的用法 使用@PreFilter也可以对集合类型的参数进行过滤,当@PreFilter标注的方法内拥有多个集合类型的参数时,可以通过@PreFilter的filterTarget属性来指定当前是针对哪个参数进行过滤的;而filterObject是@PreFilter中的一个内置表达式,表示集合中的元素对象。下面案例中测试过滤奇数id: 测试结果如下: 2.@PostFilter的用法 @PostFilter注解主要是用于对集合类型的返回值进行过滤,filterObject是@PostFilter中的一个内置表达式,表示集合中的元素对象。 使用时可根据返回列表的指定条件来进行过滤返回结果,使用方式如下:
@Slf4j
@Service
public class MenuServiceImpl implements MenuService {
@Resource
private SysMenuDao sysMenuDao;
@Override
@PostFilter("filterObject.status.equals('0')")
public List<SysMenu> queryAllMenus() {
List<SysMenu> sysMenus = sysMenuDao.selectAll();
log.info("sysMenus:{}", JSON.toJSONString(sysMenus));
return sysMenus;
}
}
数据集如下: 返回结果如下:
2.2.3 动态权限控制
Spring Security中的动态权限,主要是通过重写拦截器和决策器来进行实现,最简单的方法就是自定义一个Filter去完成权限判断。其实这里涉及到的代码,基本和Spring Security关系不大,主要是在传统的Filter进行实现,这里不再赘述。
2.3 spring security整合权限原理
本文将主要利用授权注解结合SpEL表达式实现权限控制,首先分析一下spring security权限认证流程。
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从 SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。
鉴权认证通过时获取权限信息,存入缓存部分代码如下: 这样缓存中存储的用户信息就会携带权限信息,通过在方法上使用注解@PreAuthorize或@PostAuthorize来进行权限校验即可。该方法对应的sql如下:
<select id="selectMenusByUserId" resultType="string">
SELECT DISTINCT sm.perms FROM `sys_user` u
LEFT JOIN sys_user_role sur on u.id = sur.user_id
LEFT JOIN sys_role sr ON sur.role_id = sr.id
LEFT JOIN sys_role_menu srm ON srm.role_id = sur.role_id
LEFT JOIN sys_menu sm ON srm.menu_id = sm.id
WHERE u.id = #{id,jdbcType=BIGINT}
</select>
上述sql主要是通过userId查询用户所拥有权限信息,具体表信息及解释可查看下文。
2.4 RBAC权限建表语句
CREATE TABLE `sys_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名',
`path` varchar(200) DEFAULT NULL COMMENT '路由地址',
`component` varchar(255) DEFAULT NULL COMMENT '组件路径',
`visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
`status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
`perms` varchar(255) DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',
`create_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`modify_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`del_flag` int(11) DEFAULT '0' COMMENT '是否删除(0未删除 1已删除)',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
CREATE TABLE `sys_role` (
`id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '角色名称',
`role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',
`status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
`del_flag` int(11) DEFAULT '0' COMMENT '是否删除(0未删除 1已删除)',
`create_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`modify_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`update_by` bigint(200) DEFAULT NULL,
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
CREATE TABLE `sys_role_menu` (
`role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色id',
`menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '权限id',
PRIMARY KEY (`role_id`,`menu_id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
`nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
`password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
`status` char(1) DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
`email` varchar(64) DEFAULT 'NULL' COMMENT '邮箱',
`mobile` varchar(32) DEFAULT 'NULL' COMMENT '手机号',
`sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
`create_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`modify_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`del_flag` int(11) DEFAULT '0' COMMENT '是否删除(0未删除 1已删除)',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
CREATE TABLE `sys_user_role` (
`user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色id',
`role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '权限id',
PRIMARY KEY (`user_id`,`role_id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
INSERT INTO `zipkin`.`sys_user` (`id`, `user_name`, `password`, `status`, `create_time`, `modify_time`) VALUES ('1', 'admin', '$2a$10$GoLr2BQF77XaqSM9q3ETqu3fsbaIwOddz4YjvxoL8gGVph486OWmC', '1', '2022-05-26 10:42:58', '2022-05-26 10:42:58');
由于RBAC模型主要包含用户、角色、权限。因此本文主要设计了五张表:sys_user(用户表)、sys_menu(菜单表)、sys_role(权限表)、sys_role_menu(角色权限关联表)、sys_user_role(用户角色关联表)。
2.4 权限不足异常管理
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给springSecurity即可。 如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给springSecurity即可。
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ResultData resultData = new ResultData(HttpStatus.FORBIDDEN.value(), "用户权限不足");
WebUtils.printString(response, JSON.toJSONString(resultData));
}
}
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ResultData resultData = new ResultData(HttpStatus.UNAUTHORIZED.value(), "用户认证失败");
WebUtils.printString(response, JSON.toJSONString(resultData));
}
}
2.4 配置异常拦截
2.5 测试结果
1.用户认证失败(账号密码故意填错) 2.修改@PreAuthorize(“hasAuthority(‘sys:user:add1’)”) 实际用户拥有的权限为sys:user:add 3.修改@PreAuthorize(“hasAuthority(‘sys:user:add’)”) 用户拥有的权限也是sys:user:add,能够成功访问,得到如下结果:
3.小结
1.本文介绍了spring security中进行权限控制的四种方式,每种方式都有其对应的使用场景,应甄别使用; 2.RBAC权限模型是一种基础权限模型,较为灵活,应根据场景进行权限设计,避免设计不足或过度设计; 3.利用授权注解结合SpEL表达式实现权限控制是一种灵活的权限控制方式,主要缺陷在于代码耦合度较高。
4.参考文献
1.https://www.springcloud.cc/spring-security-zhcn.html#core-services-authentication-manager 2.https://zhuanlan.zhihu.com/p/349962352 3.https://www.csdn.net/tags/NtzaUgxsNTIxMTItYmxvZwO0O0OO0O0O.html
5.附录
https://gitee.com/Marinc/springboot-demos/tree/master/spring-security
|