一、大背景
最近做的自动化测试平台需要进行重构,将原有的系统拆分成几个独立的子系统,我负责用户系统的开发,同时需要兼容老系统,我的头希望我采用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); } }
本质上来说与manager没啥区别,只是一个在上层处理掉一个在下一层处理掉,效果基本上以上,附上源码地址:git