看了半天的文檔及源碼,終於理出了spring-security的一些總體思路,spring security主要分認證(authentication)和授權(authority)。
1.認證authentication
認證主要代碼在spring-security-core下的包org.springframework.security.authentication下,主類:AuthenticationManager、AuthenticationProvider
其關系如下:
2.授權Authorization
授權也稱Access Control,主要代碼在spring-security-core下的包org.springframework.security.access下,主類:AccessDecisionManager、SecurityMetadataSource。它們的關系通過ConfigAttribute關聯起來。
SecurityMetadataSource獲取ConfigAttribute,方法:
Collection<ConfigAttribute> getAttributes(Object object)throws IllegalArgumentException;
AccessDecisionManager根據進行授權,方法:
void decide(Authentication authentication, Object object,Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,InsufficientAuthenticationException;
其實現類:AffirmativeBased的授權邏輯如下:
/** * This concrete implementation simply polls all configured * {@link AccessDecisionVoter}s and grants access if any * <code>AccessDecisionVoter</code> voted affirmatively. Denies access only if there * was a deny vote AND no affirmative votes. * <p> * If every <code>AccessDecisionVoter</code> abstained from voting, the decision will * be based on the {@link #isAllowIfAllAbstainDecisions()} property (defaults to * false). * </p> * * @param authentication the caller invoking the method * @param object the secured object * @param configAttributes the configuration attributes associated with the method * being invoked * * @throws AccessDeniedException if access is denied */ public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException { int deny = 0; for (AccessDecisionVoter voter : getDecisionVoters()) { int result = voter.vote(authentication, object, configAttributes); if (logger.isDebugEnabled()) { logger.debug("Voter: " + voter + ", returned: " + result); } switch (result) { case AccessDecisionVoter.ACCESS_GRANTED: return; case AccessDecisionVoter.ACCESS_DENIED: deny++; break; default: break; } } if (deny > 0) { throw new AccessDeniedException(messages.getMessage( "AbstractAccessDecisionManager.accessDenied", "Access is denied")); } // To get this far, every AccessDecisionVoter abstained checkAllowIfAllAbstainDecisions(); }
從上文可以看出,真正的授權是通過AccessDecisionVoter來完成的。
3.認證和授權的集成AbstractSecurityInterceptor
AbstractSecurityInterceptor包含了四個instance及其get/set方法
private AccessDecisionManager accessDecisionManager; private AfterInvocationManager afterInvocationManager; private AuthenticationManager authenticationManager = new NoOpAuthenticationManager(); private RunAsManager runAsManager = new NullRunAsManager();
加一個抽象的方法:
/** * Indicates the type of secure objects the subclass will be presenting to the * abstract parent for processing. This is used to ensure collaborators wired to the * {@code AbstractSecurityInterceptor} all support the indicated secure object class. * * @return the type of secure object the subclass provides services for */ public abstract Class<?> getSecureObjectClass();
AbstractSecurityInterceptor的實現類有兩個:
3.1 FilterSecurityInterceptor
定義:
/** * Performs security handling of HTTP resources via a filter implementation. * <p> * The <code>SecurityMetadataSource</code> required by this security interceptor is of * type {@link FilterInvocationSecurityMetadataSource}. * <p> * Refer to {@link AbstractSecurityInterceptor} for details on the workflow. * </p> * * @author Ben Alex * @author Rob Winch */ public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {}
實現了標准的servlet的Filter接口,其邏輯如下:
/** * Method that is actually called by the filter chain. Simply delegates to the * {@link #invoke(FilterInvocation)} method. * * @param request the servlet request * @param response the servlet response * @param chain the filter chain * * @throws IOException if the filter chain fails * @throws ServletException if the filter chain fails */ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); invoke(fi); }
最重要的實現invoke
public void invoke(FilterInvocation fi) throws IOException, ServletException { if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) && observeOncePerRequest) { // filter already applied to this request and user wants us to observe // once-per-request handling, so don't re-do security checking fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } else { // first time this request being called, so perform security checking if (fi.getRequest() != null) { fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); } }
3.2 MethodSecurityInterceptor
定義:
/** * Provides security interception of AOP Alliance based method invocations. * <p> * The <code>SecurityMetadataSource</code> required by this security interceptor is of * type {@link MethodSecurityMetadataSource}. This is shared with the AspectJ based * security interceptor (<code>AspectJSecurityInterceptor</code>), since both work with * Java <code>Method</code>s. * <p> * Refer to {@link AbstractSecurityInterceptor} for details on the workflow. * * @author Ben Alex * @author Rob Winch */ public class MethodSecurityInterceptor extends AbstractSecurityInterceptor implements MethodInterceptor {}
其invoke方法如下:
/** * This method should be used to enforce security on a <code>MethodInvocation</code>. * * @param mi The method being invoked which requires a security decision * * @return The returned value from the method invocation (possibly modified by the * {@code AfterInvocationManager}). * * @throws Throwable if any error occurs */ public Object invoke(MethodInvocation mi) throws Throwable { InterceptorStatusToken token = super.beforeInvocation(mi); Object result; try { result = mi.proceed(); } finally { super.finallyInvocation(token); } return super.afterInvocation(token, result); }
4.Spring Security Java Config ---@EnableWebSecurity
將@EnableWebSecurity注解加到@Configuration下來獲得spring securiy的安全性。
WebSecurityConfigurer定義的配置或者對WebSecurityConfigurerAdapter類的擴展類示例如下:
@Configuration @EnableWebSecurity public class MyWebSecurityConfiguration extends WebSecurityConfigurerAdapter { @Override public void configure(WebSecurity web) throws Exception { web.ignoring() // Spring Security should completely ignore URLs starting with /resources/ .antMatchers("/resources/"); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/public/").permitAll().anyRequest() .hasRole("USER").and() // Possibly more configuration ... .formLogin() // enable form based log in // set permitAll for all URLs associated with Form Login .permitAll(); } @Override protected void configure(AuthenticationManagerBuilder auth) { auth // enable in memory based authentication with a user named "user" and "admin" .inMemoryAuthentication().withUser("user").password("password").roles("USER") .and().withUser("admin").password("password").roles("USER", "ADMIN"); } // Possibly more overridden methods ... }
4.1 WebSecurityConfigurer
WebSecurityConfigurer允許對WebSecurity進行定制化,在絕大部分情景下,開發者使用@EnableWebSecurity注解或者對WebSecurityConfigurerAdapter進行重寫的方式來自動應用@EnableWebSecurity注解。定義如下:
public interface WebSecurityConfigurer<T extends SecurityBuilder<Filter>> extends SecurityConfigurer<Filter, T> { }
4.2 WebSecurityConfigurerAdapter
WebSecurityConfigurerAdapter提供了創建WebSecurityConfigurer實例的便利方法,它是一個基類。該類的實現通過重寫方法來實現定制化。
它自動從SpringFactoriesLoader查找AbstractHttpConfigurer,從而讓開發者可以擴展。為達到這個目的,必須創建一個AbstractHttpConfigurer的擴展類,然后在classpath路徑下創建一個文件META-INF/spring.factories,示例如下:
org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyClassThatExtendsAbstractHttpConfigurer
如果你有多個擴展類,可以使用逗號分隔,示例如下:
org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyClassThatExtendsAbstractHttpConfigurer, sample.OtherThatExtendsAbstractHttpConfigurer
4.2.1 初始化
初始化分兩個過程:獲取HttpSecurity,配置FilterSecurityInterceptor到WebSecurity
public void init(final WebSecurity web) throws Exception { final HttpSecurity http = getHttp(); web.addSecurityFilterChainBuilder(http).postBuildAction(new Runnable() { public void run() { FilterSecurityInterceptor securityInterceptor = http .getSharedObject(FilterSecurityInterceptor.class); web.securityInterceptor(securityInterceptor); } }); }
獲取HttpSecurity的過程:
/** * Creates the {@link HttpSecurity} or returns the current instance * * ] * @return the {@link HttpSecurity} * @throws Exception */ @SuppressWarnings({ "rawtypes", "unchecked" }) protected final HttpSecurity getHttp() throws Exception { if (http != null) { return http; } DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor .postProcess(new DefaultAuthenticationEventPublisher()); localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher); AuthenticationManager authenticationManager = authenticationManager(); authenticationBuilder.parentAuthenticationManager(authenticationManager); Map<Class<? extends Object>, Object> sharedObjects = createSharedObjects(); http = new HttpSecurity(objectPostProcessor, authenticationBuilder, sharedObjects); if (!disableDefaults) { // @formatter:off http .csrf().and() .addFilter(new WebAsyncManagerIntegrationFilter()) .exceptionHandling().and() .headers().and() .sessionManagement().and() .securityContext().and() .requestCache().and() .anonymous().and() .servletApi().and() .apply(new DefaultLoginPageConfigurer<HttpSecurity>()).and() .logout(); // @formatter:on ClassLoader classLoader = this.context.getClassLoader(); List<AbstractHttpConfigurer> defaultHttpConfigurers = SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader); for(AbstractHttpConfigurer configurer : defaultHttpConfigurers) { http.apply(configurer); } } configure(http); return http; }
5. xml配置解析類SecurityNamespaceHandler
它的解析器有以下幾種:
private void loadParsers() { // Parsers parsers.put(Elements.LDAP_PROVIDER, new LdapProviderBeanDefinitionParser()); parsers.put(Elements.LDAP_SERVER, new LdapServerBeanDefinitionParser()); parsers.put(Elements.LDAP_USER_SERVICE, new LdapUserServiceBeanDefinitionParser()); parsers.put(Elements.USER_SERVICE, new UserServiceBeanDefinitionParser()); parsers.put(Elements.JDBC_USER_SERVICE, new JdbcUserServiceBeanDefinitionParser()); parsers.put(Elements.AUTHENTICATION_PROVIDER, new AuthenticationProviderBeanDefinitionParser()); parsers.put(Elements.GLOBAL_METHOD_SECURITY, new GlobalMethodSecurityBeanDefinitionParser()); parsers.put(Elements.AUTHENTICATION_MANAGER, new AuthenticationManagerBeanDefinitionParser()); parsers.put(Elements.METHOD_SECURITY_METADATA_SOURCE, new MethodSecurityMetadataSourceBeanDefinitionParser()); // Only load the web-namespace parsers if the web classes are available if (ClassUtils.isPresent(FILTER_CHAIN_PROXY_CLASSNAME, getClass() .getClassLoader())) { parsers.put(Elements.DEBUG, new DebugBeanDefinitionParser()); parsers.put(Elements.HTTP, new HttpSecurityBeanDefinitionParser()); parsers.put(Elements.HTTP_FIREWALL, new HttpFirewallBeanDefinitionParser()); parsers.put(Elements.FILTER_SECURITY_METADATA_SOURCE, new FilterInvocationSecurityMetadataSourceParser()); parsers.put(Elements.FILTER_CHAIN, new FilterChainBeanDefinitionParser()); filterChainMapBDD = new FilterChainMapBeanDefinitionDecorator(); } if (ClassUtils.isPresent(MESSAGE_CLASSNAME, getClass().getClassLoader())) { parsers.put(Elements.WEBSOCKET_MESSAGE_BROKER, new WebSocketMessageBrokerSecurityBeanDefinitionParser()); } }
6.小結
spring支持注解和xml配置兩種方式,因此分析源碼可以從xml配置及注解兩方面入手,相互印證。
參考文獻:
【1】https://spring.io/guides/topicals/spring-security-architecture/
【2】http://zzy.cincout.cn/2016/12/23/spring-security-2016-12-23-spring-security-01/