Spring Security —— 整体架构与入门案例分析

Posted _瞳孔

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Security —— 整体架构与入门案例分析相关的知识,希望对你有一定的参考价值。

一:整体架构

在<Spring Security> 的架构设计中,认证<Authentication>和授权<Authorization>是分开的,无论使用什么样的认证方式。都不会影响授权,这是两个独立的存在,这种独立带来的好处之一,就是可以非常方便地整合一些外部的解决方案。

1.1:认证

1.1.1:AuthenticationManager

在Spring Security中认证是由AuthenticationManager接口来负责的,接口定义为:

public interface AuthenticationManager 
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;

  • 返回Authentication则表示认证成功
  • 返回AuthenticationException异常则表示认证失败

而该接口有如下实现类:

其中主要实现类为ProviderManager,在ProviderManager中管理了众多AuthenticationProvider实例,在一次完整的认证流程中,SpringSecurity允许存在多个AuthenticationProvider,用来实现多种认证方式,这些AuthenticationProvider都是由ProviderManager进行统一管理的

ProviderManager部分源码:

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
		InitializingBean 

	private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
	private List<AuthenticationProvider> providers = Collections.emptyList();
	private AuthenticationManager parent;
	private boolean eraseCredentialsAfterAuthentication = true;

	public ProviderManager(List<AuthenticationProvider> providers) 
		this(providers, null);
	

	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException 
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		boolean debug = logger.isDebugEnabled();

		for (AuthenticationProvider provider : getProviders()) 
			if (!provider.supports(toTest)) 
				continue;
			

			if (debug) 
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			

			try 
				result = provider.authenticate(authentication);

				if (result != null) 
					copyDetails(authentication, result);
					break;
				
			
			catch (AccountStatusException | InternalAuthenticationServiceException e) 
				prepareException(e, authentication);
				throw e;
			 catch (AuthenticationException e) 
				lastException = e;
			
		

		if (result == null && parent != null) 
			// Allow the parent to try.
			try 
				result = parentResult = parent.authenticate(authentication);
			
			catch (ProviderNotFoundException e) 
			
			catch (AuthenticationException e) 
				lastException = parentException = e;
			
		

		if (result != null) 
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) 
				((CredentialsContainer) result).eraseCredentials();
			
			
			if (parentResult == null) 
				eventPublisher.publishAuthenticationSuccess(result);
			
			return result;
		

		// Parent was null, or didn't authenticate (or throw an exception).

		if (lastException == null) 
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[]  toTest.getName() ,
					"No AuthenticationProvider found for 0"));
		

		if (parentException == null) 
			prepareException(lastException, authentication);
		

		throw lastException;
	

1.1.2:Authentication

认证以及认证成功的信息主要是由Authentication的实现类进行保存的,Authentication接口定义如下:

public interface Authentication extends Principal, Serializable 
	// 获取用户权限信息
	Collection<? extends GrantedAuthority> getAuthorities();
	
	// 获取凭证信息,一般指密码
	Object getCredentials();
	
	// 获取用户详细信息
	Object getDetails();

	// 获取用户身份信息,用户名、用户对象等
	Object getPrincipal();
	
	// 用户是否认证成功
	boolean isAuthenticated();

	// 设置认证标记
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;

1.1.3:SecurityContextHolder

SecurityContextHolder用来获取登录之后用户信息。Spring Security会将登录用户数据保存在Session中。但是,为了使用方便,Spring Security在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定。当用户登录成功后,Spring Security会将登录成功的用户信息保存到SecurityContextHolder中。SecurityContextHolder中的数据保存默认是通过ThreadLocal来实现的,使用Threadlocal创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。 当登录请求处理完毕后,Spring Security会将SecurityContextHolder中的数据拿出来保存到Session中,同时将SecurityContexHolder中的数据清空。以后每当有请求到来时,Spring Security就会先Session中取出用户登录数据,保存到SecurityContextHolder中,方便在该请求的后续处理过程中使用,同时在请求结束时将SecurityContextHolder中的数据拿出来保存到Session中,然后将SecurityContextHolder中的数据清空。这一策略能使用户在Controller、Service 层以及任何代码中获取当前登录用户数据。

