一、大背景

  最近做的自动化测试平台需要进行重构,将原有的系统拆分成几个独立的子系统,我负责用户系统的开发,同时需要兼容老系统,我的头希望我采用spring security来进行权限控制和管理。有以下几个问题需要解决:

  1、如何兼容已有的老的权限体系。

  2、用户系统登录之后,如何将认证信息同步到其它子系统。

二、调研

还是按照惯例了解一下spring security到底是什么东西,基本的原理到底是什么。在网上google和看了官方文档之后,发现spring security核心就是一条filter链表。请求过来的时候,会按照一定的顺序逐步通过这些filter,每个filter都验证通过之后,就会到达请求目标,否则就会抛出异常。

  

springboot activiti 跳过security认证_ide

  拿登陆为例,负责登陆的是UsernamePasswordAuthenticationFilter,请求到达这个过滤器之后,过滤器会把处理丢给认证管理器authenticationManager,认证管理器在把请求丢给Provider,而Provider把内容丢给了DaoAuthenticationProvider,DaoAuthenticationProvider在通过UserDetailService获取响应的用户名和密码。这里就是唯一需要编写代码的地方,也就是实现UserDetailService接口。我们也可以自动以provider

  其他过滤器的内容基本上都类似。贴上各个filter的作用和名称

过滤器名称

描述

o.s.s.web.context.SecurityContextPersistenceFilter

负责从SecurityContextRepository获取或存储SecurityContext。SecurityContext代表了用户安全和认证过的session。

o.s.s.web.authentication.logout.LogoutFilter

监控一个实际为退出功能的URL(默认为/j_spring_security_logout),并且在匹配的时候完成用户的退出功能。

o.s.s.web.authentication.UsernamePasswordAuthenticationFilter

监控一个使用用户名和密码基于form认证的URL(默认为/j_spring_security_check),并在URL匹配的情况下尝试认证该用户。

o.s.s.web.authentication.ui.DefaultLoginPageGeneratingFilter

监控一个要进行基于forn或OpenID认证的URL(默认为/spring_security_login),并生成展现登录form的HTML

o.s.s.web.authentication.www.BasicAuthenticationFilter

监控HTTP 基础认证的头信息并进行处理

o.s.s.web.savedrequest.

RequestCacheAwareFilter

用于用户登录成功后,重新恢复因为登录被打断的请求。

o.s.s.web.servletapi.

SecurityContextHolderAwareRequest

Filter

用一个扩展了HttpServletRequestWrapper的子类(o.s.s.web. servletapi.SecurityContextHolderAwareRequestWrapper)包装HttpServletRequest。

它为请求处理器提供了额外的上下文信息。

o.s.s.web.authentication.

AnonymousAuthenticationFilter

如果用户到这一步还没有经过认证,将会为这个请求关联一个认证的token,标识此用户是匿名的。

o.s.s.web.session.

SessionManagementFilter

根据认证的安全实体信息跟踪session,保证所有关联一个安全实体的session都能被跟踪到。

o.s.s.web.access.

ExceptionTranslationFilter

解决在处理一个请求时产生的指定异常

o.s.s.web.access.intercept.

FilterSecurityInterceptor

简化授权和访问控制决定,委托一个AccessDecisionManager完成授权的判断

三、开撸

  1、导入依赖:我这边用的是springboot,所以使用spring-security 都比较简单,导入对应的依赖即可,如下



<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>



  2、配置WebSecurityConfig



import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
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.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests()
                .antMatchers("/login").permitAll()
                .antMatchers("/index").hasRole("Admin");
    }


    /**
     * 权限不通过的处理
     */
    public static class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request,
                             HttpServletResponse response,
                             AuthenticationException authException) throws IOException {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
                    "Authentication Failed: " + authException.getMessage());
        }
    }

}



  3、自定义登陆:因为需要兼容老的权限体系,所以不想采用spring security 的登陆体系,即采用实现UserDetailService的方式来做,想通过自定义的登陆来实现。找了各方面的资料,发现可以通过获取spring security的上下文,向里面设置认证信息,并将认证信息保存到session中,就可以是实现完全自定义的登陆过程,示例代码如下:



import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.List;

/**
 * Created by tangrubei on 2018/4/2.
 */
@Controller
@CrossOrigin(origins = "*", maxAge = 3600)
public class MyController {

    


