Spring Security动态权限的实现方法详解

网友投稿 455 2022-07-22


目录1. 动态管理权限规则1.1 数据库设计1.2 实战2. 测试

最近在做 TienChin 项目,用的是 RuoYi-vue 脚手架,在这个脚手架中,访问某个接口需要什么权限,这个是在代码中硬编码的,具体怎么实现的,松哥下篇文章来和大家分析,有的小伙伴可能希望能让这个东西像 vhr 一样,可以在数据库中动态配置,因此这篇文章和小伙伴们简单介绍下 Spring Security 中的动态权限方案,以便于小伙伴们更好的理解 TienChin 项目中的权限方案。

1. 动态管理权限规则

通过代码来配置 URL 拦截规则和请求 URL 所需要的权限,这样就比较死板,如果想要调整访问某一个 URL 所需要的权限,就需要修改代码。

动态管理权限规则就是我们将 URL 拦截规则和访问 URL 所需要的权限都保存在数据库中,这样,在不改变源代码的情况下,只需要修改数据库中的数据,就可以对权限进行调整。

1.1 数据库设计

简单起见,我们这里就不引入权限表了,直接使用角色表,用户和角色关联,角色和资源关联,设计出来的表结构如图 13-9 所示。

图13-9  一个简单的权限数据库结构

menu 表是相当于我们的资源表,它里边保存了访问规则,如图 13-10 所示。

图13-10  访问规则

role 是角色表,里边定义了系统中的角色,如图 13-11 所示。

图13-11  用户角色表

user 是用户表,如图 13-12 所示。

图13-12  用户表

user_role 是用户角色关联表,用户具有哪些角色,可以通过该表体现出来,如图 13-13 所示。

图13-13  用户角色关联表

menu_role 是资源角色关联表,访问某一个资源,需要哪些角色,可以通过该表体现出来,如图 13-14 所示。

图13-14  资源角色关联表

至此,一个简易的权限数据库就设计好了(在本书提供的案例中,有SQL脚本)。

1.2 实战

项目创建

创建 Spring Boot 项目,由于涉及数据库操作,这里选用目前大家使用较多的 MyBatis 框架,所以除了引入 Web、Spring Security 依赖之外,还需要引入 MyBatis 以及 mysql 依赖。

最终的 pom.xml 文件内容如下:

org.springframework.boot

spring-boot-starter-security

org.springframework.boot

spring-boot-starter-web

org.mybatis.spring.boot

mybatis-spring-boot-starter

2.1.3

mysql

mysql-connector-java

runtime

项目创建完成后,接下来在 application.properties 中配置数据库连接信息:

spring.datasource.username=root

spring.datasource.password=123

spring.datasource.url=jdbc:mysql:///security13?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

配置完成后,我们的准备工作就算完成了。

创建实体类

根据前面设计的数据库,我们需要创建三个实体类。

首先来创建角色类 Role:

publicclassRole{

privateIntegerid;

privateStringname;

privateStringnameZh;

//省略getter/setter

}

然后创建菜单类 Menu:

publicclassMenu{

privateIntegerid;

privateStringpattern;

privateListroles;

//省略getter/setter

}

菜单类中包含一个 roles 属性,表示访问该项资源所需要的角色。

最后我们创建 User 类:

publicclassUserimplementsUserDetails{

privateIntegerid;

privateStringpassword;

privateStringusername;

privatebooleanenabled;

privatebooleanlocked;

privateListroles;

@Override

publicCollectiongetAuthorities(){

returnroles.stream()

.map(r->newSimpleGrantedAuthority(r.getName()))

.collect(Collectors.toList());

}

@Override

publicStringgetPassword(){

returnpassword;

}

@Override

publicStringgetUsername(){

returnusername;

}

@Override

publicbooleanisAccountNonExpired(){

returntrue;

}

@Override

publicbooleanisAccountNonLocked(){

return!locked;

}

@Override

publicbooleanisCredentialsNonExpired(){

returntrue;

}

@Override

publicbooleanisEnabled(){

returnenabled;

}

//省略其他getter/setter

}

由于数据库中有 enabled 和 locked 字段,所以 isEnabled() 和 isAccountNonLocked() 两个方法如实返回,其他几个账户状态方法默认返回 true 即可。在 getAuthorities() 方法中,我们对 roles 属性进行遍历,组装出新的集合对象返回即可。

创建Service

接下来我们创建 UserService 和 MenuService,并提供相应的查询方法。

先来看 UserService:

@Service

