Spring Security 核心过滤器 篇四

/ SpringSecurity技术 / 没有评论 / 2401浏览

2019622141857-spring-security

核心过滤器源码分析

前面的部分,我们关注了 Spring Security 是如何完成认证工作的,但是另外一部分核心的内容:过滤器,一直没有提到,我们已经知道Spring Security 使用了springSecurityFillterChian作为了安全过滤的入口,这一节主要分析一下这个过滤器链都包含了哪些关键的过滤器,并且各自的使命是什么。

核心过滤器概述

由于过滤器链路中的过滤较多,即使是 Spring Security 的官方文档中也并未对所有的过滤器进行介绍,在之前,《Spring Security 篇二》入门指南中我们配置了一个表单登录的 demo,以此为例,来看看这过程中 Spring Security 都帮我们自动配置了哪些过滤器。

Creating filter chain: o.s.s.web.util.matcher.AnyRequestMatcher@1, 
[o.s.s.web.context.SecurityContextPersistenceFilter@8851ce1, 
o.s.s.web.header.HeaderWriterFilter@6a472566, 
o.s.s.web.csrf.CsrfFilter@61cd1c71, 
o.s.s.web.authentication.logout.LogoutFilter@5e1d03d7, 
o.s.s.web.authentication.UsernamePasswordAuthenticationFilter@122d6c22, 
o.s.s.web.savedrequest.RequestCacheAwareFilter@5ef6fd7f, 
o.s.s.web.servletapi.SecurityContextHolderAwareRequestFilter@4beaf6bd, 
o.s.s.web.authentication.AnonymousAuthenticationFilter@6edcad64, 
o.s.s.web.session.SessionManagementFilter@5e65afb6, 
o.s.s.web.access.ExceptionTranslationFilter@5b9396d3, 
o.s.s.web.access.intercept.FilterSecurityInterceptor@3c5dbdf8]

上述的 log 信息是我从 springboot 启动的日志中 CV 所得,spring security 的过滤器日志有一个特点:log 打印顺序与实际配置顺序符合,也就意味着 SecurityContextPersistenceFilter 是整个过滤器链的第一个过滤器,而 FilterSecurityInterceptor 则是末置的过滤器。另外通过观察过滤器的名称,和所在的包名,可以大致地分析出他们各自的作用,如UsernamePasswordAuthenticationFilter 明显便是与使用用户名和密码登录相关的过滤器,而 FilterSecurityInterceptor 我们似乎看不出它的作用,但是其位于 web.access 包下,大致可以分析出他与访问限制相关。第四篇文章主要就是介绍这些常用的过滤器,对其中关键的过滤器进行一些源码分析。先大致介绍下每个过滤器的作用:

其中核心过滤器有如下几个: SecurityContextPersistenceFilterUsernamePasswordAuthenticationFilterAnonymousAuthenticationFilterExceptionTranslationFilterFilterSecurityInterceptor

SecurityContextPersistenceFilter

试想一下,如果我们不使用 Spring Security,如果保存用户信息呢,大多数情况下会考虑使用 Session 对吧?在Spring Security 中也是如此,用户在登录过一次之后,后续的访问便是通过 sessionId 来识别,从而认为用户已经被认证。具体在何处存放用户信息,便是第一篇文章中提到的 SecurityContextHolder;认证相关的信息是如何被存放到其中的,便是通过SecurityContextPersistenceFilter。在上篇中也提到了,SecurityContextPersistenceFilter 的两个主要作用便是请求来临时,创建 SecurityContext 安全上下文信息和请求结束时清空 SecurityContextHolder。顺带提一下:微服务的一个设计理念需要实现服务通信的无状态,而 http 协议中的无状态意味着不允许存在 session,这可以通过setAllowSessionCreation(false) 实现,这并不意味着 SecurityContextPersistenceFilter 变得无用,因为它还需要负责清除用户信息。在 Spring Security 中,虽然安全上下文信息被存储于 Session 中,但我们在实际使用中不应该直接操作 Session,而应当使用 SecurityContextHolder