SecurityContextHolder提供的方法如下:

1.2:授权

1.2.1:AccessDecisionManager

AccessDecisionManager是访问决策管理器,用来决定此次访问是否被允许

public interface AccessDecisionManager 
	// 决策方法
	void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
			InsufficientAuthenticationException;

	boolean supports(ConfigAttribute attribute);

	boolean supports(Class<?> clazz);

1.2.2: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);

AccessDecisionManager和AccessDecisionVoter都有众多的实现类,在AccessDecisionManager中会遍历AccessDecisionVoter,进而决定是否允许用户访问,因而两者的关系类似于ProviderManager和AuthenticationProvider。我们可以看看AccessDecisionManager的实现类AffirmativeBased的决策方法,这是一个很清晰的投票逻辑。

	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();
	

1.2.3:ConfigAttribute

ConfigAttribute用来保存授权时的角色信息,在Spring Security中,用户请求一个资源(通常是一个接口或者一个Java方法)需要的角色会被封装成一个ConfigAttribute对象,在ConfigAttribute中只有一个getAttribute方法, 该方法返回一个String字符串,就是角色的名称。一般来说,角色名称都带有一个ROLE_前缀,投票器AccessDecisionVoter所做的事情,其实就是比较用户所具的角色和请求某个
资源所需的ConfigAttribute之间的关系。

public interface ConfigAttribute extends Serializable 
	String getAttribute();

二:入门案例分析

2.1:环境搭建

引入SpringSecurity依赖:

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

写一个controller方法:

@RestController
public class TestController 

    @GetMapping("/hello")
    public String Hello() 
        System.out.println("hello security");
        return "hello security";
    

然后在浏览器访问http://localhost:9101/hello(我开了9101端口)的时候,就会默认跳转到http://localhost:9101/login,这就是SpringSecurity的强大之处,只需要引入一个依赖,所有的接口就会自动保护起来

输入SpringSecurity内置的user用户账号就可以登录,密码在控制台,填好后点击sign in即可跳转接口地址

2.2:实现原理

2.2.1:内置Filter以及默认加载Filter

由上面案例可知,引入依赖后所有接口都默认需要登录才可以访问,这个过程是通过filter实现的,我们可以在官网找到SpringSecurity的过滤器链结构,可以看到,客户端向应用程序发送一个请求,容器创建一个FilterChain,其中包含过滤器和Servlet,它们应该根据请求URI的路径处理HttpServletRequest。

Spring提供了一个名为DelegatingFilterProxy的过滤器实现,它允许在Servlet容器的生命周期和Spring的ApplicationContext之间建立桥接。Servlet容器允许使用自己的标准注册过滤器,但它不知道Spring定义的bean。DelegatingFilterProxy可以通过标准的Servlet容器机制注册,但可以将所有工作委托给实现Filter的Spring Bean。

DelegatingFilterProxy的另一个好处是,它允许延迟查看Filter bean实例。这很重要,因为容器在启动之前需要注册Filter实例。但是,Spring通常使用ContextLoaderListener来加载Spring bean,直到需要注册Filter实例之后才会加载

Spring Security的Servlet支持包含在FilterChainProxy中。FilterChainProxy是Spring Security提供的一个特殊的Filter,它允许通过SecurityFilterChain委托给多个Filter实例。因为FilterChainProxy是一个Bean,它通常被包装在DelegatingFilterProxy中。

FilterChainProxy使用SecurityFilterChain来确定这个请求应该调用哪个Spring Security Filter,SecurityFilterChain中的Security Filter通常是bean,但它们是用FilterChainProxy而不是DelegatingFilterProxy注册的。FilterChainProxy为直接注册Servlet容器或DelegatingFilterProxy提供了许多优势:

  • 它为Spring Security的所有Servlet支持提供了一个起点。因此,如果想排除Spring Security的Servlet支持故障,在FilterChainProxy中添加一个调试点是一个很好的开始。
  • 由于FilterChainProxy是Spring Security使用的中心,它可以执行非可选的任务。例如,它清除SecurityContext以避免内存泄漏。它还应用Spring Security的HttpFirewall来保护应用程序免受某些类型的攻击。
  • 它在决定何时应该调用SecurityFilterChain方面提供了更大的灵活性。在Servlet容器中,仅根据URL调用Filters。然而,FilterChainProxy可以通过利用RequestMatcher接口来确定基于HttpServletRequest中的任何东西的调用。