publicclassUserServiceimplementsUserDetailsService{

@Autowired

UserMapperuserMapper;

@Override

publicUserDetailsloadUserByUsername(Stringusername)

throwsUsernameNotFoundException{

Useruser=userMapper.loadUserByUsername(username);

if(user==null){

thrownewUsernameNotFoundException("用户不存在");

}

user.setRoles(userMapper.getUserRoleByUid(user.getId()));

returnuser;

}

}

这段代码应该不用多说了,不熟悉的读者可以参考本书 2.4 节。

对应的 UserMapper 如下:

@Mapper

publicinterfaceUserMapper{

ListgetUserRoleByUid(Integeruid);

UserloadUserByUsername(Stringusername);

}

UserMapper.xml:

PUBLIC"-//mybatis.org//DTDMapper3.0//EN"

"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

resultType="org.javaboy.base_on_url_dy.model.User">

select*fromuserwhereusername=#{username};

resultType="org.javaboy.base_on_url_dy.model.Role">

selectr.*fromroler,user_roleurwhereur.uid=#{uid}andur.rid=r.id

再来看 MenuService,该类只需要提供一个方法,就是查询出所有的 Menu 数据,代码如下:

@Service

publicclassMenuService{

@Autowired

MenuMappermenuMapper;

publicList

getAllMenu(){

returnmenuMapper.getAllMenu();

}

}

MenuMapper:

@Mapper