源码分析

public class SecurityContextPersistenceFilter extends GenericFilterBean {

	static final String FILTER_APPLIED = "__spring_security_scpf_applied";
  //上下文存储的仓库
	private SecurityContextRepository repo;

	private boolean forceEagerSessionCreation = false;

	public SecurityContextPersistenceFilter() {
      //HttpSessionSecurityContextRepository是SecurityContextRepository接口的一个实现类
      //使用HttpSession来存储SecurityContext
		this(new HttpSessionSecurityContextRepository());
	}

	public SecurityContextPersistenceFilter(SecurityContextRepository repo) {
		this.repo = repo;
	}

	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		if (request.getAttribute(FILTER_APPLIED) != null) {
			// ensure that filter is only applied once per request
			chain.doFilter(request, response);
			return;
		}

		final boolean debug = logger.isDebugEnabled();

		request.setAttribute(FILTER_APPLIED, Boolean.TRUE);

		if (forceEagerSessionCreation) {
			HttpSession session = request.getSession();

			if (debug && session.isNew()) {
				logger.debug("Eagerly created session: " + session.getId());
			}
		}
    //包装request,response
		HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
				response);
    //从Session中获取安全上下文信息
		SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
		try {
      //请求开始时,设置安全上下文信息,这样就避免了用户直接从Session中获取安全上下文信息
			SecurityContextHolder.setContext(contextBeforeChainExecution);
			chain.doFilter(holder.getRequest(), holder.getResponse());
		}
		finally {
      //请求结束后,清空安全上下文信息
			SecurityContext contextAfterChainExecution = SecurityContextHolder
					.getContext();
			// Crucial removal of SecurityContextHolder contents - do this before anything
			// else.
			SecurityContextHolder.clearContext();
			repo.saveContext(contextAfterChainExecution, holder.getRequest(),
					holder.getResponse());
			request.removeAttribute(FILTER_APPLIED);
			if (debug) {
				logger.debug("SecurityContextHolder now cleared, as request processing completed");
			}
		}
	}
	public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {
		this.forceEagerSessionCreation = forceEagerSessionCreation;
	}
}

过滤器一般负责核心的处理流程,而具体的业务实现,通常交给其中聚合的其他实体类,这在 Filter 的设计中很常见,同时也符合职责分离模式。例如存储安全上下文和读取安全上下文的工作完全委托给了 HttpSessionSecurityContextRepository 去处理,而这个类中也有几个方法可以稍微解读下,方便我们理解内部的工作流程

public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
   // 'SPRING_SECURITY_CONTEXT'是安全上下文默认存储在Session中的键值
   public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
   ...
   private final Object contextObject = SecurityContextHolder.createEmptyContext();
   private boolean allowSessionCreation = true;
   private boolean disableUrlRewriting = false;
   private String springSecurityContextKey = SPRING_SECURITY_CONTEXT_KEY;

   private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();

   //从当前request中取出安全上下文,如果session为空,则会返回一个新的安全上下文
   public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
      HttpServletRequest request = requestResponseHolder.getRequest();
      HttpServletResponse response = requestResponseHolder.getResponse();
      HttpSession httpSession = request.getSession(false);
      SecurityContext context = readSecurityContextFromSession(httpSession);
      if (context == null) {
         context = generateNewContext();
      }
      ...
      return context;
   }

   ...

   public boolean containsContext(HttpServletRequest request) {
      HttpSession session = request.getSession(false);
      if (session == null) {
         return false;
      }
      return session.getAttribute(springSecurityContextKey) != null;
   }

   private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
      if (httpSession == null) {
         return null;
      }
      ...
      // Session存在的情况下,尝试获取其中的SecurityContext
      Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);
      if (contextFromSession == null) {
         return null;
      }
      ...
      return (SecurityContext) contextFromSession;
   }

   //初次请求时创建一个新的SecurityContext实例
   protected SecurityContext generateNewContext() {
      return SecurityContextHolder.createEmptyContext();
   }
}