    @GetMapping(value = "login")
    @ResponseBody
    public void Login(HttpServletRequest request,@RequestParam String userName,@RequestParam String password){
        
        if("zhangsan".equals(userName)&&"123456".equals(password)){
//            设置角色
            List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_"+"Admin");
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(userName, password, authorities);
            SecurityContextHolder.getContext().setAuthentication(authRequest);
            HttpSession session = request.getSession();
            session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());
        }
    }
}



  4、自定义的登陆我们实现了,老的系统中,我们的角色和url是在数据库里面动态配置。不能将这些配置直接写死在WebSecurityConfig里或者配置文件里,如果有变更就需要重新进行配置,然后重启应用,这个就太low了,不符合实际场景的应用,我们换另一种方法。在前面的filter列表中我们知道FilterSecurityInterceptor是进行授权和访问控制的,因此我们需要考虑重写AccessDecisionManager或者重写AccessDecisionVoter,我们这边先说第一种

  5、定义AccessDecisionManager的实现类并编写自己的决策逻辑,这里为了方便表示,写了一段伪代码,可以根据后续的需求从数据库中获取或者是别的地方获取



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;
import org.springframework.security.web.FilterInvocation;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;


/**
 * Created by tangrubei on 2018/3/28.
 */
@Component
public class AppAccessDecisionManager
        implements AccessDecisionManager {

    private static Map urlMap;

    static {
        urlMap = new HashMap();
        urlMap.put("/index", "ROLE_Admin");
        urlMap.put("/login", "permitAll");

    }


    @Override
    public void decide(Authentication authentication, Object object,
                       Collection<ConfigAttribute> configAttributes)
            throws AccessDeniedException, InsufficientAuthenticationException {

        if (configAttributes == null) {
            return;
        }

        String url = ((FilterInvocation) object).getRequestUrl();
        if (url.indexOf("?") != -1) {
            url = url.substring(0, url.indexOf("?"));
        }
        String needRole = (String) urlMap.get(url);
        if ("permitAll".equals(needRole)) {
            return;
        } else {
            for (GrantedAuthority ga : authentication.getAuthorities()) {
                if (needRole.trim().equals(ga.getAuthority().trim())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("");

    }

    @Override
    public boolean supports(ConfigAttribute attribute) {

        return true;

    }

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

    }


}



修改WebSecurityConfig



@Autowired
    private AccessDecisionManager accessDecisionManager;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable().authorizeRequests().antMatchers("/*").authenticated()
                .accessDecisionManager(accessDecisionManager);

//        http.csrf().disable().authorizeRequests()
//                .antMatchers("/login").permitAll()
//                .antMatchers("/index").hasRole("Admin").accessDecisionManager(accessDecisionManager);
    }



 

到此,我们就可以自定义登陆,自定义访问策略,同时使用spring security的安全机制。最后一个问题,关于各个子系统认证的问题,我们可以采用session共享的方式来实现,这个比较简单,这里不在复述。  

  

 

  虽然已经解决了目前所有的问题,但是我们还是可以实践一下实现AccessDecisionVetor这个来实现决策自定义。代码如下:



import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by tangrubei on 2018/4/3.
 */
public class MyVoter implements AccessDecisionVoter {

    private static Map urlMap;

    static {
        urlMap = new HashMap();
        urlMap.put("/index", "ROLE_Admin");
        urlMap.put("/login", "permitAll");

    }

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

    @Override
    public int vote(Authentication authentication, Object o, Collection collection) {
        String url = ((FilterInvocation) o).getRequestUrl();
        if (url.indexOf("?") != -1) {
            url = url.substring(0, url.indexOf("?"));
        }
        String needRole = (String) urlMap.get(url);
        if ("permitAll".equals(needRole)) {
            return ACCESS_GRANTED;
        } else {
            for (GrantedAuthority ga : authentication.getAuthorities()) {
                if (needRole.trim().equals(ga.getAuthority().trim())) {
                    return ACCESS_GRANTED;
                }
            }
        }
        return ACCESS_DENIED;
    }

    @Override
    public boolean supports(Class aClass) {
        return true;
    }
}



  写一个对应的配置bean或者在xml里面配置,代码如下:



import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.vote.UnanimousBased;
import spring.boot.security.manager.MyVoter;

import java.util.Arrays;
import java.util.List;

/**
 * Created by tangrubei on 2018/4/3.
 */
@Configuration
public class BeanConfig {
    @Bean(name = "voter")
    public AccessDecisionManager accessDecisionManager(){
        List<AccessDecisionVoter<? extends Object>> decisionVoters
                = Arrays.asList(new MyVoter() );
        return new UnanimousBased(decisionVoters);
    }
    
}