publicinterfaceMenuMapper{

List

getAllMenu();

resultType="org.javaboy.base_on_url_dy.model.User">

select*fromuserwhereusername=#{username};

resultType="org.javaboy.base_on_url_dy.model.Role">

selectr.*fromroler,user_roleurwhereur.uid=#{uid}andur.rid=r.id

再来看 MenuService,该类只需要提供一个方法,就是查询出所有的 Menu 数据,代码如下:

@Service

publicclassMenuService{

@Autowired

MenuMappermenuMapper;

publicList

getAllMenu(){

resultType="org.javaboy.base_on_url_dy.model.Role">

selectr.*fromroler,user_roleurwhereur.uid=#{uid}andur.rid=r.id

再来看 MenuService,该类只需要提供一个方法,就是查询出所有的 Menu 数据,代码如下:

@Service

publicclassMenuService{

@Autowired

MenuMappermenuMapper;

publicList

returnmenuMapper.getAllMenu();

}

}

MenuMapper:

@Mapper

publicinterfaceMenuMapper{

List

}

MenuMapper.xml:

PUBLIC"-//mybatis.org//DTDMapper3.0//EN"

"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

type="org.javaboy.base_on_url_dy.model.Menu">

ofType="org.javaboy.base_on_url_dy.model.Role">

selectm.*,r.idasrid,r.nameasrname,r.nameZhasrnameZhfrommenumleftjoinmenu_rolemronm.`id`=mr.`mid`leftjoinroleronr.`id`=mr.`rid`

需要注意,由于每一个 Menu 对象都包含了一个 Role 集合,所以这个查询是一对多,这里通过 resultMap 来进行查询结果映射。

至此,所有基础工作都完成了,接下来配置 Spring Security。

配置Spring Security

回顾 13.3.6 小节的内容,SecurityMetadataSource 接口负责提供受保护对象所需要的权限。在本案例中,受保护对象所需要的权限保存在数据库中,所以我们可以通过自定义类继承自 FilterInvocationSecurityMetadataSource,并重写 getAttributes 方法来提供受保护对象所需要的权限,代码如下:

@Component

publicclassCustomSecurityMetadataSource

implementsFilterInvocationSecurityMetadataSource{

@Autowired

MenuServicemenuService;

AntPathMatcherantPathMatcher=newAntPathMatcher();

@Override

publicCollectiongetAttributes(Objectobject)

throwsIllegalArgumentException{

StringrequestURI=

((FilterInvocation)object).getRequest().getRequestURI();

List

allMenu=menuService.getAllMenu();

for(Menumenu:allMenu){

if(antPathMatcher.match(menu.getPattern(),requestURI)){

String[]roles=menu.getRoles().stream()

.map(r->r.getName()).toArray(String[]::new);

returnSecurityConfig.createList(roles);

}

}

returnnull;

}

@Override

publicCollectiongetAllConfigAttributes(){

returnnull;

}

@Override

publicbooleansupports(Class>clazz){

returnFilterInvocation.class.isAssignableFrom(clazz);

}

}

自定义 CustomSecurityMetadataSource 类并实现 FilterInvocationSecurityMetadataSource 接口,然后重写它里边的三个方法:

getAttributes:该方法的参数是受保护对象,在基于 URL 地址的权限控制中,受保护对象就是 FilterInvocation;该方法的返回值则是访问受保护对象所需要的权限。在该方法里边,我们首先从受保护对象 FilterInvocation 中提取出当前请求的 URL 地址,例如 /admin/hello,然后通过 menuService 对象查询出所有的菜单数据(每条数据中都包含访问该条记录所需要的权限),遍历查询出来的菜单数据,如果当前请求的 URL 地址和菜单中某一条记录的 pattern 属性匹配上了(例如 /admin/hello 匹配上 /admin/**),那么我们就可以获取当前请求所需要的权限。从 menu 对象中获取 roles 属性,并将其转为一个数组,然后通过 SecurityConfig.createList 方法创建一个 Collection 对象并返回。如果当前请求的 URL 地址和数据库中 menu 表的所有项都匹配不上,那么最终返回 null。如果返回 null,那么受保护对象到底能不能访问呢?这就要看 AbstractSecurityInterceptor 对象中的 rejectPublicInvocations 属性了,该属性默认为 false,表示当 getAttributes 方法返回 null 时,允许访问受保护对象(回顾 13.4.4 小节中关于 AbstractSecurityInterceptor#beforeInvocation 的讲解)。getAllConfigAttributes:该方法可以用来返回所有的权限属性,以便在项目启动阶段做校验,如果不需要校验,则直接返回 null 即可。supports:该方法表示当前对象支持处理的受保护对象是 FilterInvocation。

CustomSecurityMetadataSource 类配置完成后,接下来我们要用它来代替默认的 SecurityMetadataSource 对象,具体配置如下:

@Configuration

publicclassSecurityConfigextendsWebSecurityConfigurerAdapter{

@Autowired

CustomSecurityMetadataSourcecustomSecurityMetadataSource;

@Autowired

UserServiceuserService;

@Override

protectedvoidconfigure(AuthenticationManagerBuilderauth)

throwsException{

auth.userDetailsService(userService);

}

@Override

protectedvoidconfigure(HttpSecurityhttp)throwsException{

ApplicationContextapplicationContext=

http.getSharedObject(ApplicationContext.class);

http.apply(newUrlAuthorizationConfigurer<>(applicationContext))

.withObjectPostProcessor(new

ObjectPostProcessor(){

@Override

publicO

postProcess(Oobject){

object.setSecurityMetadataSource(customSecurityMetadataSource);

returnobject;

}

});

http.formLogin()

.and()

.csrf().disable();

}

}

关于用户的配置无需多说,我们重点来看 configure(HttpSecurity) 方法。

由于访问路径规则和所需要的权限之间的映射关系已经保存在数据库中,所以我们就没有必要在 Java 代码中配置映射关系了,同时这里的权限对比也不会用到权限表达式,所以我们通过 UrlAuthorizationConfigurer 来进行配置。

在配置的过程中,通过 withObjectPostProcessor 方法调用 ObjectPostProcessor 对象后置处理器,在对象后置处理器中,将 FilterSecurityInterceptor 中的 SecurityMetadataSource 对象替换为我们自定义的 customSecurityMetadataSource 对象即可。

2. 测试

接下来创建 HelloController,代码如下:

@RestController

publicclassHelloController{

@GetMapping("/admin/hello")

publicStringadmin(){

return"helloadmin";

}

@GetMapping("/user/hello")

publicStringuser(){

return"hellouser";

}

@GetMapping("/guest/hello")

publicStringguest(){

return"helloguest";

}

@GetMapping("/hello")

publicStringhello(){

return"hello";

}

}

最后启动项目进行测试。

首先使用 admin/123 进行登录,该用户具备 ROLE_ADMIN 角色,ROLE_ADMIN 可以访问 /admin/hello、/user/hello 以及 /guest/hello 三个接口。

接下来使用 user/123 进行登录,该用户具备 ROLE_USER 角色,ROLE_USER 可以访问 /user/hello 以及 /guest/hello 两个接口。

最后使用 javaboy/123 进行登录,该用户具备 ROLE_GUEST 角色,ROLE_GUEST 可以访问 /guest/hello 接口。

由于 /hello 接口不包含在 URL-权限 映射关系中,所以任何用户都可以访问 /hello 接口,包括匿名用户。如果希望所有的 URL 地址都必须在数据库中配置 URL-权限 映射关系后才能访问,那么可以通过如下配置实现:

http.apply(newUrlAuthorizationConfigurer<>(applicationContext))

.withObjectPostProcessor(new

ObjectPostProcessor(){

@Override

publicO

postProcess(Oobject){

object.setSecurityMetadataSource(customSecurityMetadataSource);

object.setRejectPublicInvocations(true);

returnobject;

}

});

通过设置 FilterSecurityInterceptor 中的 rejectPublicInvocations 属性为 true,就可以关闭URL的公开访问,所有 URL 必须具备对应的权限才能访问。

以上就是Spring Security动态权限的实现方法详解的详细内容,更多关于Spring Security动态权限的资料请关注我们其它相关文章!

type="org.javaboy.base_on_url_dy.model.Menu">

ofType="org.javaboy.base_on_url_dy.model.Role">

selectm.*,r.idasrid,r.nameasrname,r.nameZhasrnameZhfrommenumleftjoinmenu_rolemronm.`id`=mr.`mid`leftjoinroleronr.`id`=mr.`rid`

需要注意,由于每一个 Menu 对象都包含了一个 Role 集合,所以这个查询是一对多,这里通过 resultMap 来进行查询结果映射。

至此,所有基础工作都完成了,接下来配置 Spring Security。

配置Spring Security

回顾 13.3.6 小节的内容,SecurityMetadataSource 接口负责提供受保护对象所需要的权限。在本案例中,受保护对象所需要的权限保存在数据库中,所以我们可以通过自定义类继承自 FilterInvocationSecurityMetadataSource,并重写 getAttributes 方法来提供受保护对象所需要的权限,代码如下:

@Component

publicclassCustomSecurityMetadataSource

implementsFilterInvocationSecurityMetadataSource{

@Autowired

MenuServicemenuService;

AntPathMatcherantPathMatcher=newAntPathMatcher();

@Override

publicCollectiongetAttributes(Objectobject)

throwsIllegalArgumentException{

StringrequestURI=

((FilterInvocation)object).getRequest().getRequestURI();

List

allMenu=menuService.getAllMenu();

ofType="org.javaboy.base_on_url_dy.model.Role">

selectm.*,r.idasrid,r.nameasrname,r.nameZhasrnameZhfrommenumleftjoinmenu_rolemronm.`id`=mr.`mid`leftjoinroleronr.`id`=mr.`rid`

需要注意,由于每一个 Menu 对象都包含了一个 Role 集合,所以这个查询是一对多,这里通过 resultMap 来进行查询结果映射。

至此,所有基础工作都完成了,接下来配置 Spring Security。

配置Spring Security

回顾 13.3.6 小节的内容,SecurityMetadataSource 接口负责提供受保护对象所需要的权限。在本案例中,受保护对象所需要的权限保存在数据库中,所以我们可以通过自定义类继承自 FilterInvocationSecurityMetadataSource,并重写 getAttributes 方法来提供受保护对象所需要的权限,代码如下:

@Component

publicclassCustomSecurityMetadataSource

implementsFilterInvocationSecurityMetadataSource{

@Autowired

MenuServicemenuService;

AntPathMatcherantPathMatcher=newAntPathMatcher();

@Override

publicCollectiongetAttributes(Objectobject)

throwsIllegalArgumentException{

StringrequestURI=

((FilterInvocation)object).getRequest().getRequestURI();

List

for(Menumenu:allMenu){

if(antPathMatcher.match(menu.getPattern(),requestURI)){

String[]roles=menu.getRoles().stream()

.map(r->r.getName()).toArray(String[]::new);

returnSecurityConfig.createList(roles);

}

}

returnnull;

}

@Override

publicCollectiongetAllConfigAttributes(){

returnnull;

}

@Override

publicbooleansupports(Class>clazz){

returnFilterInvocation.class.isAssignableFrom(clazz);

}

}

自定义 CustomSecurityMetadataSource 类并实现 FilterInvocationSecurityMetadataSource 接口,然后重写它里边的三个方法:

getAttributes:该方法的参数是受保护对象,在基于 URL 地址的权限控制中,受保护对象就是 FilterInvocation;该方法的返回值则是访问受保护对象所需要的权限。在该方法里边,我们首先从受保护对象 FilterInvocation 中提取出当前请求的 URL 地址,例如 /admin/hello,然后通过 menuService 对象查询出所有的菜单数据(每条数据中都包含访问该条记录所需要的权限),遍历查询出来的菜单数据,如果当前请求的 URL 地址和菜单中某一条记录的 pattern 属性匹配上了(例如 /admin/hello 匹配上 /admin/**),那么我们就可以获取当前请求所需要的权限。从 menu 对象中获取 roles 属性,并将其转为一个数组,然后通过 SecurityConfig.createList 方法创建一个 Collection 对象并返回。如果当前请求的 URL 地址和数据库中 menu 表的所有项都匹配不上,那么最终返回 null。如果返回 null,那么受保护对象到底能不能访问呢?这就要看 AbstractSecurityInterceptor 对象中的 rejectPublicInvocations 属性了,该属性默认为 false,表示当 getAttributes 方法返回 null 时,允许访问受保护对象(回顾 13.4.4 小节中关于 AbstractSecurityInterceptor#beforeInvocation 的讲解)。getAllConfigAttributes:该方法可以用来返回所有的权限属性,以便在项目启动阶段做校验,如果不需要校验,则直接返回 null 即可。supports:该方法表示当前对象支持处理的受保护对象是 FilterInvocation。

CustomSecurityMetadataSource 类配置完成后,接下来我们要用它来代替默认的 SecurityMetadataSource 对象,具体配置如下:

@Configuration

publicclassSecurityConfigextendsWebSecurityConfigurerAdapter{

@Autowired

CustomSecurityMetadataSourcecustomSecurityMetadataSource;

@Autowired

UserServiceuserService;

@Override

protectedvoidconfigure(AuthenticationManagerBuilderauth)

throwsException{

auth.userDetailsService(userService);

}

@Override

protectedvoidconfigure(HttpSecurityhttp)throwsException{

ApplicationContextapplicationContext=

http.getSharedObject(ApplicationContext.class);

http.apply(newUrlAuthorizationConfigurer<>(applicationContext))

.withObjectPostProcessor(new

ObjectPostProcessor(){

@Override

publicO

postProcess(Oobject){

object.setSecurityMetadataSource(customSecurityMetadataSource);

returnobject;

}

});

http.formLogin()

.and()

.csrf().disable();

}

}

关于用户的配置无需多说,我们重点来看 configure(HttpSecurity) 方法。

由于访问路径规则和所需要的权限之间的映射关系已经保存在数据库中,所以我们就没有必要在 Java 代码中配置映射关系了,同时这里的权限对比也不会用到权限表达式,所以我们通过 UrlAuthorizationConfigurer 来进行配置。

在配置的过程中,通过 withObjectPostProcessor 方法调用 ObjectPostProcessor 对象后置处理器,在对象后置处理器中,将 FilterSecurityInterceptor 中的 SecurityMetadataSource 对象替换为我们自定义的 customSecurityMetadataSource 对象即可。

2. 测试

接下来创建 HelloController,代码如下:

@RestController

publicclassHelloController{

@GetMapping("/admin/hello")

publicStringadmin(){

return"helloadmin";

}

@GetMapping("/user/hello")

publicStringuser(){

return"hellouser";

}

@GetMapping("/guest/hello")

publicStringguest(){

return"helloguest";

}

@GetMapping("/hello")

publicStringhello(){

return"hello";

}

}

最后启动项目进行测试。

首先使用 admin/123 进行登录,该用户具备 ROLE_ADMIN 角色,ROLE_ADMIN 可以访问 /admin/hello、/user/hello 以及 /guest/hello 三个接口。

接下来使用 user/123 进行登录,该用户具备 ROLE_USER 角色,ROLE_USER 可以访问 /user/hello 以及 /guest/hello 两个接口。

最后使用 javaboy/123 进行登录,该用户具备 ROLE_GUEST 角色,ROLE_GUEST 可以访问 /guest/hello 接口。

由于 /hello 接口不包含在 URL-权限 映射关系中,所以任何用户都可以访问 /hello 接口,包括匿名用户。如果希望所有的 URL 地址都必须在数据库中配置 URL-权限 映射关系后才能访问,那么可以通过如下配置实现:

http.apply(newUrlAuthorizationConfigurer<>(applicationContext))

.withObjectPostProcessor(new

ObjectPostProcessor(){

@Override

publicO

postProcess(Oobject){

object.setSecurityMetadataSource(customSecurityMetadataSource);

object.setRejectPublicInvocations(true);

returnobject;

}

});

通过设置 FilterSecurityInterceptor 中的 rejectPublicInvocations 属性为 true,就可以关闭URL的公开访问,所有 URL 必须具备对应的权限才能访问。

以上就是Spring Security动态权限的实现方法详解的详细内容,更多关于Spring Security动态权限的资料请关注我们其它相关文章!


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Java实现注册登录跳转
下一篇:Java界面编程实现界面跳转
相关文章

 发表评论

暂时没有评论,来抢沙发吧~