SecurityContextPersistenceFilterHttpSessionSecurityContextRepository 配合使用,构成了 Spring Security 整个调用链路的入口,为什么将它放在最开始的地方也是显而易见的,后续的过滤器中大概率会依赖 Session 信息和安全上下文信息。

UsernamePasswordAuthenticationFilter

表单认证是最常用的一个认证方式,一个最直观的业务场景便是允许用户在表单中输入用户名和密码进行登录,而这背后的UsernamePasswordAuthenticationFilter,在整个 Spring Security 的认证体系中则扮演着至关重要的角色。

2019620204316-user-authentication

上述的时序图,可以看出 UsernamePasswordAuthenticationFilter 主要肩负起了调用身份认证器,校验身份的作用,至于认证的细节,在前面几章花了很大篇幅进行了介绍,到这里,其实 Spring Security 的基本流程就已经走通了。

org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication

	public UsernamePasswordAuthenticationFilter() {
      //该过滤器默认拦截 /login 的 POST 请求
		super(new AntPathRequestMatcher("/login", "POST"));
	}
   //做简单的认证
	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();
      //组装成 username+password 形式的token,把改Token传递到具体的 Provider 做认证
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);
      //如果需要传递而外的信息到Provider可设置改字段
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
      //交给内部的AuthenticationManager去认证,并返回认证信息
		return this.getAuthenticationManager().authenticate(authRequest);
	}

UsernamePasswordAuthenticationFilter 本身的代码只包含了上述这么一个方法,非常简略,而在其父类 AbstractAuthenticationProcessingFilter 中包含了大量的细节,值得我们分析:

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
      implements ApplicationEventPublisherAware, MessageSourceAware {
	//包含了一个身份认证器
	private AuthenticationManager authenticationManager;
	//用于实现remeberMe
	private RememberMeServices rememberMeServices = new NullRememberMeServices();
	private RequestMatcher requiresAuthenticationRequestMatcher;
	//这两个Handler很关键,分别代表了认证成功和失败相应的处理器
	private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
	private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
	
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {

		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		...
		Authentication authResult;
		try {
			//此处实际上就是调用UsernamePasswordAuthenticationFilter的attemptAuthentication方法
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				//子类未完成认证,立刻返回
				return;
			}
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		//在认证过程中可以直接抛出异常,在过滤器中,就像此处一样,进行捕获
		catch (InternalAuthenticationServiceException failed) {
			//内部服务异常
			unsuccessfulAuthentication(request, response, failed);
			return;
		}
		catch (AuthenticationException failed) {
			//认证失败
			unsuccessfulAuthentication(request, response, failed);
			return;
		}
		//认证成功
		if (continueChainBeforeSuccessfulAuthentication) {
			chain.doFilter(request, response);
		}
		//注意,认证成功后过滤器把authResult结果也传递给了成功处理器
		successfulAuthentication(request, response, chain, authResult);
	}
}

整个流程理解起来也并不难,主要就是内部调用了 authenticationManager 完成认证,根据认证结果执行 successfulAuthentication 或者 unsuccessfulAuthentication ,无论成功失败,一般的实现都是转发或者重定向等处理,不再细究 AuthenticationSuccessHandlerAuthenticationFailureHandler ,有兴趣的朋友,可以去看看两者的实现类。

AnonymousAuthenticationFilter

匿名认证过滤器,可能有人会想:匿名了还有身份?我自己对于 Anonymous 匿名身份的理解是 Spirng Security 为了整体逻辑的统一性,即使是未通过认证的用户,也给予了一个匿名身份。而 AnonymousAuthenticationFilter 该过滤器的位置也是非常的科学的,它位于常用的身份认证过滤器(如 UsernamePasswordAuthenticationFilter、BasicAuthenticationFilter、RememberMeAuthenticationFilter )之后,意味着只有在上述身份过滤器执行完毕后,SecurityContext 依旧没有用户信息,AnonymousAuthenticationFilter 该过滤器才会有意义—-基于用户一个匿名身份。

