需求缘起

       在上一篇我们通过扩展access()的SpEL表达式实现了

动态权限控制,本节将通过AccessDesionManager进行实现动态权限,

代码是基于《基于URL动态权限:准备工作》在往下编码。

 

Spring Security的简单原理

SpringSecurity使用众多的拦截器对url拦截,以此来管理权限,这里主要讲里面核心流程的两个:

(1)登陆验证拦截器

AuthenticationProcessingFilter。

(2)资源管理拦截器

AbstractSecurityInterceptor。

对于拦截器里面的实现是由

AuthenticationManager、accessDecisionManager

等组件来支撑的。

       对于这个基本的认知,我们来理一下整个流程:

(1)用户登陆,会被 AuthenticationProcessingFilter拦截;

(2)AuthenticationProcessingFilter会调用AuthenticationManager的实现;

(3)AuthenticationManager会调用ProviderManager来获取用户验证信息(不同的Provider调用的服务不同,因为这些信息可以是在数据库上,可以是在LDAP服务器上,可以是xml配置文件上等),如果验证通过后会将用户的权限信息封装一个User放到spring的全局缓存SecurityContextHolder中,以备后面访问资源时使用;

(4)访问资源(即授权管理),访问url时,会通过AbstractSecurityInterceptor拦截器拦截;

(5)会调用FilterInvocationSecurityMetadataSource的方法来获取被拦截url所需的全部权限;

(6)再调用授权管理器AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的策略(有:一票决定,一票否定,少数服从多数等),如果权限足够,则返回,权限不够则报错并调用权限不足页面。

       这里我们先有一个基本的认识,主要是为了方便我们编码的理解,在之后的章节中我们重点介绍下SpringSecurity的原理。

 

编码思路

       对于URL动态权限配置,主要解决如下几个问题:

(1)基于URL的用户权限信息保存在哪里:需要定义一张权限表,保存权限信息,然后角色和权限有一个关联关系。

(2)怎么加载用户的权限信息:仍然是通过loadUserByUsername进行加载,用户的权限信息这块的编码不变。

(3)URL对应的权限配置:这个主要是通过FilterInvocationSecurityMetadataSource进行配置。

(4)如何决定某一个用户是否有权限访问某个URL : 自定义AccessDecisionManager类的decide方法,决定某一个用户是否有权限访问某个URL。

(5)如何使用自定义的处理器:使用HttpSecurity的withObjectPostProcessor进行指定。

 

一、基于URL动态权限配置

       接下来我们看下具体的编码步骤,我们的代码在基本工作之后进行编码,所以权限的实体类,初始化数据就不重复说明了。

1.1 加载权限信息

       继承FilterInvocationSecurityMetadataSource重写getAttributes的方法进行通过uri获取权限配置信息:

 

package com.kfit.config;

import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.kfit.permission.service.PermissionService;

public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource{

    @Autowired
    private PermissionService permissionService;

    /**
     * 此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法。
     * object-->FilterInvocation
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        System.out.println("MyFilterInvocationSecurityMetadataSource.getAttributes()");

        Map<String, Collection<ConfigAttribute>> map = permissionService.getPermissionMap();
        FilterInvocation filterInvocation = (FilterInvocation) object;
        System.out.println(filterInvocation.getFullRequestUrl());

        if (isMatcherAllowedRequest(filterInvocation)) return null ; //return null 表示允许访问,不做拦截

        HttpServletRequest request = filterInvocation.getHttpRequest();
        String resUrl;
        //URL规则匹配.
        AntPathRequestMatcher matcher;
        for(Iterator<String> it  = map.keySet().iterator();it.hasNext();) {
            resUrl = it.next();
            matcher = new AntPathRequestMatcher(resUrl);
            if(matcher.matches(request)) {
                System.out.println(map.get(resUrl));
                return map.get(resUrl);
            }
        }
        //SecurityConfig.createList("ROLE_USER");
         //方式一:没有匹配到,直接是白名单了.不登录也是可以访问的。
        //return null;

        //方式二:配有匹配到,需要指定相应的角色:
        return SecurityConfig.createList("ROLE_admin");
    }




    /**
     * 判断当前请求是否在允许请求的范围内
     * @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/**");
    }

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

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }

}

 

1.2 权限校验

       继承AccessDecisionManager重写decide方法进行编码:

package com.kfit.config;

import java.util.Collection;
import java.util.Iterator;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;

public class MyAccessDecisionManager implements AccessDecisionManager{

    /**
     * 方法是判定是否拥有权限的决策方法,
     * (1)authentication 是释CustomUserService中循环添加到 GrantedAuthority 对象中的权限信息集合.
     * (2)object 包含客户端发起的请求的requset信息,可转换为 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
     * (3)configAttributes 为MyFilterInvocationSecurityMetadataSource的getAttributes(Object object)这个方法返回的结果,此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法
     */
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
            throws AccessDeniedException, InsufficientAuthenticationException {
        System.out.println("MyAccessDecisionManager.decide()");
        if(configAttributes == null  || configAttributes.size()==0) {
            throw new AccessDeniedException("permission denied");
        }

        ConfigAttribute cfa;
        String needRole;
        //遍历基于URL获取的权限信息和用户自身的角色信息进行对比.
        for(Iterator<ConfigAttribute> it=configAttributes.iterator();it.hasNext();) {
            cfa = it.next();
            needRole = cfa.getAttribute();
            System.out.println("decide,needRole:"+needRole+",authentication="+authentication);
            //authentication 为CustomUserDetailService中添加的权限信息.
            for(GrantedAuthority grantedAuthority:authentication.getAuthorities()) {
                if(needRole.equals(grantedAuthority.getAuthority())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("permission denied");
    }



    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }

}

 

 

1.3 指定配置类

       在WebSecurityConfig类中指定我们刚编码的配置类,核心类是使用withObjectPostProcessor进行指定:

package com.kfit.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests() // 定义哪些URL需要被保护、哪些不需要被保护
            .antMatchers("/login").permitAll()// 设置所有人都可以访问登录页面
            .antMatchers("/","/index").permitAll()
            .antMatchers("/test/**","/test1/**").permitAll()
            .antMatchers("/res/**/*.{js,html}").permitAll()
            .withObjectPostProcessor(new MyObjectPostProcessor())
            .anyRequest().authenticated()  // 任何请求,登录后可以访问
            .and()
            .formLogin().loginPage("/login")
            ;

    }

    @Bean  
    public PasswordEncoder passwordEncoder() {  
        return new BCryptPasswordEncoder();  
    } 

    @Bean
    public FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource() {
        return new MyFilterInvocationSecurityMetadataSource();
    }
    @Bean
    public MyAccessDecisionManager accessDecisionManager() {
        return new MyAccessDecisionManager();
    }

    private class MyObjectPostProcessor implements ObjectPostProcessor<FilterSecurityInterceptor> {
        @Override
        public <O extends FilterSecurityInterceptor> O postProcess(O fsi) {
            fsi.setSecurityMetadataSource(filterInvocationSecurityMetadataSource());
            fsi.setAccessDecisionManager(accessDecisionManager());
            return fsi;
        }

    }
}

       到这里就可以测试下效果了,和之前的结果应该是一样的。