事实上,FilterChainProxy可以用来确定应该使用哪个SecurityFilterChain。这允许为应用程序的不同部分提供完全独立的配置。


而过滤器链也不一定是一组,有可能是多组,此时就可以根据不同的请求去设置不同的过滤器链,这时就用到了Multiple SecurityFilterChain,在下图中,FilterChainProxy决定应该使用哪个SecurityFilterChain。只有第一个匹配的SecurityFilterChain会被调用。如果一个/api/messages/的URL被请求,它会首先匹配SecurityFilterChain0的/api/**模式,所以即使它也匹配SecurityFilterChainn,也只有SecurityFilterChain0会被调用。如果一个/messages/的URL被请求,它将不匹配SecurityFilterChain0的/api/**模式,因此FilterChainProxy将继续尝试后面的SecurityFilterChain

当然官网也按照顺序列出来了完整的过滤器链,其中15个过滤器会默认加载,标红表示默认加载:

  • ChannelProcessingFilter:过滤请求协议HTTP、HTTPS,
  • WebAsyncManagerIntegrationFilter:将WebAsyncManager与SpringSecurity上下文进行集成
  • SecurityContextPersistenceFilter:主要是使用SecurityContextRepository在session中保存或更新一个SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文。即在处理请求之前,将安全信息加载到SecurityContextHolder中
  • HeaderWriterFilter:向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制
  • CorsFilter:处理跨域问题
  • CsrfFilter:处理CSRF攻击,CSRF又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息,如果不包含,则报错。起到防止CSRF攻击的效果。
  • LogoutFilter:匹配URL为/logout的请求,实现用户退出,清除认证信息。
  • OAuth2AuthorizationRequestRedirectFilter:处理OAuth2认证重定向
  • Saml2WebSsoAuthenticationRequestFilter:处理SAML认证重定向
  • X509AuthenticationFilter:处理X509认证
  • AbstractPreAuthenticatedProcessingFilter:处理预认证问题
  • CasAuthenticationFilter:处理CAS单点登录
  • OAuth2LoginAuthenticationFilter:处理OAuth2认证
  • Saml2WebSsoAuthenticationFilter:处理SAML认证
  • UsernamePasswordAuthenticationFilter:表单登录认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。
  • OpenIDAuthenticationFilter:处理OpenID认证
  • DefaultLoginPageGeneratingFilter:配置默认登录页面,如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。
  • DefaultLogoutPageGeneratingFilter:配置默认注销页面,由此过滤器可以生产一个默认的退出登录页面
  • ConcurrentSessionFilter:处理Session有效期
  • DigestAuthenticationFilter:处理HTTP摘要认证
  • BearerTokenAuthenticationFilter:处理OAuth2认证的Access Token
  • BasicAuthenticationFilter:处理HttpBasic登录,此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。
  • RequestCacheAwareFilter:处理缓存请求,通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest
  • SecurityContextHolderAwareRequestFilter:针对ServletRequest进行了一次包装,使得request具有更加丰富的API
  • JaasApiIntegrationFilter:处理JAAS认证
  • RememberMeAuthenticationFilter:处理RememberMe登录
  • AnonymousAuthenticationFilter:配置匿名认证,当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。Spring Security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。
  • OAuth2AuthorizationCodeGrantFilter:处理OAuth2认证中的授权码
  • SessionManagementFilter:处理Session并发问题,SecurityContextRepository限制同一用户开启多个会话的数量
  • ExceptionTranslationFilter:处理认证/授权中的异常,异常转换过滤器位于整个SpringSecurityFilterChain的后方,用来转换整个链路中出现的异常
  • FilterSecurityInterceptor:处理授权相关操作,会获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限。
  • SwitchUserFilter:处理账户切换

可以看出,Spring Security提供了30多个过滤器。默认情况下Spring Boot在对Spring Security进入自动化配置时,会创建一个名为SpringSecurityFilterChain的过滤器,并注入到Spring容器中,这个过滤器将负责所有的安全管理,包括用户认证、授权、重定向到登录页面等。我们可以从源码开始看:

首先是DelegatingFilterProxy的部分源码

public class DelegatingFilterProxy extends GenericFilterBean 

	@Nullable
	private String contextAttribute;
	@Nullable
	private WebApplicationContext webApplicationContext;
	@Nullable
	private String targetBeanName;
	private boolean targetFilterLifecycle;
	@Nullable
	private volatile Filter delegate; //注:这个过滤器是真正加载的过滤器
	private final Object delegateMonitor;
	
	// doFilter是过滤器的入口
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException 
		Filter delegateToUse = this.delegate;
		if (delegateToUse == null) 
			synchronized(this.delegateMonitor) 
				delegateToUse = this.delegate;
				if (delegateToUse == null) 
					WebApplicationContext wac = this.findWebApplicationContext();
					if (wac == null) 
						throw new IllegalStateException("No WebApplicationContext found: no
						ContextLoaderListener or DispatcherServlet registered?");
					
					// 第一步:doFilter中最重要的一步,初始化上面私有过滤器属性delegate
					delegateToUse = this.initDelegate(wac);
				 
				this.delegate = delegateToUse;
			
		
		// 第三步:执行FilterChainProxy过滤器
		this.invokeDelegate(delegateToUse, request, response, filterChain);
	
	
	// 第二步:直接看最终加载的过滤器到底是谁
	protected Filter initDelegate(WebApplicationContext wac) throws ServletException 
		// debug得知targetBeanName为:springSecurityFilterChain
		String targetBeanName = this.getTargetBeanName();
		Assert.state(targetBeanName != null, "No target bean name set");
		// debug得知delegate对象为:FilterChainProxy
		Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class);
		if (this.isTargetFilterLifecycle()) 
			delegate.init(this.getFilterConfig());
		 
		return delegate;
	 
	
	protected void invokeDelegate(Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException 
		delegate.doFilter(request, response, filterChain);
	

我们对第二步进行debug,如下图所示,DelegatingFilterProxy通过springSecurityFilterChain这个名称,得到了一个FilterChainProxy过滤器,最终在第三步执行了这个过滤器。

接着看FilterChainProxy:

public class FilterChainProxy extends GenericFilterBean 
	
	private static final Log logger = LogFactory.getLog(FilterChainProxy.class);
	private static final String FILTER_APPLIED = FilterChainProxy.class.getName().concat(".APPLIED");
	private List<SecurityFilterChain> filterChains;
	private FilterChainProxy.FilterChainValidator filterChainValidator;
	private HttpFirewall firewall;
	
	// 通过SecurityFilterChain的对象实例化出一个FilterChainProxy对象
	public FilterChainProxy(SecurityFilterChain chain) 
		this(Arrays.asList(chain));
	 

	public FilterChainProxy(List<SecurityFilterChain> filterChains) 
		this.filterChainValidator = new FilterChainProxy.NullFilterChainValidator();
		this.firewall = new StrictHttpFirewall();
		this.filterChains = filterChains;
	 

	// 直接从doFilter看
	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);
				this.doFilterInternal(request, response, chain);
			 finally 
				SecurityContextHolder.clearContext();
				request.removeAttribute(FILTER_APPLIED);
			
		 else 
			// 第一步:具体操作调用下面的doFilterInternal方法了
			this.doFilterInternal(request, response, chain);
		
	 

	private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException 
		FirewalledRequest fwRequest = this.firewall.getFirewalledRequest以上是关于Spring Security —— 整体架构与入门案例分析的主要内容,如果未能解决你的问题,请参考以下文章

安全框架Spring Security是什么?如何理解Spring Security的权限管理?

Spring Security源码:建造者详解

Spring Security源码:整体框架设计

Spring starter security or spring cloud security 如何保护整个微服务架构?

Spring整体架构

Spring Security 架构原理学习笔记