org.springframework.security.web.authentication.AnonymousAuthenticationFilter

package org.springframework.security.web.authentication;

.....

/**
 * Detects if there is no {@code Authentication} object in the
 * {@code SecurityContextHolder}, and populates it with one if needed.
 *
 * @author Ben Alex
 * @author Luke Taylor
 */
public class AnonymousAuthenticationFilter extends GenericFilterBean implements
		InitializingBean {
	private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
	private String key;
	private Object principal;
	private List<GrantedAuthority> authorities;
	/**
	 * Creates a filter with a principal named "anonymousUser" and the single authority
	 * "ROLE_ANONYMOUS".
	 *
	 * @param key the key to identify tokens created by this filter
	 */
    //自动创建一个"anonymousUser"的匿名用户,其具有ANONYMOUS角色
	public AnonymousAuthenticationFilter(String key) {
		this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
	}

	/**
	 *
	 * @param key key the key to identify tokens created by this filter
	 * @param principal the principal which will be used to represent anonymous users
	 * @param authorities the authority list for anonymous users
	 */
	public AnonymousAuthenticationFilter(String key, Object principal,
			List<GrantedAuthority> authorities) {
		Assert.hasLength(key, "key cannot be null or empty");
		Assert.notNull(principal, "Anonymous authentication principal must be set");
		Assert.notNull(authorities, "Anonymous authorities must be set");
		this.key = key;
		this.principal = principal;
		this.authorities = authorities;
	}

	@Override
	public void afterPropertiesSet() {
		Assert.hasLength(key, "key must have length");
		Assert.notNull(principal, "Anonymous authentication principal must be set");
		Assert.notNull(authorities, "Anonymous authorities must be set");
	}

	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
      //过滤器链都执行到匿名认证过滤器这儿了还没有身份信息,塞一个匿名身份进去
		if (SecurityContextHolder.getContext().getAuthentication() == null) {
			SecurityContextHolder.getContext().setAuthentication(
					createAuthentication((HttpServletRequest) req));

			if (logger.isDebugEnabled()) {
				logger.debug("Populated SecurityContextHolder with anonymous token: '"
						+ SecurityContextHolder.getContext().getAuthentication() + "'");
			}
		}
		else {
			if (logger.isDebugEnabled()) {
				logger.debug("SecurityContextHolder not populated with anonymous token, as it already contained: '"
						+ SecurityContextHolder.getContext().getAuthentication() + "'");
			}
		}

		chain.doFilter(req, res);
	}
   //创建一个 AnonymousAuthenticationToken
	protected Authentication createAuthentication(HttpServletRequest request) {
		AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
				principal, authorities);
		auth.setDetails(authenticationDetailsSource.buildDetails(request));

		return auth;
	}

	public void setAuthenticationDetailsSource(
			AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
		Assert.notNull(authenticationDetailsSource,
				"AuthenticationDetailsSource required");
		this.authenticationDetailsSource = authenticationDetailsSource;
	}

	public Object getPrincipal() {
		return principal;
	}

	public List<GrantedAuthority> getAuthorities() {
		return authorities;
	}
}  

其实对比 AnonymousAuthenticationFilterUsernamePasswordAuthenticationFilter 就可以发现一些门道了, UsernamePasswordAuthenticationToken 对应 AnonymousAuthenticationToken ,他们都是 Authentication 的实现类,而 Authentication 则是被 SecurityContextHolder(SecurityContext) 持有的,一切都被串联在了一起。

ExceptionTranslationFilter

