Spring Boot 之 Spring Security高级配置(权限和资源的关系)

Spring Security高级配置(权限和资源的关系)

角色继承

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     | $10$Y7TCbZ.De3suSD.4boCArugDit4hyOgkywUMuLUquc7OZuL6sTAR.    |      1 |      0 |
|  2 | admin    | $10$Y7TCbZ.De3suSD.4boCArugDit4hyOgkywUMuLUquc7OZuL6sTAR.    |      1 |      0 |
|  3 | suo      | $2a$10$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.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.2</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.8</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>
<build>
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
        </resource>
        <resource>
            <directory>src/main/resources</directory>
            <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);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--
 namespace:必须有值,自定义的唯一字符串 
 推荐使用:dao 接口的全限定名称 
-->
<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方法返回类对象是否支持校验。

7.自定义AccessDecisionManager

当一个请求走完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();
}
<?xml version="1.0" encoding="UTF-8" ?>
<!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 表中动态调整。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值