一、大背景
最近做的自动化测试平台需要进行重构,将原有的系统拆分成几个独立的子系统,我负责用户系统的开发,同时需要兼容老系统,我的头希望我采用spring security来进行权限控制和管理。有以下几个问题需要解决:
1、如何兼容已有的老的权限体系。
2、用户系统登录之后,如何将认证信息同步到其它子系统。
二、调研
还是按照惯例了解一下spring security到底是什么东西,基本的原理到底是什么。在网上google和看了官方文档之后,发现spring security核心就是一条filter链表。请求过来的时候,会按照一定的顺序逐步通过这些filter,每个filter都验证通过之后,就会到达请求目标,否则就会抛出异常。
拿登陆为例,负责登陆的是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);
}
}