ExceptionTranslationFilter 异常转换过滤器位于整个 springSecurityFilterChain 的后方,用来转换整个链路中出现的异常,将其转化,顾名思义,转化以意味本身并不处理。一般其只处理两大类异常:AccessDeniedException 访问异常和 AuthenticationException 认证异常。

这个过滤器非常重要,因为它将 Java 中的异常和 HTTP 的响应连接在了一起,这样在处理异常时,我们不用考虑密码错误该跳到什么页面,账号锁定该如何,只需要关注自己的业务逻辑,抛出相应的异常便可。如果该过滤器检测到 AuthenticationException ,则将会交给内部的 AuthenticationEntryPoint 去处理,如果检测到 AccessDeniedException,需要先判断当前用户是不是匿名用户,如果是匿名访问,则和前面一样运行AuthenticationEntryPoint,否则会委托给AccessDeniedHandler 去处理,而 AccessDeniedHandler 的默认实现,是 AccessDeniedHandlerImpl 。所以 ExceptionTranslationFilter 内部的 AuthenticationEntryPoint 是至关重要的,顾名思义:认证的入口点。

public class ExceptionTranslationFilter extends GenericFilterBean {
  //处理异常转换的核心方法
  private void handleSpringSecurityException(HttpServletRequest request,
        HttpServletResponse response, FilterChain chain, RuntimeException exception)
        throws IOException, ServletException {
     if (exception instanceof AuthenticationException) {
       	//重定向到登录端点
        sendStartAuthentication(request, response, chain,
              (AuthenticationException) exception);
     }
     else if (exception instanceof AccessDeniedException) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
		  //重定向到登录端点
           sendStartAuthentication(
                 request,
                 response,
                 chain,
                 new InsufficientAuthenticationException(
                       "Full authentication is required to access this resource"));
        }
        else {
           //交给accessDeniedHandler处理
           accessDeniedHandler.handle(request, response,
                 (AccessDeniedException) exception);
        }
     }
  }
}

剩下的便是要搞懂 AuthenticationEntryPointAccessDeniedHandler 就可以了。 20196211823-AuthenticationEntryPoint-interface

选择了几个常用的登录端点,以其中第一个为例来介绍,看名字就能猜到是认证失败之后,让用户跳转到登录页面。还记得我们一开始怎么配置表单登录页面的吗?

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()//FormLoginConfigurer
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }
}

我们顺着 formLogin 返回的 FormLoginConfigurer 往下找,看看能发现什么,最终在 FormLoginConfigurer 的父类 AbstractAuthenticationFilterConfigurer 中有了不小的收获:

public abstract class AbstractAuthenticationFilterConfigurer extends ...{
   ...
   //formLogin不出所料配置了AuthenticationEntryPoint
   private LoginUrlAuthenticationEntryPoint authenticationEntryPoint;
   //认证失败的处理器
   private AuthenticationFailureHandler failureHandler;
   ...
}

具体如何配置的就不看了,我们得出了结论,formLogin() 配置了之后最起码做了两件事,其一,为 UsernamePasswordAuthenticationFilter 设置了相关的配置,其二配置了 AuthenticationEntryPoint

登录端点还有 Http401AuthenticationEntryPointHttp403ForbiddenEntryPoint 这些都是很简单的实现,有时候我们访问受限页面,又没有配置登录,就看到了一个空荡荡的默认错误页面,上面显示着401,403,就是这两个入口起了作用。

还剩下一个 AccessDeniedHandler 访问决策器未被讲解,简单提一下:AccessDeniedHandlerImpl 这个默认实现类会根据 errorPage 和状态码来判断,最终决定跳转的页面

org.springframework.security.web.access.AccessDeniedHandlerImpl#handle

