SpringSecurity 是基于 Spring AOP 和 Servlet 过滤器的安全框架,提供全面的安全性解决方案。
Spring Security核心功能包括用户认证(Authentication)、用户授权(Authorization)和攻击防护3个部分:
- 用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程
- 用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限
- 攻击防护即防止伪造身份
Spring security大量使用了责任链和委托的代码设计风格,过滤器负责对请求进行安全校验和设置,某个过滤器涉及认证或授权时,认证/授权具体实现委派给认证管理器和授权管理器,过滤器不负责具体实现
SpringSecurity过滤器链采用的是责任链的设计模式,它有一条很长的过滤器链:
Spring Security Filter并不是直接嵌入到 Web Filter中的,而是通过 FilterChainProxy来统一管理 Spring Security Filter,FilterChainProxy本身则通过Spring提供的DelegatingFilterProxy代理过滤器嵌入到Servlet Filter 之中
问题:在Spring MVC应用中,需要先启动Servlet容器再启动Spring容器,servlet过滤器位于spring容器前无法被spring容器管理(例如,无法在实现Filter接口的类中使用@Value和@Autowire注解)
Spring 提供了一个名为DelegatingFilterProxy
的Filter
实现。这个 Servet 在 Servlet 容器的生命周期和 Spring 的 ApplicationContext 之间建立了桥接。Servlet 容器用自己的标准注册 Filter,但它对 Spring Bean 无感知。 DelegatingFilterProxy 通过标准 Servlet 容器机制注册到 Servlet 中,但将所有工作都委托给了实现 Filter 的 Spring Bean
DelegatingFilterProxy 伪代码如下:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { // Lazily get Filter that was registered as a Spring Bean // For the example in DelegatingFilterProxy, delegate is an instance of Bean Filter0 Filter delegate = getFilterBean(someBeanName); // delegate work to the Spring Bean delegate.doFilter(request, response); }
DelegatingFilterProxy通过过滤器名获取bean,并委托bean进行请求处理
Spring Security 对 Servlet 的支持包含在 FilterChainProxy。 FilterChainProxy 是 Spring Security 提供的一个特殊的 Filter。它通过过滤功能代理给 SecurityFilterChain 维护的一组Filter链
当请求到达 FilterChainProxy 之后,FilterChainProxy 会根据请求的路径,将请求转发到不同的 Spring Security Filters 上面去,不同的 Spring Security Filters 对应了不同的过滤器,也就是不同的请求将经过不同的过滤器
// FilterChainProxy源码 private final static String FILTER_APPLIED = FilterChainProxy.class.getName().concat( ".APPLIED"); private List<SecurityFilterChain> filterChains; private FilterChainValidator filterChainValidator = new NullFilterChainValidator(); private HttpFirewall firewall = new StrictHttpFirewall(); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { boolean clearContext = request.getAttribute(FILTER_APPLIED) == null; if (clearContext) { try { request.setAttribute(FILTER_APPLIED, Boolean.TRUE); doFilterInternal(request, response, chain); } finally { SecurityContextHolder.clearContext(); request.removeAttribute(FILTER_APPLIED); } } else { doFilterInternal(request, response, chain); } } private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FirewalledRequest fwRequest = firewall .getFirewalledRequest((HttpServletRequest) request); HttpServletResponse fwResponse = firewall .getFirewalledResponse((HttpServletResponse) response); List<Filter> filters = getFilters(fwRequest); if (filters == null || filters.size() == 0) { if (logger.isDebugEnabled()) { logger.debug(UrlUtils.buildRequestUrl(fwRequest) + (filters == null ? " has no matching filters" : " has an empty filter list")); } fwRequest.reset(); chain.doFilter(fwRequest, fwResponse); return; } VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters); vfc.doFilter(fwRequest, fwResponse); } private List<Filter> getFilters(HttpServletRequest request) { for (SecurityFilterChain chain : filterChains) { if (chain.matches(request)) { return chain.getFilters(); } } return null; }
filterChains
不是某个过滤器,而是多个过滤器链的集合
在 doFilter 方法中,正常来说,clearContext 参数每次都是 true,于是每次都先给 request 标记上 FILTER_APPLIED 属性,然后执行 doFilterInternal 方法去走过滤器,执行完毕后,最后在 finally 代码块中清除 SecurityContextHolder 中保存的用户信息,同时移除 request 中的标记
doFilterInternal
方法:
private static class VirtualFilterChain implements FilterChain { private final FilterChain originalChain; private final List<Filter> additionalFilters; private final FirewalledRequest firewalledRequest; private final int size; private int currentPosition = 0; private VirtualFilterChain(FirewalledRequest firewalledRequest, FilterChain chain, List<Filter> additionalFilters) { this.originalChain = chain; this.additionalFilters = additionalFilters; this.size = additionalFilters.size(); this.firewalledRequest = firewalledRequest; } @Override public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if (currentPosition == size) { if (logger.isDebugEnabled()) { logger.debug(UrlUtils.buildRequestUrl(firewalledRequest) + " reached end of additional filter chain; proceeding with original chain"); } // Deactivate path stripping as we exit the security filter chain this.firewalledRequest.reset(); originalChain.doFilter(request, response); } else { currentPosition++; Filter nextFilter = additionalFilters.get(currentPosition - 1); if (logger.isDebugEnabled()) { logger.debug(UrlUtils.buildRequestUrl(firewalledRequest) + " at position " + currentPosition + " of " + size + " in additional filter chain; firing Filter: '" + nextFilter.getClass().getSimpleName() + "'"); } nextFilter.doFilter(request, response, this); } } }
currentPosition == size
,表示过滤器链已经执行完毕,此时通过调用 originalChain.doFilter 进入到原生过滤链方法中,同时也退出了 Spring Security 过滤器链。否则就从 additionalFilters 取出 Spring Security 过滤器链中的一个个过滤器,挨个调用 doFilter 方法SecurityFilterChain中的 Filter是 Spring Bean,它们是注册 FilterChainProxy 中,而不是在 DelegatingFilterProxy 注册的。相比较直接向 Servlet 容器或 DelegatingFilterProxy 注册,FilterChainProxy 有许多优势。
多个SecurityFilterChain, FilterChainProxy 使用第一个匹配的 SecurityFilterChain进行请求过滤
多过滤器链配置示例:
@Configuration public class SecurityConfig { @Configuration @Order(1) static class DefaultWebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.antMatcher("/foo/**") .authorizeRequests() .anyRequest().hasRole("admin") .and() .csrf().disable(); } } @Configuration @Order(2) static class DefaultWebSecurityConfig2 extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.antMatcher("/bar/**") .authorizeRequests() .anyRequest().hasRole("user") .and() .formLogin() .permitAll() .and() .csrf().disable(); } } }
UsernamePasswordAuthenticationFilter
负责表单认证,继承自AbstractAuthenticationProcessingFilter
抽象类。
其父类AbstractAuthenticationProcessingFilter
的doFilte
方法是一个模板方法,定义了认证的流程:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { // 根据请求路径,判断是否需要认证,不需要认证直接调用下个过滤器 if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } try { // 返回请求认证,UsernamePasswordAuthenticationFilter实现此方法 Authentication authenticationResult = attemptAuthentication(request, response); // token为空直接返回 if (authenticationResult == null) { // return immediately as subclass has indicated that it hasn't completed return; } // 会话相关策略设置 this.sessionStrategy.onAuthentication(authenticationResult, request, response); // Authentication success // 认证后是否继续调用下个过滤器,默认false if (this.continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } // 钩子,提供扩展点 successfulAuthentication(request, response, chain, authenticationResult); } catch (InternalAuthenticationServiceException failed) { this.logger.error("An internal error occurred while trying to authenticate the user.", failed); unsuccessfulAuthentication(request, response, failed); } catch (AuthenticationException ex) { // Authentication failed unsuccessfulAuthentication(request, response, ex); } }
UsernamePasswordAuthenticationFilter
实现attemptAuthentication
方法:
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { // 如果不是post请求,抛出异常 if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } //从请求中获取用户名、密码 String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); // 构建token,现在token还未认证 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property // 把请求中的远传地址等信息设置到token中 setDetails(request, authRequest); // 获取认证管理器,并委派认证管理器进行认证,返回认证token(此时,token携带认证是否成功信息) return this.getAuthenticationManager().authenticate(authRequest); }
在 Spring Security 中,用来处理身份认证的类是 AuthenticationManager,我们也称之为认证管理器。
AuthenticationManager 中规范了 Spring Security 的过滤器要如何执行身份认证,并在身份认证成功后返回一个经过认证的 Authentication 对象。AuthenticationManager 是一个接口,我们可以自定义它的实现,但是通常我们使用更多的是系统提供的 ProviderManager
ProviderManager 是的最常用的 AuthenticationManager 实现类。
ProviderManager 管理了一个 AuthenticationProvider 列表,每个 AuthenticationProvider 都是一个认证器,不同的 AuthenticationProvider 用来处理不同的 Authentication 对象的认证。一次完整的身份认证流程可能会经过多个 AuthenticationProvider。
每一个 ProviderManager 管理多个 AuthenticationProvider,同时每一个 ProviderManager 都可以配置一个 parent,如果当前的 ProviderManager 中认证失败了,还可以去它的 parent 中继续执行认证,所谓的 parent 实例,一般也是 ProviderManager,也就是 ProviderManager 的 parent 还是 ProviderManager
一个系统中,我们可以配置多个 HttpSecurity(多个过滤器链),而每一个 HttpSecurity 都有一个对应的 AuthenticationManager 实例(局部 AuthenticationManager),这些局部的 AuthenticationManager 实例都有一个共同的 parent,那就是全局的 AuthenticationManager。
ProviderManager
类认证方法authenticate
:
public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); // 获取当前认证管理器的所有Provider for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } // 如果token存在对应的Provider,则用此Provider进行认证 result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } // 当前局部认证管理器没认证成功,则调用父认证管理器进行认证 if (result == null && parent != null) { result = parentResult = parent.authenticate(authentication); } if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { ((CredentialsContainer) result).eraseCredentials(); } if (parentResult == null) { eventPublisher.publishAuthenticationSuccess(result); } return result; } throw lastException; }
AuthenticationProvider 定义了 Spring Security 中的验证逻辑:
public interface AuthenticationProvider { Authentication authenticate(Authentication authentication) throws AuthenticationException; boolean supports(Class<?> authentication); }
每个AuthenticationProvider和token一一对应,UsernamePasswordAuthenticationToken对应的Provider是DaoAuthenticationProvider,DaoAuthenticationProvider继承自AbstractUserDetailsAuthenticationProvider,其父类方法authenticate
定义认证逻辑:
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { logger.debug("User '" + username + "' not found"); if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } else { throw notFound; } } } try { preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { if (cacheWasUsed) { cacheWasUsed = false; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } else { throw exception; } } postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } return createSuccessAuthentication(principalToReturn, authentication, user); } public boolean supports(Class<?> authentication) { return (UsernamePasswordAuthenticationToken.class .isAssignableFrom(authentication)); } }
DaoAuthenticationProvider
类主要实现了父类的additionalAuthenticationChecks
方法,定义如何比较密码:
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { @SuppressWarnings("deprecation") protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } String presentedPassword = authentication.getCredentials().toString(); if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } } }
public interface UserDetalls extends Serializble { Collection<? extend GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccontNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
FilterSecurityInterceptor
决定了访问特定路径应该具备的权限,访问的用户的角色,权限是什么?访问的路径需要什么样的角色和权限
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter { // 是否执行过该过滤器的标记 private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied"; // 访问的资源元数据,默认是ExpressionBasedFilterInvocationSecurityMetadataSource private FilterInvocationSecurityMetadataSource securityMetadataSource; // 是否每次只请求一次该过滤器,例如在jsp进行转发的时候,会多次经过该过滤器,这个标记就是用来 // 判断此时需不需要spring-security再进行一次安全检查 private boolean observeOncePerRequest = true; public void init(FilterConfig arg0) throws ServletException { } public void destroy() { } // 过滤方法,实际上是new一个FilterInvocation然后委托给它执行 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); // 核心调用 invoke(fi); } public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() { return this.securityMetadataSource; } public SecurityMetadataSource obtainSecurityMetadataSource() { return this.securityMetadataSource; } public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) { this.securityMetadataSource = newSource; } // 安全对象类型 public Class<?> getSecureObjectClass() { return FilterInvocation.class; } public void invoke(FilterInvocation fi) throws IOException, ServletException { // 如果request不为空并且已经执行过该过滤器并且observeOncePerRequest = true // (只请求一次该过滤器)则过滤器继续往下走,不执行spring-security检查 if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) && observeOncePerRequest) { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } else { // 如果请求不为空并且只请求一次该过滤器,设置已经执行过该过滤器的标记 if (fi.getRequest() != null && observeOncePerRequest) { fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } // 安全对象调用前进行权限判断 InterceptorStatusToken token = super.beforeInvocation(fi); try { // 过滤链继续执行 fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { // 安全对象调用完成后,清理AbstractSecurityInterceptor的工作 super.finallyInvocation(token); } // 安全对象调用完成后,完成AbstractSecurityInterceptor的工作。 super.afterInvocation(token, null); } } public boolean isObserveOncePerRequest() { return observeOncePerRequest; } public void setObserveOncePerRequest(boolean observeOncePerRequest) { this.observeOncePerRequest = observeOncePerRequest; } }
beforeInvocation
方法,执行授权关键操作:
obtainSecurityMetadataSource
方法获取当前请求需要的权限列表accessDecisionManager.decide
授权管理器方法进行授权FilterInvocationSecurityMetadataSource
是一个标记接口,用来获取资源角色元数据,包含3个方法:
Collection getAttributes(Object object)
根据提供的受保护对象的信息,其实就是URI,获取该URI 配置的所有角色Collection getAllConfigAttributes()
获取全部角色boolean supports(Class<?> clazz)
对特定的安全对象是否提供 ConfigAttribute 支持实现实例:
@Component public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { @Autowired MenuService menuService; //从数据库加载url及关联的角色 AntPathMatcher antPathMatcher = new AntPathMatcher(); @Override // 入参object就是受保护的对象 public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { // 获取当前请求路径 String requestURI = ((FilterInvocation) object).getRequest().getRequestURI(); List<Menu> allMenu = menuService.getAllMenu(); // 遍历以查找当前请求路径所需要的角色/权限 for (Menu menu : allMenu) { if (antPathMatcher.match(menu.getPattern(), requestURI)) { String[] roles = menu.getRoles().stream() .map(r -> r.getName()).toArray(String[]::new); return SecurityConfig.createList(roles); } } return null; } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } }
如果当前请求的 URL 地址和数据库中 menu 表的所有项都匹配不上,那么最终返回 null。如果返回 null,那么受保护对象到底能不能访问呢?这就要看 AbstractSecurityInterceptor 对象中的 rejectPublicInvocations 属性了,该属性默认为 false,表示当 getAttributes 方法返回 null 时,允许访问受保护对象
当用户想要访问某一个资源时,授权管理器通过持有的投票器根据用户的角色投出赞成或者反对票;
- 所谓投票器其实就是判断方法,授权管理器调用decide方法时会委派给持有的投票器进行判断
- 一个授权管理器可以持有多个投票器,如何综合每个投票器的结果做出判断就是所谓的表决机制
public interface AccessDecisionManager { // 决策 主要通过其持有的 AccessDecisionVoter 来进行投票决策 void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException; // 以确定AccessDecisionManager是否可以处理传递的ConfigAttribute boolean supports(ConfigAttribute attribute); //以确保配置的AccessDecisionManager支持安全拦截器将呈现的安全 object 类型。 boolean supports(Class<?> clazz); }
AccessDecisionManager
有三个默认实现(表决机制):
AffirmativeBased
基于肯定的决策器。 用户持有一个同意访问的角色就能通过ConsensusBased
基于共识的决策器。 用户持有同意的角色数量多于禁止的角色数UnanimousBased
基于一致的决策器。 用户持有的所有角色都同意访问才能放行AccessDecisionManager
授权管理器依赖投票器AccessDecisionVoter
,AccessDecisionVoter
定义如下:
public interface AccessDecisionVoter<S> { int ACCESS_GRANTED = 1; int ACCESS_ABSTAIN = 0; int ACCESS_DENIED = -1; boolean supports(ConfigAttribute attribute); boolean supports(Class<?> clazz); int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes); }
常用投票器有:
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) { if (authentication == null) { return ACCESS_DENIED; } int result = ACCESS_ABSTAIN; Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication); for (ConfigAttribute attribute : attributes) { if (this.supports(attribute)) { result = ACCESS_DENIED; for (GrantedAuthority authority : authorities) { if (attribute.getAttribute().equals(authority.getAuthority())) { return ACCESS_GRANTED; } } } } return result; }
如果当前登录主体为 null,则直接返回 ACCESS_DENIED 表示拒绝访问;否则就从当前登录主体 authentication 中抽取出角色信息,然后和 attributes 进行对比,如果具备 attributes 中所需角色的任意一种,则返回 ACCESS_GRANTED 表示允许访问
RoleHierarchyVoter 是 RoleVoter 的一个子类,在 RoleVoter 角色判断的基础上,引入了角色分层管理,也就是角色继承
RoleHierarchyVoter接口定义如下:
public interface RoleHierarchy { Collection<? extends GrantedAuthority> getReachableGrantedAuthorities( Collection<? extends GrantedAuthority> authorities); }
该接口中只有一个方法,返回值是一个可访问的权限集合
RoleHierarchy 接口有两个实现类:
public class RoleHierarchyImpl implements RoleHierarchy { private static final Log logger = LogFactory.getLog(RoleHierarchyImpl.class); private String roleHierarchyStringRepresentation = null; private Map<GrantedAuthority, Set<GrantedAuthority>> rolesReachableInOneStepMap = null; private Map<GrantedAuthority, Set<GrantedAuthority>> rolesReachableInOneOrMoreStepsMap = null; public void setHierarchy(String roleHierarchyStringRepresentation) { this.roleHierarchyStringRepresentation = roleHierarchyStringRepresentation; logger.debug("setHierarchy() - The following role hierarchy was set: " + roleHierarchyStringRepresentation); buildRolesReachableInOneStepMap(); buildRolesReachableInOneOrMoreStepsMap(); } private void buildRolesReachableInOneStepMap() { Pattern pattern = Pattern.compile("(\\s*([^\\s>]+)\\s*>\\s*([^\\s>]+))"); Matcher roleHierarchyMatcher = pattern .matcher(this.roleHierarchyStringRepresentation); this.rolesReachableInOneStepMap = new HashMap<GrantedAuthority, Set<GrantedAuthority>>(); while (roleHierarchyMatcher.find()) { GrantedAuthority higherRole = new SimpleGrantedAuthority( roleHierarchyMatcher.group(2)); GrantedAuthority lowerRole = new SimpleGrantedAuthority( roleHierarchyMatcher.group(3)); Set<GrantedAuthority> rolesReachableInOneStepSet; if (!this.rolesReachableInOneStepMap.containsKey(higherRole)) { rolesReachableInOneStepSet = new HashSet<GrantedAuthority>(); this.rolesReachableInOneStepMap.put(higherRole, rolesReachableInOneStepSet); } else { rolesReachableInOneStepSet = this.rolesReachableInOneStepMap .get(higherRole); } addReachableRoles(rolesReachableInOneStepSet, lowerRole); logger.debug("buildRolesReachableInOneStepMap() - From role " + higherRole + " one can reach role " + lowerRole + " in one step."); } } private void buildRolesReachableInOneOrMoreStepsMap() { this.rolesReachableInOneOrMoreStepsMap = new HashMap<GrantedAuthority, Set<GrantedAuthority>>(); // iterate over all higher roles from rolesReachableInOneStepMap for (GrantedAuthority role : this.rolesReachableInOneStepMap.keySet()) { Set<GrantedAuthority> rolesToVisitSet = new HashSet<GrantedAuthority>(); if (this.rolesReachableInOneStepMap.containsKey(role)) { rolesToVisitSet.addAll(this.rolesReachableInOneStepMap.get(role)); } Set<GrantedAuthority> visitedRolesSet = new HashSet<GrantedAuthority>(); while (!rolesToVisitSet.isEmpty()) { // take a role from the rolesToVisit set GrantedAuthority aRole = rolesToVisitSet.iterator().next(); rolesToVisitSet.remove(aRole); addReachableRoles(visitedRolesSet, aRole); if (this.rolesReachableInOneStepMap.containsKey(aRole)) { Set<GrantedAuthority> newReachableRoles = this.rolesReachableInOneStepMap .get(aRole); // definition of a cycle: you can reach the role you are starting from if (rolesToVisitSet.contains(role) || visitedRolesSet.contains(role)) { throw new CycleInRoleHierarchyException(); } else { // no cycle rolesToVisitSet.addAll(newReachableRoles); } } } this.rolesReachableInOneOrMoreStepsMap.put(role, visitedRolesSet); logger.debug("buildRolesReachableInOneOrMoreStepsMap() - From role " + role + " one can reach " + visitedRolesSet + " in one or more steps."); } } }
用户传入的字符串变量(继承关系字符串)设置给 roleHierarchyStringRepresentation 属性,然后通过 buildRolesReachableInOneStepMap 和 buildRolesReachableInOneOrMoreStepsMap 方法完成对角色层级的解析
buildRolesReachableInOneStepMap 方法用来将角色关系解析成一层一层的形式
假设角色继承关系是 ROLE_A > ROLE_B \n ROLE_C > ROLE_D \n ROLE_C > ROLE_E
,Map 中的数据是这样
假设角色继承关系是 ROLE_A > ROLE_B > ROLE_C > ROLE_D
,Map 中的数据是这样:
buildRolesReachableInOneOrMoreStepsMap 方法则是对 rolesReachableInOneStepMap 集合进行再次解析,将角色的继承关系拉平。经过 buildRolesReachableInOneOrMoreStepsMap 方法解析之后,新的 Map 中保存的数据如下:
A-->[B、C、D]
B-->[C、D]
C-->D
在 Spring Security 中,由于框架本身大量采用了 Java 配置,并且没有将对象的各个属性都暴露出来,这样做的本意是为了简化配置。然而这样带来的一个问题就是需要我们手动将 Bean 注册到 Spring 容器中去,ObjectPostProcessor 就是为了解决该问题。一旦将 Bean 注册到 Spring 容器中了,我们可以用ObjectPostProcessor
去增强一个 Bean 的功能,或者需修改一个 Bean 的属性
package org.springframework.security.config.annotation; public interface ObjectPostProcessor<T> { <O extends T> O postProcess(O object); }
Spring Security 框架源码中,随处可见手动装配。Spring Security 中,过滤器链中的所有过滤器都是通过对应的 xxxConfigure 来进行配置的,而所有的 xxxConfigure 都是继承自 SecurityConfigurerAdapter,而在这些 xxxConfigure 的 configure 方法中,无一例外的都会让他们各自配置的管理器去 Spring 容器中走一圈,例如 AbstractAuthenticationFilterConfigurer#configure 方法:
public void configure(B http) throws Exception { ... ... F filter = postProcess(authFilter); http.addFilter(filter); }
例如,权限管理本身是由 FilterSecurityInterceptor 控制的,系统默认的 FilterSecurityInterceptor 已经创建好了,而且我也没办法修改它的属性,那么怎么办呢?我们可以利用 withObjectPostProcessor 方法,去修改 FilterSecurityInterceptor 中的相关属性
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { object.setAccessDecisionManager(customUrlDecisionManager); object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource); return object; } }) .and() ... } }
上面这个配置生效的原因之一是因为 FilterSecurityInterceptor 在创建成功后,会重走一遍 postProcess 方法,这里通过重写 postProcess 方法就能实现属性修改
-- ---------------------------- -- Table structure for menu -- ---------------------------- DROP TABLE IF EXISTS `menu`; CREATE TABLE `menu` ( `mid` bigint(20) NOT NULL COMMENT '菜单ID', `pattern` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '菜单URL', PRIMARY KEY (`mid`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '菜单表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for menu_role -- ---------------------------- DROP TABLE IF EXISTS `menu_role`; CREATE TABLE `menu_role` ( `id` bigint(20) NOT NULL COMMENT 'ID', `mid` bigint(20) NOT NULL COMMENT '菜单ID', `rid` bigint(20) NOT NULL COMMENT '角色ID', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '菜单-角色权限映射表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for role -- ---------------------------- DROP TABLE IF EXISTS `role`; CREATE TABLE `role` ( `rid` bigint(20) NOT NULL COMMENT '角色ID', `name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色名称', `note` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色描述', PRIMARY KEY (`rid`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for user -- ---------------------------- DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `uid` bigint(20) NOT NULL COMMENT '用户ID', `username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户帐号', `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户密码', `enabled` tinyint(4) NOT NULL COMMENT '帐号是否启用', `locked` tinyint(4) NOT NULL COMMENT '帐号是否锁定', PRIMARY KEY (`uid`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for user_role -- ---------------------------- DROP TABLE IF EXISTS `user_role`; CREATE TABLE `user_role` ( `id` bigint(20) NOT NULL COMMENT 'ID', `uid` bigint(20) NOT NULL COMMENT '用户ID', `rid` bigint(20) NOT NULL COMMENT '角色ID', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户-角色映射表' ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;
User类
@TableName(value ="user") public class User implements Serializable { @TableId(value = "uid") @TableField(value = "username") private String username; @TableField(value = "password") private String password; @TableField(value = "enabled") private Integer enabled; @TableField(value = "locked") private Integer locked; }
UserDetail类(主要用于loadUserByUsername方法)
@AllArgsConstructor @NoArgsConstructor public class UserDetail implements UserDetails { private Long uid; private String username; private String password; private Integer enabled; private Integer locked; private List<Role> roles; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return roles.stream() .map(r -> new SimpleGrantedAuthority(r.getName())) .collect(Collectors.toList()); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return locked < 1 ? true : false; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return enabled > 0 ? true : false; } }
MenuDetail类(主要用于动态权限)
@Data @AllArgsConstructor @NoArgsConstructor public class MenuDetail { private Long mid; private String pattern; private List<Role> roles; }
Role类
@TableName(value ="role") public class Role implements Serializable { @TableId(value = "rid") private Long rid; @TableField(value = "name") private String name; @TableField(value = "note") private String note; }
Menu类(资源菜单)
@TableName(value ="menu") public class Menu implements Serializable { @TableId(value = "mid") private Long mid; @TableField(value = "pattern") private String pattern; }
UserRole类
user-role关联表,略
MenuRole类
menu-role关联表,略
UserService
@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Autowired private PasswordEncoder passwordEncoder; @Autowired private UserRoleMapper userRoleMapper; @Override public ResultVO regist(User user) { String username = user.getUsername(); QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.eq("username", username); List<User> users = userMapper.selectList(wrapper); if (user == null || users.size() == 0) { user.setEnabled(1); user.setLocked(0); user.setPassword(passwordEncoder.encode(user.getPassword())); int i = userMapper.insert(user); if (i > 0) { user.setPassword(null); return new ResultVO(ResultStatus.OK, "注册成功", user); } else { return new ResultVO(ResultStatus.NO, "注册失败,请重新注册", null); } } else { return new ResultVO(ResultStatus.NO, "用户已存在!", null); } } @Override public ResultVO setRoles(long uid, List<Long> rids) { Long[] success = new Long[rids.size()]; boolean isSuccess = true; for (int i = 0;i < rids.size();i++) { Long rid = rids.get(i); UserRole userRole = new UserRole(); userRole.setUid(uid); userRole.setRid(rid); int j = userRoleMapper.insert(userRole); if (j > 0) { success[i] = userRole.getId(); } else { isSuccess = false; break; } } if (isSuccess) { return new ResultVO(ResultStatus.OK, "角色绑定成功", null); } else { for (int k = 0; k < success.length; k++) { userRoleMapper.deleteById(success[k]); } return new ResultVO(ResultStatus.NO, "角色绑定失败!", null); } } @Override public UserDetail loadUserByUsername(String username) { return userMapper.loadUserByUsername(username); } }
MenuService
@Service @Slf4j public class MenuServiceImpl implements MenuService { @Autowired private MenuMapper menuMapper; @Autowired private MenuRoleMapper menuRoleMapper; @Override public ResultVO save(Menu menu) { int i = menuMapper.insert(menu); if (i > 0) { return new ResultVO(ResultStatus.OK, "success", menu); } else { return new ResultVO(ResultStatus.NO, "fail!", null); } } @Override public ResultVO setRoles(long mid, List<Long> rids) { long[] success = new long[rids.size()]; boolean isSuccess = true; for (int i = 0; i < rids.size(); i++) { long rid = rids.get(i); MenuRole menuRole = new MenuRole(); menuRole.setMid(mid); menuRole.setRid(rid); int j = menuRoleMapper.insert(menuRole); if (j > 0) { success[i] = menuRole.getId(); } else { isSuccess = false; break; } } if (isSuccess) { return new ResultVO(ResultStatus.OK, "success", null); } else { for (int k = 0; k < success.length; k++) { menuRoleMapper.deleteById(success[k]); } return new ResultVO(ResultStatus.NO, "fail!", null); } } @Override public List<MenuDetail> queryAllMenuDetails() { return menuMapper.queryAllMenuDetails(); } }
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public UserDetailsService userDetailsService() { return new UserDetailsService() { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserDetail userDetail = userService.loadUserByUsername(username); if (userDetail == null) { throw new UsernameNotFoundException("用户不存在!"); } return userDetail; } }; } }
认证成功回调
@Component public class LoginSuccessHandler implements AuthenticationSuccessHandler { @Autowired private ObjectMapper objectMapper; @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType("application/json"); ResultVO result = new ResultVO(ResultStatus.OK, "登录成功", authentication); objectMapper.writeValue(httpServletResponse.getOutputStream(), result); } }
认证失败回调
@Component public class LoginFailHandler implements AuthenticationFailureHandler { @Autowired private ObjectMapper objectMapper; @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType("application/json"); ResultVO result = new ResultVO(ResultStatus.NO, "登录失败", null); objectMapper.writeValue(httpServletResponse.getOutputStream(), result); } }
配置回调
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private LoginSuccessHandler loginSuccessHandler; @Autowired private LoginFailHandler loginFailHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests().anyRequest().authenticated() .and() .formLogin() .loginProcessingUrl("/login").permitAll() .successHandler(loginSuccessHandler) .failureHandler(loginFailHandler); } }
动态授权配置分为两步:
- 实现FilterInvocationSecurityMetadataSource接口,返回请求资源所需要的权限
- 配置权限管理器,实现权限鉴定,有两种方式:
- 实现AccessDecisionManager接口,自定义投票逻辑,不依赖系统内置投票器,但实现权限继承比较麻烦
- 配置内置授权管理器和分层投票器,实现角色继承
@Component public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { @Autowired private MenuService menuService; @Autowired private AntPathMatcher antPathMatcher; @Override public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException { // 获取当前保护对象的url String requestURI = ((FilterInvocation) o).getRequest().getRequestURI(); // 查询所有url对应的角色 List<MenuDetail> menuDetails = menuService.queryAllMenuDetails(); // 查找当前请求所需要的角色 for (MenuDetail menuDetail : menuDetails) { if (antPathMatcher.match(menuDetail.getPattern(), requestURI)) { String[] list = menuDetail.getRoles().stream().map(r -> r.getName()).toArray(String[]::new); return SecurityConfig.createList(list); } } return null; } @Override // 如果不为空,security启动时会做校验,一般直接返回null即可 public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> aClass) { // 类的isAssignableFrom判断当前类是否是入参的接口或父类 // 权限过滤器会封装请求、响应、调用链为FilterInvocation。所已本方法返回true // 当方法返回true,才能调用getAttributes return FilterInvocation.class.isAssignableFrom(aClass); } }
FilterInvocationSecurityMetadataSource返回值会作为给AccessDecisionManager.decide方法入参
@Component public class CustomAccessDecisionManager implements AccessDecisionManager { @Override /** * * @author weixia * @date 2022/8/27 * @param authentication 当前请求的用户,包含当前用户所拥有的权限信息 * @param o 待保护对象 * @param collection 目标请求所需要的权限 */ public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException { if (collection == null || collection.size() == 0) { // 如果受保护对象不需要权限,则直接返回放行 return; } if (authentication == null) { throw new AccessDeniedException("请用户登录再访问"); } Iterator<ConfigAttribute> iterator = collection.iterator(); Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); while (iterator.hasNext()) { ConfigAttribute configAttribute = iterator.next(); for (GrantedAuthority authority : authorities) { if (configAttribute.getAttribute().equalsIgnoreCase(authority.getAuthority())) { return; } } throw new AccessDeniedException("请用户登录再访问"); } } @Override public boolean supports(ConfigAttribute configAttribute) { return true; } @Override public boolean supports(Class<?> aClass) { return true; } }
利用ObjectPostProcessor重写权限过滤器的属性:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomSecurityMetadataSource customSecurityMetadataSource; @Autowired private CustomAccessDecisionManager customAccessDecisionManager; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests().anyRequest().authenticated() .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O o) { // 可以注入系统内置管理器也可以注入自己实现的授权管理器 o.setAccessDecisionManager(accessDecisionManager()); o.setSecurityMetadataSource(customSecurityMetadataSource); return o; } }); } @Bean public RoleHierarchy roleHierarchy() { RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl(); roleHierarchy.setHierarchy( "ROLE_admin > ROLE_student\n" + "ROLE_admin > ROLE_teacher" ); return roleHierarchy; } @Bean public AffirmativeBased accessDecisionManager() { RoleHierarchyVoter roleHierarchyVoter = new RoleHierarchyVoter(roleHierarchy()); return new AffirmativeBased( Arrays.asList(roleHierarchyVoter) ); } }
未认证用户访问受限资源异常
@Component public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint { @Autowired private ObjectMapper objectMapper; @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType("application/json"); ResultVO result = new ResultVO(ResultStatus.NO, "该资源需要登录访问", null); objectMapper.writeValue(httpServletResponse.getOutputStream(), result); } }
访问受限资源时权限不足异常
@Component public class MyAccessDeniedHandler implements AccessDeniedHandler { @Autowired private ObjectMapper objectMapper; @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType("application/json"); ResultVO result = new ResultVO(ResultStatus.NO, "权限不足!", null); objectMapper.writeValue(httpServletResponse.getOutputStream(), result); } }
配置回调
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyAuthenticationEntryPoint myAuthenticationEntryPoint; @Autowired private MyAccessDeniedHandler myAccessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests().anyRequest().authenticated() .and() .exceptionHandling() .authenticationEntryPoint(myAuthenticationEntryPoint) .accessDeniedHandler(myAccessDeniedHandler); } }
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Autowired private LoginSuccessHandler loginSuccessHandler; @Autowired private LoginFailHandler loginFailHandler; @Autowired private MyAuthenticationEntryPoint myAuthenticationEntryPoint; @Autowired private MyAccessDeniedHandler myAccessDeniedHandler; @Autowired private CustomSecurityMetadataSource customSecurityMetadataSource; @Autowired private CustomAccessDecisionManager customAccessDecisionManager; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests().anyRequest().authenticated() .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O o) { o.setAccessDecisionManager(accessDecisionManager()); o.setSecurityMetadataSource(customSecurityMetadataSource); return o; } }) .and() .formLogin() .loginProcessingUrl("/login").permitAll() .successHandler(loginSuccessHandler) .failureHandler(loginFailHandler) .and() .exceptionHandling() .authenticationEntryPoint(myAuthenticationEntryPoint) .accessDeniedHandler(myAccessDeniedHandler); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder()); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/user/regist"); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public UserDetailsService userDetailsService() { return new UserDetailsService() { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserDetail userDetail = userService.loadUserByUsername(username); if (userDetail == null) { throw new UsernameNotFoundException("用户不存在!"); } return userDetail; } }; } @Bean public ObjectMapper objectMapper() { return new ObjectMapper(); } @Bean public AntPathMatcher antPathMatcher() { return new AntPathMatcher(); } @Bean public RoleHierarchy roleHierarchy() { RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl(); roleHierarchy.setHierarchy( "ROLE_admin > ROLE_student\n" + "ROLE_admin > ROLE_teacher" ); return roleHierarchy; } @Bean public AffirmativeBased accessDecisionManager() { RoleHierarchyVoter roleHierarchyVoter = new RoleHierarchyVoter(roleHierarchy()); return new AffirmativeBased( Arrays.asList(roleHierarchyVoter) ); } }