赞
踩
上文写到了SpringSecurity从入门到放弃之JWT认证登陆(一),该文章介绍了spring security的登陆原理以及整合JWT的登陆案例,本文将基于上文,介绍spring securtity整合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模型的本质就是根据场景设计不同的角色,用角色来关联不同权限(菜单),最后不同的用户根据场景关联对应的角色即可。任何模型都是要根据不同的场景进行设计,不能简单设计,也不能过度设计。
在spring security中,对接口的拦截或放行,主要有以下四种权限控制方式:
1.利用Ant表达式实现权限控制;
2.利用授权注解结合SpEL表达式实现权限控制;
3.利用过滤器注解实现权限控制;
4.利用动态权限实现权限控制。
在Spring Security中,SecurityExpressionOperations接口定义了一系列方法用于用户权限的设置,如下所示:
这些方法的作用分别如下:
方法 | 作用 |
---|---|
hasRole | 用户具备某个角色即可访问 |
hasAnyRole | 用户具备多个角色中一个即可访问 |
permitAll | 所有请求均允许访问 |
denyAll | 所有请求都拒绝访问 |
hasPermission | 用户具备某个权限即可访问 |
isFullyAuthenticated | 判断是否用户名/密码登陆 |
isRememberMe | 判断是否通过记住我功能登陆 |
isAnonymous | 判断是否匿名登陆 |
isAuthenticated | 允许认证通过用户访问 |
hasAuthority | 判断是否拥有权限,类似于hasRole |
hasAnyAuthority | 判断是否拥有某一权限,类似于hasAnyRole |
使用方法如下:
protected void configure(HttpSecurity http) throws Exception { //不通过Session获取SecurityContext http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() //登录接口允许匿名访问 .authorizeRequests() .antMatchers("/login/user").anonymous() //USER角色可以访问/user路径下所有请求 .antMatchers("/user/**").hasRole("USER") //ADMIN角色可以访问/admin路径下所有请求 .antMatchers("/admin/**").hasRole("ADMIN") //除上述接口,均需要授权访问 .anyRequest().authenticated(); // 关闭csrf http.csrf().disable(); }
Spring Security提供了方法注解来进行权限控制,常用的授权注解如下:
@PreAuthorize:在方法执行前进行权限检查;
@PostAuthorize:在方法执行后进行权限检查;
@Secured:类似于 @PreAuthorize。
使用方式如下:
1.首先利用@EnableGlobalMethodSecurity注解开启授权注解功能
2.在方法上开启注解进行权限控制
上述这种方式相对较为灵活,唯一的缺陷是代码耦合度较高。
在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 //根据状态来进行过滤,过滤出状态为0的菜单 @PostFilter("filterObject.status.equals('0')") public List<SysMenu> queryAllMenus() { List<SysMenu> sysMenus = sysMenuDao.selectAll(); log.info("sysMenus:{}", JSON.toJSONString(sysMenus)); return sysMenus; } }
数据集如下:
返回结果如下:
Spring Security中的动态权限,主要是通过重写拦截器和决策器来进行实现,最简单的方法就是自定义一个Filter去完成权限判断。其实这里涉及到的代码,基本和Spring Security关系不大,主要是在传统的Filter进行实现,这里不再赘述。
本文将主要利用授权注解结合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查询用户所拥有权限信息,具体表信息及解释可查看下文。
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(用户角色关联表)。
在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));
}
}
1.用户认证失败(账号密码故意填错)
2.修改@PreAuthorize(“hasAuthority(‘sys:user:add1’)”)
实际用户拥有的权限为sys:user:add
3.修改@PreAuthorize(“hasAuthority(‘sys:user:add’)”)
用户拥有的权限也是sys:user:add,能够成功访问,得到如下结果:
1.本文介绍了spring security中进行权限控制的四种方式,每种方式都有其对应的使用场景,应甄别使用;
2.RBAC权限模型是一种基础权限模型,较为灵活,应根据场景进行权限设计,避免设计不足或过度设计;
3.利用授权注解结合SpEL表达式实现权限控制是一种灵活的权限控制方式,主要缺陷在于代码耦合度较高。
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
https://gitee.com/Marinc/springboot-demos/tree/master/spring-security
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。