public void handle(HttpServletRequest request, HttpServletResponse response,
      AccessDeniedException accessDeniedException) throws IOException,
      ServletException {
   if (!response.isCommitted()) {
      if (errorPage != null) {
         // Put exception into request scope (perhaps of use to a view)
         request.setAttribute(WebAttributes.ACCESS_DENIED_403,
               accessDeniedException);
         // Set the 403 status code.
         response.setStatus(HttpServletResponse.SC_FORBIDDEN);
         // forward to error page.
         RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
         dispatcher.forward(request, response);
      }
      else {
         response.sendError(HttpServletResponse.SC_FORBIDDEN,
               accessDeniedException.getMessage());
      }
   }
}

自己在实际项目中处理 AuthenticationException 异常是在个入口Filter中去做的,比如 AjaxLoginProcessingFilter extends AbstractAuthenticationProcessingFilter,通过看 AbstractAuthenticationProcessingFilter 源码:

	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
		if (logger.isDebugEnabled()) {
			logger.debug("Request is to process authentication");
		}
		Authentication authResult;
		try {
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				// authentication
				return;
			}
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		catch (InternalAuthenticationServiceException failed) {
			logger.error(
					"An internal error occurred while trying to authenticate the user.",
					failed);
         //注意这里的异常交给了unsuccessfulAuthentication处理
			unsuccessfulAuthentication(request, response, failed);

			return;
		}
		catch (AuthenticationException failed) {
         //同样这里的AuthenticationException也是
			// Authentication failed
			unsuccessfulAuthentication(request, response, failed);

			return;
		}
		// Authentication success
		if (continueChainBeforeSuccessfulAuthentication) {
			chain.doFilter(request, response);
		}
		successfulAuthentication(request, response, chain, authResult);
	}
   //最终能暴露给用户自定义处理异常的接口 failureHandle(AuthenticationFailureHandler)
	protected void unsuccessfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, AuthenticationException failed)
			throws IOException, ServletException {
		SecurityContextHolder.clearContext();
      ......
		rememberMeServices.loginFail(request, response);
		failureHandler.onAuthenticationFailure(request, response, failed);
	}

FilterSecurityInterceptor

想想整个认证安全控制流程还缺了什么?我们已经有了认证,有了请求的封装,有了 Session 的关联…还缺一个:由什么控制哪些资源是受限的,这些受限的资源需要什么权限,需要什么角色…这一切和访问控制相关的操作,都是由 FilterSecurityInterceptor 完成的。

FilterSecurityInterceptor 的工作流程用笔者的理解可以理解如下:FilterSecurityInterceptorSecurityContextHolder 中获取 Authentication 对象,然后比对用户拥有的权限和资源所需的权限。前者可以通过 Authentication 对象直接获得,而后者则需要引入我们之前一直未提到过的两个类:SecurityMetadataSourceAccessDecisionManager 。理解清楚决策管理器的整个创建流程和 SecurityMetadataSource 的作用需要花很大一笔功夫,这里,暂时只介绍其大概的作用。

JavaConfig 的配置中,我们通常如下配置路径的访问控制一般有两种方式,第一种是直接写死角色或权限,第二种是动态从数据库加载,第一种粒度比较大,灵活性不太好。第二种有较高的灵活性,在程序启动的时候从数据库加载数据,但是像做到在运行时 reload 权限,目前来看除非在运行时 reload 相关的 bean(木有经过实战)

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
					.antMatchers("/", "/home").permitAll()
					.antMatchers("/admin/**").hasRole("ADMIN")
					.antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
					.anyRequest().authenticated()
                .and()
					.formLogin()
					.loginPage("/login")
					.permitAll()
                .and()
					.logout()
					.permitAll();
    }

总结

本篇文章在介绍过滤器时,顺便进行了一些源码的分析,目的是方便理解整个 Spring Security 的工作流。伴随着整个过滤器链的介绍,安全框架的轮廓应该已经浮出水面了,下面的章节,主要打算通过自定义一些需求,再次分析其他组件的源码,学习应该如何改造 Spring Security,为我们所用。

原文链接:http://blog.didispace.com/xjf-spring-security-4/