spring security动态权限控制,springboot security动态权限
00-1010 1.动态管理权限规则1.1数据库设计1.2实战2。最近测试,正在做天琴项目,用的是若逸-Vue脚手架。在这个脚手架中,访问接口需要什么权限?这是代码中硬编码的,是如何实现的?宋歌将在下一篇文章中与您一起分析它。可能有朋友希望这个东西能像vhr一样在数据库中动态配置,所以本文和朋友简单介绍一下Spring Security中的动态权限方案,让朋友们更好的了解TienChin项目中的权限方案。
00-1010配置URL阻止规则和通过代码请求URL所需的权限相当严格。如果您想要调整访问某个URL所需的权限,您需要修改代码。
动态权限规则是指我们在数据库中保存URL拦截规则和访问URL所需的权限,这样只需要修改数据库中的数据就可以调整权限,而不需要修改源代码。
00-1010为了简单起见,这里不介绍权限表。我们直接用角色表,用户和角色的关联,角色和资源的关联。设计的表格结构如图13-9所示。
图13-9简单的权限数据库结构
菜单相当于我们的资源表,它保存了访问规则,如图13-10所示。
图13-10访问规则
角色是角色表,定义了系统中的角色,如图13-11所示。
图13-11用户角色表
用户是用户表,如图13-12所示。
图13-12用户表
User_role是用户角色关联表,用户拥有哪些角色可以通过这个表反映出来,如图13-13所示。
图13-13用户角色关联表
Menu_role是资源角色关联表。访问某个资源需要哪些角色可以反映在这个表中,如图13-14所示。
图13-14资源角色关联表
至此,已经设计了一个简单的权限数据库(在本书提供的案例中,有SQL脚本)。
目录
项目创建
创建Spring Boot项目,因为涉及到数据库操作,这里选择了目前广泛使用的MyBatis框架。所以除了Web和Spring的安全依赖,还需要引入MyBatis和MySQL的依赖。
最终的pom.xml文件如下:
dependencies dependencygroupid org . spring framework . boot/groupidatifactidspring-boot-starter-security/artifactId/dependencydependencycgroupid org . spring framework . boot/groupidatifactidspring-boot-starter-web/artifactId/dependencydependencycgroupid org . mybatis . spring . boot/groupidatifactidmybatis-spring-boot-starter/artifactidversion 2 . 1 . 3/version/dependencydependencycgroupid MySQL/groupId
gt; <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency></dependencies>项目创建完成后,接下来在 application.properties 中配置数据库连接信息:
spring.datasource.username=rootspring.datasource.password=123spring.datasource.url=jdbc:mysql:///security13?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
配置完成后,我们的准备工作就算完成了。
创建实体类
根据前面设计的数据库,我们需要创建三个实体类。
首先来创建角色类 Role:
public class Role { private Integer id; private String name; private String nameZh; //省略getter/setter}
然后创建菜单类 Menu:
public class Menu { private Integer id; private String pattern; private List<Role> roles; //省略getter/setter}
菜单类中包含一个 roles 属性,表示访问该项资源所需要的角色。
最后我们创建 User 类:
public class User implements UserDetails { private Integer id; private String password; private String username; private boolean enabled; private boolean locked; private List<Role> roles; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return roles.stream() .map(r -> new SimpleGrantedAuthority(r.getName())) .collect(Collectors.toList()); } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return !locked; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return enabled; } //省略其他getter/setter}
由于数据库中有 enabled 和 locked 字段,所以 isEnabled() 和 isAccountNonLocked() 两个方法如实返回,其他几个账户状态方法默认返回 true 即可。在 getAuthorities() 方法中,我们对 roles 属性进行遍历,组装出新的集合对象返回即可。
创建Service
接下来我们创建 UserService 和 MenuService,并提供相应的查询方法。
先来看 UserService:
@Servicepublic class UserService implements UserDetailsService { @Autowired UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.loadUserByUsername(username); if (user == null) { throw new UsernameNotFoundException("用户不存在"); } user.setRoles(userMapper.getUserRoleByUid(user.getId())); return user; }}
这段代码应该不用多说了,不熟悉的读者可以参考本书 2.4 节。
对应的 UserMapper 如下:
@Mapperpublic interface UserMapper { List<Role> getUserRoleByUid(Integer uid); User loadUserByUsername(String username);}
UserMapper.xml:
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="org.javaboy.base_on_url_dy.mapper.UserMapper"> <select id="loadUserByUsername" resultType="org.javaboy.base_on_url_dy.model.User"> select * from user where username=#{username}; </select> <select id="getUserRoleByUid" resultType="org.javaboy.base_on_url_dy.model.Role"> select r.* from role r,user_role ur where ur.uid=#{uid} and ur.rid=r.id </select></mapper>
再来看 MenuService,该类只需要提供一个方法,就是查询出所有的 Menu 数据,代码如下:
@Servicepublic class MenuService { @Autowired MenuMapper menuMapper; public List<Menu> getAllMenu() { return menuMapper.getAllMenu(); }}
MenuMapper:
@Mapperpublic interface MenuMapper { List<Menu> getAllMenu();}
MenuMapper.xml:
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="org.javaboy.base_on_url_dy.mapper.MenuMapper"> <resultMap id="MenuResultMap" type="org.javaboy.base_on_url_dy.model.Menu"> <id property="id" column="id"/> <result property="pattern" column="pattern"></result> <collection property="roles" ofType="org.javaboy.base_on_url_dy.model.Role"> <id column="rid" property="id"/> <result column="rname" property="name"/> <result column="rnameZh" property="nameZh"/> </collection> </resultMap> <select id="getAllMenu" resultMap="MenuResultMap"> select m.*,r.id as rid,r.name as rname,r.nameZh as rnameZh from menu m left join menu_role mr on m.`id`=mr.`mid` left join role r on r.`id`=mr.`rid` </select></mapper>
需要注意,由于每一个 Menu 对象都包含了一个 Role 集合,所以这个查询是一对多,这里通过 resultMap 来进行查询结果映射。
至此,所有基础工作都完成了,接下来配置 Spring Security。
配置Spring Security
回顾 13.3.6 小节的内容,SecurityMetadataSource 接口负责提供受保护对象所需要的权限。在本案例中,受保护对象所需要的权限保存在数据库中,所以我们可以通过自定义类继承自 FilterInvocationSecurityMetadataSource,并重写 getAttributes 方法来提供受保护对象所需要的权限,代码如下:
@Componentpublic class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { @Autowired MenuService menuService; AntPathMatcher antPathMatcher = new AntPathMatcher(); @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { String requestURI = ((FilterInvocation) object).getRequest().getRequestURI(); List<Menu> allMenu = menuService.getAllMenu(); for (Menu menu : allMenu) { if (antPathMatcher.match(menu.getPattern(), requestURI)) { String[] roles = menu.getRoles().stream() .map(r -> r.getName()).toArray(String[]::new); return SecurityConfig.createList(roles); } } return null; } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); }}
自定义 CustomSecurityMetadataSource 类并实现 FilterInvocationSecurityMetadataSource 接口,然后重写它里边的三个方法:
getAttributes:该方法的参数是受保护对象,在基于 URL 地址的权限控制中,受保护对象就是 FilterInvocation;该方法的返回值则是访问受保护对象所需要的权限。在该方法里边,我们首先从受保护对象 FilterInvocation 中提取出当前请求的 URL 地址,例如/admin/hello
,然后通过 menuService 对象查询出所有的菜单数据(每条数据中都包含访问该条记录所需要的权限),遍历查询出来的菜单数据,如果当前请求的 URL 地址和菜单中某一条记录的 pattern 属性匹配上了(例如/admin/hello
匹配上/admin/**
),那么我们就可以获取当前请求所需要的权限。从 menu 对象中获取 roles 属性,并将其转为一个数组,然后通过SecurityConfig.createList
方法创建一个Collection<ConfigAttribute>
对象并返回。如果当前请求的 URL 地址和数据库中 menu 表的所有项都匹配不上,那么最终返回 null。如果返回 null,那么受保护对象到底能不能访问呢?这就要看 AbstractSecurityInterceptor 对象中的 rejectPublicInvocations 属性了,该属性默认为 false,表示当 getAttributes 方法返回 null 时,允许访问受保护对象(回顾 13.4.4 小节中关于AbstractSecurityInterceptor#beforeInvocation
的讲解)。getAllConfigAttributes:该方法可以用来返回所有的权限属性,以便在项目启动阶段做校验,如果不需要校验,则直接返回 null 即可。supports:该方法表示当前对象支持处理的受保护对象是 FilterInvocation。CustomSecurityMetadataSource
类配置完成后,接下来我们要用它来代替默认的SecurityMetadataSource
对象,具体配置如下:
@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired CustomSecurityMetadataSource customSecurityMetadataSource; @Autowired UserService userService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService); } @Override protected void configure(HttpSecurity http) throws Exception { ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class); http.apply(new UrlAuthorizationConfigurer<>(applicationContext)) .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { object.setSecurityMetadataSource(customSecurityMetadataSource); return object; } }); http.formLogin() .and() .csrf().disable(); }}
关于用户的配置无需多说,我们重点来看 configure(HttpSecurity) 方法。
由于访问路径规则和所需要的权限之间的映射关系已经保存在数据库中,所以我们就没有必要在 Java 代码中配置映射关系了,同时这里的权限对比也不会用到权限表达式,所以我们通过 UrlAuthorizationConfigurer 来进行配置。
在配置的过程中,通过 withObjectPostProcessor 方法调用 ObjectPostProcessor 对象后置处理器,在对象后置处理器中,将 FilterSecurityInterceptor 中的 SecurityMetadataSource 对象替换为我们自定义的 customSecurityMetadataSource 对象即可。
2. 测试
接下来创建 HelloController,代码如下:
@RestControllerpublic class HelloController { @GetMapping("/admin/hello") public String admin() { return "hello admin"; } @GetMapping("/user/hello") public String user() { return "hello user"; } @GetMapping("/guest/hello") public String guest() { return "hello guest"; } @GetMapping("/hello") public String hello() { 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(new UrlAuthorizationConfigurer<>(applicationContext)) .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { object.setSecurityMetadataSource(customSecurityMetadataSource);
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。