Spring Security基于数据库配置权限(角色,路径)

传统的后台管理系统,在权限处理上通常5个表:用户表角色表资源表用户角色关联表角色资源关联表。现在为了避免重复造轮子,自己写拦截处理,我们可以使用Spring Security来做权限控制。
Spring Security官方推荐通过配置来实现角色和资源的对应,这样的问题是假如需要线上配置角色与资源对应就不行了,所以下面讲讲如何基于数据库中存储角色资源用户 来使用Spring Security做权限控制。

1.首先是数据准备

创建权限相关的五张表和对应的实体类。以及相关的查询实现等。

2.配置类SecurityConfiguration

只要使用spring security都要有这样一个配置类(如果是类配置的话),继承WebSecurityConfigurerAdapter 主要实现两个方法configure(AuthenticationManagerBuilder auth)

configure(HttpSecurity http)

  • AuthenticationManagerBuilder 主要配置身份认证来源,也就是用户及其角色。

  • HttpSecurity 主要配置路径,也就是资源的访问权限(是否需要认证,需要什么角色等)。

2.1 configure(AuthenticationManagerBuilder auth) 相关

该方法本身只需要设置一个身份认证来源,所以我们需要先写个方法来创建该来源

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); //创建密码加密对象
    }

    /**
     * @return 封装身份认证提供者
     */
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(customUserService);  //自定义的用户和角色数据提供者
        authenticationProvider.setPasswordEncoder(passwordEncoder()); //设置密码加密对象
        return authenticationProvider;
    }

     @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider()); //设置身份认证提供者
    }

自定义的类需要实现UserDetailsService
实现接口中的方法

UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;

UserDetails中包含用户的用户名,密码,角色等信息。所以需要自己创建一个类实现UserDetails,这样方便我们赋值我们查到的角色集合
比如创建一个UserPrincipal

public class UserPrincipal implements UserDetails {

    private User user;

    private List<String> roleCodes;

    UserPrincipal(User user,List<String> roleCodes){
        this.user = user;
        this.roleCodes = roleCodes;
    }

   /**
    * 设置用户角色集合
    */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roleCodes.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }
    ......
}

CustomUserService 提供用户和对应的角色

public class CustomUserService implements UserDetailsService {


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User user = userRepository.findByUsername(username); //根据用户名查询用户
        if (user == null) {
            throw new UsernameNotFoundException("can not found username " + username);
        }
        return new UserPrincipal(user,getRoleCodes(user));
    }

    private List<String> getRoleCodes(User user){
       return ....//根据用户查询该用户拥有的角色编号集合
    }
}

这样就完成了对用户和角色的查询,接下来就是配置路径(资源)和角色映射了

2.2 configure(HttpSecurity http)相关

主要就配置登陆路径,错误页面路径,后置处理程序等。

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().loginPage("/login").defaultSuccessUrl("/",true)
                .and()
                .exceptionHandling().accessDeniedPage("/error")
                .and()
                .csrf().disable()
                .authorizeRequests().anyRequest().authenticated()
                .withObjectPostProcessor(postProcessor);//设置后置处理程序对象
    }

加上前面的代码,配置类基本就完成了,记得加上@Configuration注解。接下来就是利用角色对路径进行判断了,这部分处理都封装在postProcessor 中的 FilterSecurityInterceptor中。

3.权限处理配置 postProcessor

现在需要定义一个类来实现 ObjectPostProcessor<FilterSecurityInterceptor>

public class CustomPostProcessor implements ObjectPostProcessor<FilterSecurityInterceptor> {

    @Override
    public <T extends FilterSecurityInterceptor> T postProcess(T fsi) {
        fsi.setAccessDecisionManager(accessDecisionManager); //权限决策处理类
        fsi.setSecurityMetadataSource(filterSecurityMetadataSource); //路径(资源)拦截处理
        return fsi;
    }
}
3.1 路径拦截处理类

filterSecurityMetadataSource 需要实现 FilterInvocationSecurityMetadataSource接口,Collection<ConfigAttribute> getAttributes(Object object)会返回改路径所需的角色集合到权限决策处理类中供其使用.


public class CustomFilterSecurityMetadataSource implements FilterInvocationSecurityMetadataSource{

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        FilterInvocation fi = (FilterInvocation) object; //当前请求对象
        if (isMatcherAllowedRequest(fi)) return null ; //return null 表示允许访问,不做拦截
        List<ConfigAttribute> configAttributes = getMatcherConfigAttribute(fi.getRequestUrl());
        return configAttributes.size() > 0 ? configAttributes : deniedRequest(); //返回当前路径所需角色,如果没有则拒绝访问
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class< ? > aClass) {
        return FilterInvocation.class.isAssignableFrom(aClass);
    }

    /**
     * 获取当前路径所需要的角色
     * @param url 当前路径
     * @return 所需角色集合
     */
    private List<ConfigAttribute> getMatcherConfigAttribute(String url){
        return roleResourceRepository.findByResource_ResUrl(url).stream()
                .map(roles -> new SecurityConfig(roles.getRole().getRoleCode()))
                .collect(Collectors.toList());
    }

    /**
     * 判断当前请求是否在允许请求的范围内
     * @param fi 当前请求
     * @return 是否在范围中
     */
    private boolean isMatcherAllowedRequest(FilterInvocation fi){
        return allowedRequest().stream().map(AntPathRequestMatcher::new)
                .filter(requestMatcher -> requestMatcher.matches(fi.getHttpRequest()))
                .toArray().length > 0;
    }

    /**
     * @return 定义允许请求的列表
     */
    private List<String> allowedRequest(){
        return Arrays.asList("/login","/css/**","/fonts/**","/js/**","/scss/**","/img/**");
    }

    /**
     * @return 默认拒绝访问配置
     */
    private List<ConfigAttribute> deniedRequest(){
        return Collections.singletonList(new SecurityConfig("ROLE_DENIED"));
    }
}
3.2 权限决策处理

accessDecisionManager需要实现 AccessDecisionManager接口。实现方法decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) 来做权限决策

  • authentication可获取当前用户拥有的角色集合
  • configAttributes 路径拦截处理中查询到的,能访问当前路径的角色集合
public class CustomAccessDecisionManager implements AccessDecisionManager {

    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        if(authentication == null){
            throw new AccessDeniedException("permission denied");
        }

        //当前用户拥有的角色集合
        List<String> roleCodes = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());

        //访问路径所需要的角色集合
        List<String> configRoleCodes = configAttributes.stream().map(ConfigAttribute::getAttribute).collect(Collectors.toList());
        for (String roleCode : roleCodes){
            if(configRoleCodes.contains(roleCode)){
                return;
            }
        }
        throw new AccessDeniedException("permission denied");
    }
}

这样就完成了基于数据库存储的Spring Security权限控制实现。

Logo

快速构建 Web 应用程序

更多推荐