Spring Security中经常有多个角色,角色之间应该有一定关系,一般来说角色之间是有关系的,例如ROLE_admin一般既具有admin的权限,又具有user的权限。那么如何配置这种角色继承关系呢?在Spring Security 中只需要开发者提供一 个RoleHierarchy 即可。假设ROLE_dba 是终极大Boss,具有所有的权限,ROLE_admin 具有ROLE_user的权限,ROLE_ user 则是一个公共角色,即ROLE_admin继承ROLE_user、ROLE_dba继承ROLE_ admin ,要描述这种继承关系,只需要开发者在SpringSecurity的配置类中提供一个RoleHierarchy即可,代码如下:
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserService userService;
//这是增加的部分
@Bean
RoleHierarchy roleHierarchy(){
RoleHierarchyImpl roleHierarchy=new RoleHierarchyImpl();
String hierarchy="Role_dba>Role_admin>Role_user";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
@Bean
public static PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/db/**").hasRole("dba")
.antMatchers("/user/**").hasRole("user")
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login").permitAll()
.and()
.csrf().disable();
}
}
配置完RoleHierarchy 之后,具有ROLE_dba角色的用户就可以访问所有资源了,具有ROLE_admin 角色的用户也可以访问具有ROLE_ user 角色才能访问的资源。
动态配置权限使用HttpSecurity配置的认证授权规则还是不够灵活,无法实现资源和角色之间的动态调整,要实现动态配置URL权限,就需要开发者自定义权限配置,配置步骤如下。
1. 数据库设计- menu资源表中定义了用户能够访问的 URL 模式。
- menu_role资源角色表则定义了访问该模式的 URL 需要什么样的角色。
mysql> select * from role;
+----+------------+--------------------+
| id | name | nameZh |
+----+------------+--------------------+
| 1 | ROLE_dba | 数据库管理员 |
| 2 | ROLE_admin | 系统管理员 |
| 3 | ROLE_user | 用户 |
+----+------------+--------------------+
3 rows in set (0.00 sec)
mysql> select * from user;
+----+----------+--------------------------------------------------------------+--------+--------+
| id | username | password | enable | locked |
+----+----------+--------------------------------------------------------------+--------+--------+
| 1 | root | $Y7TCbZ.De3suSD.4boCArugDit4hyOgkywUMuLUquc7OZuL6sTAR. | 1 | 0 |
| 2 | admin | $Y7TCbZ.De3suSD.4boCArugDit4hyOgkywUMuLUquc7OZuL6sTAR. | 1 | 0 |
| 3 | suo | a$Y7TCbZ.De3suSD.4boCArugDit4hyOgkywUMuLUquc7OZuL6sTAR. | 1 | 0 |
+----+----------+--------------------------------------------------------------+--------+--------+
3 rows in set (0.00 sec)
mysql> select * from user_role;
+----+------+------+
| id | uid | rid |
+----+------+------+
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 2 | 2 |
| 4 | 3 | 3 |
+----+------+------+
4 rows in set (0.00 sec)
mysql> select * from menu;
+----+-----------+
| id | pattern |
+----+-----------+
| 1 | /db/** |
| 2 | /admin/** |
| 3 | /user/** |
+----+-----------+
3 rows in set (0.00 sec)
mysql> select * from menu_role;
+----+------+------+
| id | mid | rid |
+----+------+------+
| 1 | 1 | 1 |
| 2 | 2 | 2 |
| 3 | 3 | 3 |
+----+------+------+
3 rows in set (0.00 sec)
2.创建项目
Mybatis灵活,JPA便利,本案例选择前者,因此创建Spring Boot Web添加以下依赖:
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.8version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
<build>
<resources>
<resource>
<directory>src/main/javadirectory>
<includes>
<include>**/*.xmlinclude>
includes>
resource>
<resource>
<directory>src/main/resourcesdirectory>
<includes>
<include>**/*include>
includes>
resource>
resources>
build>
3. 配置数据库
在application.properties中进行数据库连接配置:
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://120.55.61.170:3306/fristweb?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
spring.datasource.username=fristweb
spring.datasource.password=dTNFJW4B5MrwT4KS
spring.datasource.initialSize= 5
spring.datasource.minIdle=5
spring.datasource.maxActive= 20
spring.datasource.maxWait=60000
spring.datasource.timeBetweenEvictionRunsMillis=60000
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
spring.datasource.poolPreparedStatements=true
mybatis.type-aliases-package=suohechuan.testforever.model
mybatis.mapper-locations=classpath:mapper/*.xml
4. 创建实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Role {
private Integer id;
private String name;
private String nameZh;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private Boolean enable;
private Boolean locked;
private List<Role> roles;
@Override
public String getUsername() {
return username;
}
@Override
public String getPassword() {
return password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return !locked;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enable;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Menu {
private Integer id;
private String pattern;
private List<Role> roles;
}
5. 创建UserService
接下来创建UserService,代码如下:
@Service
public 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.getUserRolesByUid(user.getId()));
return user;
}
}
代码解释:
- 定义UserService实现UserDetailService接口,并实现该接口中的loadUserByUsername方法,该方法的参数就是用户登录时输入的用户名,通过用户名去数据库中查找用户,如果没有查找到用户,就抛出一个账户不存在的异常,如果查找到了用户,就继续查找该用户所具有的角色信息,并将获取到的user对象返回,再由系统提供的DaoAuthenticationProvider类去比对密码是否正确。
- loadUserByUsername 方法将在用户登录时自动调用。
当然,这里还涉及UserMapper和UserMapper.xml,相关源码如下:
@Mapper
public interface UserMapper {
User loadUserByUsername(String username);
List<Role> getUserRolesByUid(Integer id);
}
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="suohechuan.testforever.UserMapper">
<select id="loadUserByUsername" resultType="suohechuan.testforever.model.User">
select *
from user
where username = #{username}
select>
<select id="getUserRolesByUid" resultType="suohechuan.testforever.model.Role">
select *
from role r,
user_role ur
where r.id = ur.rid and ur.uid =#{id}
select>
mapper>
6.自定义FilterInvocationSecurityMetadataSource
要实现动态配置权限,首先要自定义FilterInvocationSecurityMetadataSource, Spring Security中通过FilterInvocationSecurityMetadataSource接口中的getAttributes方法来确定一个请求需要哪些角色,FilterInvocationSecurityMetadataSource接口的默认实现类是DefaultFilterInvocationSecurityMetadataSource,参考DefaultFilterInvocationSecurityMetadataSource的实现,开发者可以定义自己的FilterInvocationSecurityMetadataSource,代码如下:
@Component
public class CustomFilterInvocationSecurityMetadataSource
implements FilterInvocationSecurityMetadataSource {
// 创建一个AnipathMatcher,主要用来实现ant风格的URL匹配。
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Autowired
MenuMapper menuMapper;
@Override
public Collection<ConfigAttribute> getAttributes(Object object)
throws IllegalArgumentException {
// 从参数中提取出当前请求的URL
String requestUrl = ((FilterInvocation) object).getRequestUrl();
// 从数据库中获取所有的资源信息,即本案例中的menu表以及menu所对应的role
// 在真实项目环境中,开发者可以将资源信息缓存在Redis或者其他缓存数据库中。
List<Menu> allMenus = menuMapper.getAllMenus();
// 遍历资源信息,遍历过程中获取当前请求的URL所需要的角色信息并返回。
for (Menu menu : allMenus) {
if (antPathMatcher.match(menu.getPattern(), requestUrl)) {
List<Role> roles = menu.getRoles();
String[] roleArr = new String[roles.size()];
for (int i = 0; i < roleArr.length; i++) {
roleArr[i] = roles.get(i).getName();
}
return SecurityConfig.createList(roleArr);
}
}
// 如果当前请求的URL在资源表中不存在相应的模式,就假设该请求登录后即可访问,即直接返回 ROLE_LOGIN.
return SecurityConfig.createList("ROLE_LOGIN");
}
// 该方法用来返回所有定义好的权限资源,Spring Security在启动时会校验相关配置是否正确。
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
// 如果不需要校验,那么该方法直接返回null即可。
return null;
}
// supports方法返回类对象是否支持校验。
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
代码解释:
- 开发者自定义FilterInvocationSecurityMetadataSource主要实现该接口中的getAttributes方法,该方法的参数是一个FilterInvocation, 开发者可以从FilterInvocation 中提取出当前请求的URL,返回值是Collection,表示当前请求URL所需的角色。
- 第6行创建一个AntPathMatcher,主要用来实现ant风格的URL匹配。
- 第15行从参数中提取出当前请求的URL。
- 第19行从数据库中获取所有的资源信息,即本案例中的menu表以及menu所对应的role,在真实项目环境中,开发者可以将资源信息缓存在Redis或者其他缓存数据库中。
- 第22~31行遍历资源信息,遍历过程中获取当前请求的URL所需要的角色信息并返回。如果当前请求的URL在资源表中不存在相应的模式,就假设该请求登录后即可访问,即直接返回ROLE LOGIN。
- getAllConfigAttributes方法用来返回所有定义好的权限资源,Spring Security在启动时会校验相关配置是否正确,如果不需要校验,那么该方法直接返回null即可。
- supports方法返回类对象是否支持校验。
当一个请求走完FilterInvocationSecurityMetadataSource中的getAttributes方法后,接下来就会来到AccessDecisionManager类中进行角色信息的比对,自定义AccessDecisionManager如下:
@Component
public class CustomAccessDecisionManager
implements AccessDecisionManager {
// 该方法判断当前登录的用户是否具备当前请求URL所需要的角色信息
@Override
public void decide(Authentication auth,
Object object,
Collection<ConfigAttribute> ca){
Collection<? extends GrantedAuthority> auths = auth.getAuthorities();
// 如果具备权限,则不做任何事情即可
for (ConfigAttribute configAttribute : ca) {
// 如果需要的角色是ROLE_LOGIN,说明当前请求的URL用户登录后即可访问
// 如果auth是UsernamePasswordAuthenticationToken的实例,说明当前用户已登录,该方法到此结束
if ("ROLE_LOGIN".equals(configAttribute.getAttribute())
&& auth instanceof UsernamePasswordAuthenticationToken) {
return;
}
// 否则进入正常的判断流程
for (GrantedAuthority authority : auths) {
// 如果当前用户具备当前请求需要的角色,那么方法结束。
if (configAttribute.getAttribute().equals(authority.getAuthority())) {
return;
}
}
}
// 如果不具备权限,就抛出AccessDeniedException异常
throw new AccessDeniedException("权限不足");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
代码解释:
- 自定义AccessDecisionManager并重写decide 方法,在该方法中判断当前登录的用户是否具备当前请求URL所需要的角色信息,如果不具备,就抛出AccessDeniedException 异常,否则不做任何事即可。
- decide方法有三个参数,第一个参数包含当前登录用户的信息;第二个参数则是一个FilterInvocation对象,可以获取当前请求对象等;第三个参数就是FilterInvocationSecurityMetadataSource中的gettributes方法的返回值,即当前请求URL所需要的角色。
- 第7~32行进行角色信息对比,如果需要的角色是ROLE_LOGIN,说明当前请求的URL用户登录后即可访问,如果auth是UsernamePasswordAuthenticationToken的实例,那么说明当前用户已登录,该方法到此结束,否则进入正常的判断流程,如果当前用户具备当前请求需要的角色,那么方法结束。
当然,本案例还涉及MenuMapper和MenuMapper.xml,实现如下:
@Mapper
public interface MenuMapper {
List<Menu> getAllMenus();
}
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="suohechuan.testforever.MenuMapper">
<resultMap id="BaseResultMap" type="suohechuan.testforever.model.Menu">
<id property="id" column="id"/>
<result property="pattern" column="pattern"/>
<collection property="roles" ofType="suohechuan.testforever.model.Role">
<id property="id" column="rid"/>
<result property="name" column="rname"/>
<result property="nameZh" column="rnameZh"/>
collection>
resultMap>
<select id="getAllMenus" resultMap="BaseResultMap">
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 mr.`rid`=r.`id`
select>
mapper>
8. 配置 Spring Security
这里与前文的配置相比,主要是修改了 configure(HttpSecurity http) 方法的实现并添加了两个 Bean。至此我们边实现了动态权限配置,权限和资源的关系可以在 menu_role 表中动态调整。
@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserService userService;
// 指定密码的加密方式
@SuppressWarnings("deprecation")
@Bean
PasswordEncoder passwordEncoder(){
// 使用BCrypt进行加密
return new BCryptPasswordEncoder(10);
}
// 配置用户及其对应的角色
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
// 配置 URL 访问权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setSecurityMetadataSource(cfisms());
object.setAccessDecisionManager(cadm());
return object;
}
})
.and().formLogin().loginProcessingUrl("/login").permitAll()//开启表单登录并配置登录接口
.and().csrf().disable(); // 关闭csrf
}
@Bean
CustomFilterInvocationSecurityMetadataSource cfisms() {
return new CustomFilterInvocationSecurityMetadataSource();
}
@Bean
CustomAccessDecisionManager cadm() {
return new CustomAccessDecisionManager();
}
}
代码解释:
- 本案例 WebSecurityConfig类的定义是对10.2节中WebSecurityConfig定义的补充,主要是修改了configure(HttpSecurity htp)方法的实现并添加了两个Bean。
- 第9、10行,在定义FilterSecurityInterceptor时,将我们自定义的两个实例设置进去即可。
配置完成后,接下来就可以创建Controller进行测试了,测试方式与Security上节致,这里不再赘述。
@RestController
public class HelloController {
@GetMapping("/admin/hello")
public String admin() {
return "hello admin!";
}
@GetMapping("/user/hello")
public String user() {
return "hello user! ";
}
@GetMapping(" /db/hello")
public String dba() {
return "hello dba!";
}
@GetMapping("/hello")
public String hello() {
return "hello!";
}
}
经过上面的配置,我们已经实现了动态配置权限,权限和资源的关系可以在menu_ role 表中动态调整。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)