在前面的讲解中,我们分别对动态用户、动态权限的实现做了相关介绍。可能大家在看的过程中,会发现一个问题:目前都是通过注解控制权限的,并且角色是事先定义好的,且需要数据库、Java程序保持一致。
这是非常不友好的,不能自由的定义角色及其所能控制的资源。角色比较少且固定的业务场景还好,如OA,只有管理员和普通用户两种角色。但是遇到大型业务系统,角色细且繁多,需要自定义,且需要频繁变更其所拥有的资源,那么,目前的形式就显得非常笨拙。
接下来,就如何进行资源权限动态控制进行讲解,要实现的目标为:Java程序、数据库无需商定角色标识,以及角色所拥有的资源,一切均通过功能动态维护。
下面,就开始吧。
首先,定义数据结构。
create table SYS_FUNC
(
ID varchar(32) not null comment '主键',
NAME varchar(60) comment '功能名称',
URL varchar(60) comment '功能地址',
PID varchar(32) comment '父功能id',
SORT int comment '顺序号',
GMT_CREATE timestamp default CURRENT_TIMESTAMP comment '新增时间,默认当前时间,不随数据改变而改变',
GMT_MODIFIED timestamp default CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP comment '修改时间,默认当前时间,随数据改变而改变',
primary key (ID)
);
alter table SYS_FUNC comment '系统功能';
create table SYS_ROLE_FUNC
(
ID varchar(32) not null comment '主键',
ROLE_ID varchar(32) comment '角色ID',
FUNC_ID varchar(32) comment '功能ID',
primary key (ID)
);
alter table SYS_ROLE_FUNC comment '角色功能';
然后,定义功能Dao,查询角色与功能映射关系。
public List<SysFuncRole> listFuncRole() {
String sql = "select sys_role.id roleId,\n" +
" sys_role.code roleCode,\n" +
" sys_func.id funcId,\n" +
" sys_func.url url\n" +
" from sys_func, sys_role_func, sys_role\n" +
" where sys_func.id = sys_role_func.func_id\n" +
" and sys_role_func.role_id = sys_role.id\n";
return list(sql, new HashMap<>(), new BeanPropertyRowMapper<>(SysFuncRole.class));
}
自定义 FilterSecurityInterceptor。注意,该类没有任何实质性内容,空类,但不可省略。
public class CustomFilterSecurityInterceptor extends FilterSecurityInterceptor {
}
组织 FilterSecurityInterceptor 所需要的 SecurityMetadataSource。
private LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> obtainRequestMap() {
List<SysFuncRole> sysFuncRoles = this.funcDao.listFuncRole();
if (CollectionUtils.isEmpty(sysFuncRoles)) {
return new LinkedHashMap<>();
}
Map<String, Set<String>> urlRoleMap = new HashMap<>();
for (SysFuncRole sysFuncRole : sysFuncRoles) {
String url = determineAntUrl(sysFuncRole.getUrl());
Set<String> configAttributes = urlRoleMap.get(url);
if (configAttributes == null) {
configAttributes = new HashSet<>();
}
configAttributes.add(sysFuncRole.getRoleCode());
urlRoleMap.put(url, configAttributes);
}
LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap = new LinkedHashMap<>();
for(String url : urlRoleMap.keySet()) {
Set<String> needRoles = urlRoleMap.get(url);
// 注意此处,我们设置ConfigAttribute为 ROLE_ 前缀加上角色标识,与 CustomJdbcUserDetailsService 里面组织UserDetails设置角色标识呼应
requestMap.put(new AntPathRequestMatcher(url), needRoles.stream().map(role -> new SecurityConfig("ROLE_" + role)).collect(Collectors.toSet()));
}
return requestMap;
}
需要注意,在本例中,我们假设一旦给用户分配了某个功能,即代表该功能下的所有操作用户都可以访问。另外,如果一个功能有多个角色控制,那么我们默认只要分配了其中的任何一个角色,都可以访问该功能。如果有别的业务场景,可参考详细源码适当改造,如满足所有角色才能访问功能(更换访问控制管理器中的投票器实现)、细分功能权限控制(修改SecurityMetadataSource组织逻辑)等等。
配置自定义的 FilterSecurityInterceptor。
private FilterSecurityInterceptor customFilterSecurityInterceptor() throws Exception {
CustomFilterSecurityInterceptor filterSecurityInterceptor = new CustomFilterSecurityInterceptor();
filterSecurityInterceptor.setSecurityMetadataSource(new DefaultFilterInvocationSecurityMetadataSource(obtainRequestMap()));
filterSecurityInterceptor.setAccessDecisionManager(accessDecisionManager());
filterSecurityInterceptor.setAuthenticationManager(authenticationManager());
return filterSecurityInterceptor;
}
最后,把自定义的 FilterSecurityInterceptor 配置到 Spring Security 中。
http.addFilterAfter(customFilterSecurityInterceptor(), FilterSecurityInterceptor.class);
另外,需要把 Controller 中所有 @PreAuthorize 注解 和 Spring Security 配置类中的 @EnableGlobalMethodSecurity 注解删除。
好了,一切已经准备就绪。启动系统,访问个人中心,如预想的一样,可以正常访问。
接下来,我们把角色用户映射表中当前用户的的数据先备份以下,然后删除。再来访问一下个人中心,已经不能访问,提示403无权限。
然后,我们再执行备份的sql,把映射关系补回来,再访问个人中心,熟悉的界面又回来了。
资源权限动态控制实现完成。
源码
github
https://github.com/liuminglei/SpringSecurityLearning/tree/master/15
gitee
https://gitee.com/xbd521/SpringSecurityLearning/tree/master/15