Spring Security 原理总结

5.1 SpringSecurity 的过滤器介绍

SpringSecurity采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤器链的15个过滤器进行说明

  1. WebAsyncManagerIntegrationFilter:将Security 上下文与Spring Web 中用于处理异步请求映射的WebAsyncManager 进行集成。
  2. SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到SecurityContextHolder 中,然后在该次请求处理完成之后,将SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将SecurityContextHolder 中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的。
  3. HeaderWriterFilter:用于将头信息加入响应中。
  4. CsrfFilter:用于处理跨站请求伪造。
  5. LogoutFilter:用于处理退出登录。
  6. UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自/login的请求。从表单中获取用户名和密码时,默认使用的表单name 值为username和password,这两个值可以通过设置这个过滤器的usernameParameter 和passwordParameter 两个参数的值进行修改。
  7. DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
  8. BasicAuthenticationFilter:检测和处理http basic 认证。
  9. RequestCacheAwareFilter:用来处理请求的缓存。
  10. SecurityContextHolderAwareRequestFilter:主要是包装请求对象request。
  11. AnonymousAuthenticationFilter:检测SecurityContextHolder 中是否存在Authentication 对象,如果不存在为其提供一个匿名Authentication。
  12. SessionManagementFilter:管理session 的过滤器。
  13. ExceptionTranslationFilter:处理AccessDeniedException 和AuthenticationException 异常。
  14. FilterSecurityInterceptor:可以看做过滤器链的出口。
  15. RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从cookie 里找出用户的信息, 如果Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。

5.2 SpringSecurity基本流程

Spring Security采取过滤链实现认证与授权,只有当前过滤器通过,才能进入下一个过滤器

image

绿色部分是认证过滤器,需要我们自己配置,可以配置多个认证过滤器。认证过滤器可以使用Spring Security提供的认证过滤器,也可以自定义过滤器(例如:短信验证)。认证过滤器要在configure(HttpSecurity http)方法中配置,没有配置不生效。下面会重点介绍以下三个过滤器:

  • UsernamePasswordAuthenticationFilter过滤器:该过滤器会拦截前端提交的POST 方式的登录表单请求,并进行身份认证。
  • ExceptionTranslationFilter过滤器:该过滤器不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。
  • FilterSecurityInterceptor过滤器:该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,并由ExceptionTranslationFilter过滤器进行捕获和处理

5.3 SpringSecurity认证流程

认证流程是在UsernamePasswordAuthenticationFilter过滤器中处理的,具体流程如下所示:

image

5.3.1 UsernamePasswordAuthenticationFilter源码

当前端提交的是一个POST 方式的登录表单请求,就会被该过滤器拦截,并进行身份认证。

该过滤器的doFilter() 方法实现首先在其抽象父类AbstractAuthenticationProcessingFilter中,查看相关源码:

/**
 * Invokes the
 * {@link #requiresAuthentication(HttpServletRequest, HttpServletResponse)
 * requiresAuthentication} method to determine whether the request is for
 * authentication and should be handled by this filter. If it is an authentication
 * request, the
 * {@link #attemptAuthentication(HttpServletRequest, HttpServletResponse)
 * attemptAuthentication} will be invoked to perform the authentication. There are
 * then three possible outcomes:
 * <ol>
 * <li>An <tt>Authentication</tt> object is returned. The configured
 * {@link SessionAuthenticationStrategy} will be invoked (to handle any
 * session-related behaviour such as creating a new session to protect against
 * session-fixation attacks) followed by the invocation of
 * {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, Authentication)}
 * method</li>
 * <li>An <tt>AuthenticationException</tt> occurs during authentication. The
 * {@link #unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException)
 * unsuccessfulAuthentication} method will be invoked</li>
 * <li>Null is returned, indicating that the authentication process is incomplete. The
 * method will then return immediately, assuming that the subclass has done any
 * necessary work (such as redirects) to continue the authentication process. The
 * assumption is that a later request will be received by this method where the
 * returned <tt>Authentication</tt> object is not null.
 * </ol>
 */
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(request, response, failed);

		return;
	}
	catch (AuthenticationException failed) {
		// Authentication failed
		unsuccessfulAuthentication(request, response, failed);

		return;
	}

	// Authentication success
	if (continueChainBeforeSuccessfulAuthentication) {
		chain.doFilter(request, response);
	}

	successfulAuthentication(request, response, chain, authResult);
}
  1. 判断一个请求是否要再这层filter进行处理

    if (!requiresAuthentication(request, response)) {
        chain.doFilter(request, response);
    
        return;
    }
    
  2. 调用子类 UsernamePasswordAuthenticationFilter 重写的方法进行身份认证,返回的 authResult 对象封装认证后的用户信息

    Authentication 是用来存储用户认证信息的类,后续会进行详细介绍

    Authentication authResult;
    
    try {
        authResult = attemptAuthentication(request, response);
        if (authResult == null) {
            // return immediately as subclass has indicated that it hasn't completed
            // authentication
            return;
        }
    
  3. Session 策略处理(如果配置了用户 Session 最大并发数,就是在此处进行判断并处理)

    sessionStrategy.onAuthentication(authResult, request, response);
    
  4. 认证失败,调用认证失败的处理器

    catch (InternalAuthenticationServiceException failed) {
        logger.error(
            "An internal error occurred while trying to authenticate the user.",
            failed);
        unsuccessfulAuthentication(request, response, failed);
    
        return;
    }
    
    /**
     * Default behaviour for unsuccessful authentication.
     * <ol>
     * <li>Clears the {@link SecurityContextHolder}</li>
     * <li>Stores the exception in the session (if it exists or
     * <tt>allowSesssionCreation</tt> is set to <tt>true</tt>)</li>
     * <li>Informs the configured <tt>RememberMeServices</tt> of the failed login</li>
     * <li>Delegates additional behaviour to the {@link AuthenticationFailureHandler}.</li>
     * </ol>
     */
    protected void unsuccessfulAuthentication(HttpServletRequest request,
    		HttpServletResponse response, AuthenticationException failed)
    		throws IOException, ServletException {
    	SecurityContextHolder.clearContext();
    
    	if (logger.isDebugEnabled()) {
    		logger.debug("Authentication request failed: " + failed.toString(), failed);
    		logger.debug("Updated SecurityContextHolder to contain null Authentication");
    		logger.debug("Delegating to authentication failure handler " + failureHandler);
    	}
    
    	rememberMeServices.loginFail(request, response);
    
    	failureHandler.onAuthenticationFailure(request, response, failed);
    }
    
  5. 认证成功的处理

    默认的 continueChainBeforeSuccessfulAuthentication为 false,所以认证成功之后不进入下一个过滤器

    调用认证成功的处理器

    // Authentication success
    if (continueChainBeforeSuccessfulAuthentication) {
        chain.doFilter(request, response);
    }
    
    successfulAuthentication(request, response, chain, authResult);
    

上述的第二过程调用了UsernamePasswordAuthenticationFilter的attemptAuthentication() 方法,源码如下:

  1. 默认表单用户名参数为 username
  2. 默认密码参数为 password
  3. 默认请求方式只能为POST
  4. 默认登录表单提交路径为/logtn,POST 方式请求
/**
 * Processes an authentication form submission. Called
 * {@code AuthenticationProcessingFilter} prior to Spring Security 3.0.
 * <p>
 * Login forms must present two parameters to this filter: a username and password. The
 * default parameter names to use are contained in the static fields
 * {@link #SPRING_SECURITY_FORM_USERNAME_KEY} and
 * {@link #SPRING_SECURITY_FORM_PASSWORD_KEY}. The parameter names can also be changed by
 * setting the {@code usernameParameter} and {@code passwordParameter} properties.
 * <p>
 * This filter by default responds to the URL {@code /login}.
 *
 * @author Ben Alex
 * @author Colin Sampaleanu
 * @author Luke Taylor
 * @since 3.0
 */
public class UsernamePasswordAuthenticationFilter extends
		AbstractAuthenticationProcessingFilter {
	// ~ Static fields/initializers
	// =====================================================================================

	public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

	private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
	private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
	private boolean postOnly = true;

	// ~ Constructors
	// ===================================================================================================

	public UsernamePasswordAuthenticationFilter() {
		super(new AntPathRequestMatcher("/login", "POST"));
	}
public Authentication attemptAuthentication(HttpServletRequest request,
                                            HttpServletResponse response) throws AuthenticationException {
    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();

    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
        username, password);

    // Allow subclasses to set the "details" property
    setDetails(request, authRequest);

    return this.getAuthenticationManager().authenticate(authRequest);
}

上述的 doFilter()方法调用此 attenptAuthentication()方法进行身份认证

  1. 默认情况下,如果请求方式不是 POST,会抛出异常

    if (postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException(
            "Authentication method not supported: " + request.getMethod());
    }
    
  2. 获取请求携带的 usernane 和 password

    String username = obtainUsername(request);
    String password = obtainPassword(request);
    
    if (username == null) {
        username = "";
    }
    
    if (password == null) {
        password = "";
    }
    
  3. 使用前端传入的 username、password 构造 Authent1cation 对象,标记该对象未认证

    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
    				username, password);
    
  4. 将请求中的一些属性信息设置到 Authentication 对象中,如:remoteAddress、sessionId

    // Allow subclasses to set the "details" property
    setDetails(request, authRequest);
    
  5. 调用 ProviderManager 类的 authenticate()方法进行身份认证

    return this.getAuthenticationManager().authenticate(authRequest);
    

上述的(3)过程创建的UsernamePasswordAuthenticationToken是Authentication 接口的实现类,该类有两个构造器,一个用于封装前端请求传入的未认证的用户信息,一个用于封装认证成功后的用户信息:

/**
 * An {@link org.springframework.security.core.Authentication} implementation that is
 * designed for simple presentation of a username and password.
 * <p>
 * The <code>principal</code> and <code>credentials</code> should be set with an
 * <code>Object</code> that provides the respective property via its
 * <code>Object.toString()</code> method. The simplest such <code>Object</code> to use is
 * <code>String</code>.
 *
 * @author Ben Alex
 */
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	// ~ Instance fields
	// ================================================================================================

	private final Object principal;
	private Object credentials;

	// ~ Constructors
	// ===================================================================================================

	/**
	 * This constructor can be safely used by any code that wishes to create a
	 * <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
	 * will return <code>false</code>.
	 * 用于封装前端请求传入的未认证的用户信息,前面的 authRequest 对象就是调用该构造器进行构造的
	 */
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);					// 用户权限为 null
		this.principal = principal;		// 前端传入的用户名
		this.credentials = credentials;	// 前端传入的密码
		setAuthenticated(false);		// 标记未认证
	}

	/**
	 * This constructor should only be used by <code>AuthenticationManager</code> or
	 * <code>AuthenticationProvider</code> implementations that are satisfied with
	 * producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
	 * authentication token.
	 * 用于封装认证成功后的用户信息
	 *
	 * @param principal
	 * @param credentials
	 * @param authorities
	 */
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		super(authorities);				// 用户权限集合
		this.principal = principal;		// 封装认证用户信息的UserDetails对象,不再是用户名
		this.credentials = credentials;	// 前端传入的密码
		super.setAuthenticated(true); 	// 标记认证成功
	}

Authentication 接口的实现类用于存储用户认证信息,查看该接口具体定义:

/**
 * Represents the token for an authentication request or for an authenticated principal
 * once the request has been processed by the
 * {@link AuthenticationManager#authenticate(Authentication)} method.
 * <p>
 * Once the request has been authenticated, the <tt>Authentication</tt> will usually be
 * stored in a thread-local <tt>SecurityContext</tt> managed by the
 * {@link SecurityContextHolder} by the authentication mechanism which is being used. An
 * explicit authentication can be achieved, without using one of Spring Security's
 * authentication mechanisms, by creating an <tt>Authentication</tt> instance and using
 * the code:
 *
 * <pre>
 * SecurityContextHolder.getContext().setAuthentication(anAuthentication);
 * </pre>
 *
 * Note that unless the <tt>Authentication</tt> has the <tt>authenticated</tt> property
 * set to <tt>true</tt>, it will still be authenticated by any security interceptor (for
 * method or web invocations) which encounters it.
 * <p>
 * In most cases, the framework transparently takes care of managing the security context
 * and authentication objects for you.
 *
 * @author Ben Alex
 */
public interface Authentication extends Principal, Serializable {
	// ~ Methods
	// ========================================================================================================

	/**
	 * Set by an <code>AuthenticationManager</code> to indicate the authorities that the
	 * principal has been granted. Note that classes should not rely on this value as
	 * being valid unless it has been set by a trusted <code>AuthenticationManager</code>.
	 * <p>
	 * Implementations should ensure that modifications to the returned collection array
	 * do not affect the state of the Authentication object, or use an unmodifiable
	 * instance.
	 * </p>
	 *
	 * @return the authorities granted to the principal, or an empty collection if the
	 * token has not been authenticated. Never null.
	 */
	Collection<? extends GrantedAuthority> getAuthorities();

	/**
	 * The credentials that prove the principal is correct. This is usually a password,
	 * but could be anything relevant to the <code>AuthenticationManager</code>. Callers
	 * are expected to populate the credentials.
	 *
	 * @return the credentials that prove the identity of the <code>Principal</code>
	 */
	Object getCredentials();

	/**
	 * Stores additional details about the authentication request. These might be an IP
	 * address, certificate serial number etc.
	 *
	 * @return additional details about the authentication request, or <code>null</code>
	 * if not used
	 */
	Object getDetails();

	/**
	 * The identity of the principal being authenticated. In the case of an authentication
	 * request with username and password, this would be the username. Callers are
	 * expected to populate the principal for an authentication request.
	 * <p>
	 * The <tt>AuthenticationManager</tt> implementation will often return an
	 * <tt>Authentication</tt> containing richer information as the principal for use by
	 * the application. Many of the authentication providers will create a
	 * {@code UserDetails} object as the principal.
	 *
	 * @return the <code>Principal</code> being authenticated or the authenticated
	 * principal after authentication.
	 */
	Object getPrincipal();

	/**
	 * Used to indicate to {@code AbstractSecurityInterceptor} whether it should present
	 * the authentication token to the <code>AuthenticationManager</code>. Typically an
	 * <code>AuthenticationManager</code> (or, more often, one of its
	 * <code>AuthenticationProvider</code>s) will return an immutable authentication token
	 * after successful authentication, in which case that token can safely return
	 * <code>true</code> to this method. Returning <code>true</code> will improve
	 * performance, as calling the <code>AuthenticationManager</code> for every request
	 * will no longer be necessary.
	 * <p>
	 * For security reasons, implementations of this interface should be very careful
	 * about returning <code>true</code> from this method unless they are either
	 * immutable, or have some way of ensuring the properties have not been changed since
	 * original creation.
	 *
	 * @return true if the token has been authenticated and the
	 * <code>AbstractSecurityInterceptor</code> does not need to present the token to the
	 * <code>AuthenticationManager</code> again for re-authentication.
	 */
	boolean isAuthenticated();

	/**
	 * See {@link #isAuthenticated()} for a full description.
	 * <p>
	 * Implementations should <b>always</b> allow this method to be called with a
	 * <code>false</code> parameter, as this is used by various classes to specify the
	 * authentication token should not be trusted. If an implementation wishes to reject
	 * an invocation with a <code>true</code> parameter (which would indicate the
	 * authentication token is trusted - a potential security risk) the implementation
	 * should throw an {@link IllegalArgumentException}.
	 *
	 * @param isAuthenticated <code>true</code> if the token should be trusted (which may
	 * result in an exception) or <code>false</code> if the token should not be trusted
	 *
	 * @throws IllegalArgumentException if an attempt to make the authentication token
	 * trusted (by passing <code>true</code> as the argument) is rejected due to the
	 * implementation being immutable or implementing its own alternative approach to
	 * {@link #isAuthenticated()}
	 */
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

5.3.2 ProviderManager 源码

上述过程中,UsernamePasswordAuthenticationFilter过滤器的attemptAuthentication() 方法的(5)过程将未认证的Authentication 对象传入ProviderManager 类的authenticate() 方法进行身份认证。

ProviderManager 是AuthenticationManager 接口的实现类,该接口是认证相关的核心接口,也是认证的入口。在实际开发中,我们可能有多种不同的认证方式,例如:用户名+密码、邮箱+密码、手机号+验证码等,而这些认证方式的入口始终只有一个,那就是AuthenticationManager。在该接口的常用实现类ProviderManager 内部会维护一个List列表,存放多种认证方式,实际上这是委托者模式(Delegate)的应用。每种认证方式对应着一个AuthenticationProvider,AuthenticationManager 根据认证方式的不同(根据传入的Authentication 类型判断)委托对应的AuthenticationProvider 进行用户认证。

/**
 * Attempts to authenticate the passed {@link Authentication} object.
 * <p>
 * The list of {@link AuthenticationProvider}s will be successively tried until an
 * <code>AuthenticationProvider</code> indicates it is capable of authenticating the
 * type of <code>Authentication</code> object passed. Authentication will then be
 * attempted with that <code>AuthenticationProvider</code>.
 * <p>
 * If more than one <code>AuthenticationProvider</code> supports the passed
 * <code>Authentication</code> object, the first one able to successfully
 * authenticate the <code>Authentication</code> object determines the
 * <code>result</code>, overriding any possible <code>AuthenticationException</code>
 * thrown by earlier supporting <code>AuthenticationProvider</code>s.
 * On successful authentication, no subsequent <code>AuthenticationProvider</code>s
 * will be tried.
 * If authentication was not successful by any supporting
 * <code>AuthenticationProvider</code> the last thrown
 * <code>AuthenticationException</code> will be rethrown.
 *
 * @param authentication the authentication request object.
 *
 * @return a fully authenticated object including credentials.
 *
 * @throws AuthenticationException if authentication fails.
 */
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);
			// SEC-546: Avoid polling additional providers if auth failure is due to
			// invalid account status
			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) {
			// ignore as we will throw below if no other exception occurred prior to
			// calling parent and the parent
			// may throw ProviderNotFound even though a provider in the child already
			// handled the request
		}
		catch (AuthenticationException e) {
			lastException = parentException = e;
		}
	}

	if (result != null) {
		if (eraseCredentialsAfterAuthentication
				&& (result instanceof CredentialsContainer)) {
			// Authentication is complete. Remove credentials and other secret data
			// from authentication
			((CredentialsContainer) result).eraseCredentials();
		}

		// If the parent AuthenticationManager was attempted and successful then it will publish an AuthenticationSuccessEvent
		// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
		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 the parent AuthenticationManager was attempted and failed then it will publish an AbstractAuthenticationFailureEvent
	// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
	if (parentException == null) {
		prepareException(lastException, authentication);
	}

	throw lastException;
}
  1. 获取传入的 Authentication 类型,即 UsernamePasswordAuthenticationToken.class

    Class<? extends Authentication> toTest = authentication.getClass();
    
  2. 循环迭代

    for (AuthenticationProvider provider : getProviders()) {
    
  3. 判断当前 AuthenticationProvider是否适用UsernanePasswordAuthenticationToken.class类型的

    if (!provider.supports(toTest)) {
        continue;
    }
    

    成功找到适配当前认证方式的 AuthenticationProvider,此处为 DaoAuthenticationProvider

  4. 调用 DaoAuthenticationProvider的 authenticate()方法进行认证。如果认证成功,会返回一个标记已认证的 Authent1cation 对象

    result = provider.authenticate(authentication);
    
  5. 认证成功后,将传入的 Authentication对象中的 deta1ls 信息拷贝到已认证的 Authentication对象

    if (result != null) {
        copyDetails(authentication, result);
        break;
    }
    
  6. 认证成功之后,去除 result 的敏感信息,要求相关类实现CredentialsContainer 接口

    去除过程就是调用 CredentialsContainer 接口的 eraseCredentials()方法

    if (eraseCredentialsAfterAuthentication
        && (result instanceof CredentialsContainer)) {
        // Authentication is complete. Remove credentials and other secret data
        // from authentication
        ((CredentialsContainer) result).eraseCredentials();
    }
    
  7. 发布认证成功的事件

    // If the parent AuthenticationManager was attempted and successful then it will publish an AuthenticationSuccessEvent
    // This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
    if (parentResult == null) {
        eventPublisher.publishAuthenticationSuccess(result);
    }
    
  8. 认证失败之后,抛出失败的异常信息

    if (lastException == null) {
        lastException = new ProviderNotFoundException(messages.getMessage(
            "ProviderManager.providerNotFound",
            new Object[] { toTest.getName() },
            "No AuthenticationProvider found for {0}"));
    }
    
    // If the parent AuthenticationManager was attempted and failed then it will publish an AbstractAuthenticationFailureEvent
    // This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
    if (parentException == null) {
        prepareException(lastException, authentication);
    }
    

上述认证成功之后的(6)过程,调用CredentialsContainer 接口定义的eraseCredentials() 方法去除敏感信息。查看UsernamePasswordAuthenticationToken 实现的eraseCredentials() 方法,该方法实现在其父类中:

父类实现了 CredentialsContainer 接口

/**
 * Base class for <code>Authentication</code> objects.
 * <p>
 * Implementations which use this class should be immutable.
 *
 * @author Ben Alex
 * @author Luke Taylor
 */
public abstract class AbstractAuthenticationToken implements Authentication,
        CredentialsContainer {
    /**
     * Checks the {@code credentials}, {@code principal} and {@code details} objects,
     * invoking the {@code eraseCredentials} method on any which implement
     * {@link CredentialsContainer}.
     */
    public void eraseCredentials() {
        eraseSecret(getCredentials());
        eraseSecret(getPrincipal());
        eraseSecret(details);
    }

    private void eraseSecret(Object secret) {
        if (secret instanceof CredentialsContainer) {
            ((CredentialsContainer) secret).eraseCredentials();
        }
    }
  1. credentials(前端传入的密码)会置为 null

    eraseSecret(getCredentials());
    
  2. principal 在已认证的 Authentication 中是 UserDetails 实现类;如果该实现类想要去除敏感信息,需要实现 CredentialsContainer 接口的 eraseCredentials()方法;由于我们自定义的 user 类没有实现该接口,所以不进行任何操作。

    eraseSecret(getPrincipal());
    

5.3.3 认证成功/失败处理

上述过程就是认证流程的最核心部分,接下来重新回到UsernamePasswordAuthenticationFilter过滤器的doFilter() 方法,查看认证成功/失败的处理

查看 successfulAuthentication()和unsuccessfulAuthentication()方法源码:

/**
 * Default behaviour for successful authentication.
 * <ol>
 * <li>Sets the successful <tt>Authentication</tt> object on the
 * {@link SecurityContextHolder}</li>
 * <li>Informs the configured <tt>RememberMeServices</tt> of the successful login</li>
 * <li>Fires an {@link InteractiveAuthenticationSuccessEvent} via the configured
 * <tt>ApplicationEventPublisher</tt></li>
 * <li>Delegates additional behaviour to the {@link AuthenticationSuccessHandler}.</li>
 * </ol>
 *
 * Subclasses can override this method to continue the {@link FilterChain} after
 * successful authentication.
 * @param request
 * @param response
 * @param chain
 * @param authResult the object returned from the <tt>attemptAuthentication</tt>
 * method.
 * @throws IOException
 * @throws ServletException
 */
protected void successfulAuthentication(HttpServletRequest request,
		HttpServletResponse response, FilterChain chain, Authentication authResult)
		throws IOException, ServletException {

	if (logger.isDebugEnabled()) {
		logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
				+ authResult);
	}

	SecurityContextHolder.getContext().setAuthentication(authResult);

	rememberMeServices.loginSuccess(request, response, authResult);

	// Fire event
	if (this.eventPublisher != null) {
		eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
				authResult, this.getClass()));
	}

	successHandler.onAuthenticationSuccess(request, response, authResult);
}

认证成功后的处理:

  1. 将认证成功的用户信息对象Authentication封装进 SecurityContext 对象中,并存入 SecurityContext

    SecurityContextHolder是对 ThreadLocal 的一个封装,后续会介绍

    SecurityContextHolder.getContext().setAuthentication(authResult);
    
  2. renenberMe 的处理

    rememberMeServices.loginSuccess(request, response, authResult);
    
  3. 发布认证成功的事件

    // Fire event
    if (this.eventPublisher != null) {
        eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
            authResult, this.getClass()));
    }
    
  4. 调用认证成功处理器

    successHandler.onAuthenticationSuccess(request, response, authResult);
    
/**
 * Default behaviour for unsuccessful authentication.
 * <ol>
 * <li>Clears the {@link SecurityContextHolder}</li>
 * <li>Stores the exception in the session (if it exists or
 * <tt>allowSesssionCreation</tt> is set to <tt>true</tt>)</li>
 * <li>Informs the configured <tt>RememberMeServices</tt> of the failed login</li>
 * <li>Delegates additional behaviour to the {@link AuthenticationFailureHandler}.</li>
 * </ol>
 */
protected void unsuccessfulAuthentication(HttpServletRequest request,
		HttpServletResponse response, AuthenticationException failed)
		throws IOException, ServletException {
	SecurityContextHolder.clearContext();

	if (logger.isDebugEnabled()) {
		logger.debug("Authentication request failed: " + failed.toString(), failed);
		logger.debug("Updated SecurityContextHolder to contain null Authentication");
		logger.debug("Delegating to authentication failure handler " + failureHandler);
	}

	rememberMeServices.loginFail(request, response);

	failureHandler.onAuthenticationFailure(request, response, failed);
}

认证失败后的处理:

  1. 清除该线程在 SecurityContextHolder 中对应的 SecurityContext 对象

    SecurityContextHolder.clearContext();
    
    if (logger.isDebugEnabled()) {
        logger.debug("Authentication request failed: " + failed.toString(), failed);
        logger.debug("Updated SecurityContextHolder to contain null Authentication");
        logger.debug("Delegating to authentication failure handler " + failureHandler);
    }
    
  2. remenberMe 的处理

    rememberMeServices.loginFail(request, response);
    
  3. 调用认证失败处理器

    failureHandler.onAuthenticationFailure(request, response, failed);
    

image

5.4 SpringSecurity权限访问流程

上一个部分通过源码的方式介绍了认证流程,下面介绍权限访问流程,主要是对ExceptionTranslationFilter过滤器和FilterSecurityInterceptor过滤器进行介绍。

image

5.4.1 ExceptionTranslationFilter过滤器

该过滤器是用于处理异常的,不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。具体源码如下:

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

	try {
		chain.doFilter(request, response);

		logger.debug("Chain processed normally");
	}
	catch (IOException ex) {
		throw ex;
	}
	catch (Exception ex) {
		// Try to extract a SpringSecurityException from the stacktrace
		Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
		RuntimeException ase = (AuthenticationException) throwableAnalyzer
				.getFirstThrowableOfType(AuthenticationException.class, causeChain);

		if (ase == null) {
			ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
					AccessDeniedException.class, causeChain);
		}

		if (ase != null) {
			if (response.isCommitted()) {
				throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
			}
			handleSpringSecurityException(request, response, chain, ase);
		}
		else {
			// Rethrow ServletExceptions and RuntimeExceptions as-is
			if (ex instanceof ServletException) {
				throw (ServletException) ex;
			}
			else if (ex instanceof RuntimeException) {
				throw (RuntimeException) ex;
			}

			// Wrap other Exceptions. This shouldn't actually happen
			// as we've already covered all the possibilities for doFilter
			throw new RuntimeException(ex);
		}
	}
}
  1. 对于前端提交的请求会直接放行,不进行拦截

    try {
        chain.doFilter(request, response);
    
        logger.debug("Chain processed normally");
    }
    
  2. 捕获后续出现的异常进行处理

    catch (Exception ex) {
        // Try to extract a SpringSecurityException from the stacktrace
        Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
        RuntimeException ase = (AuthenticationException) throwableAnalyzer
            .getFirstThrowableOfType(AuthenticationException.class, causeChain);
    
        if (ase == null) {
            ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
                AccessDeniedException.class, causeChain);
        }
    

    访问需要认证的资源,但当前请求未认证所抛出的异常

    RuntimeException ase = (AuthenticationException) throwableAnalyzer
            .getFirstThrowableOfType(AuthenticationException.class, causeChain);
    

    访问权限受限的资源所抛出的异常

    ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
                AccessDeniedException.class, causeChain);
    

5.4.2 FilterSecurityInterceptor过滤器

FilterSecurityInterceptor是过滤器链的最后一个过滤器,该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,最终所抛出的异常会由前一个过滤器ExceptionTranslationFilter进行捕获和处理。

具体源码如下:

/**
 * 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);
}
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 && observeOncePerRequest) {
			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);
	}
}
  1. 根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果不能访问,则抛出相应的异常

    InterceptorStatusToken token = super.beforeInvocation(fi);
    
  2. 访问相关资源,通过 SpringMVc 的核心组件 DispatcherServlet 进行访问

    fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
    

需要注意,Spring Security的过滤器链是配置在SpringMVC 的核心组件DispatcherServlet 运行之前。也就是说,请求通过Spring Security的所有过滤器,不意味着能够正常访问资源,该请求还需要通过SpringMVC 的拦截器链。

5.5 SpringSecurity请求间共享认证信息

一般认证成功后的用户信息是通过Session 在多个请求之间共享,那么Spring Security中是如何实现将已认证的用户信息对象Authentication 与Session 绑定的进行具体分析。

在前面讲解认证成功的处理方法successfulAuthentication() 时,有以下代码:

protected void successfulAuthentication(HttpServletRequest request,
		HttpServletResponse response, FilterChain chain, Authentication authResult)
		throws IOException, ServletException {

	if (logger.isDebugEnabled()) {
		logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
				+ authResult);
	}

	SecurityContextHolder.getContext().setAuthentication(authResult);

查看SecurityContext 接口及其实现类SecurityContextImpl,该类其实就是对Authentication 的封装

/**
 * Base implementation of {@link SecurityContext}.
 * <p>
 * Used by default by {@link SecurityContextHolder} strategies.
 *
 * @author Ben Alex
 */
public class SecurityContextImpl implements SecurityContext {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	// ~ Instance fields
	// ================================================================================================

	private Authentication authentication;

	public SecurityContextImpl() {}

	public SecurityContextImpl(Authentication authentication) {
		this.authentication = authentication;
	}

查看SecurityContextHolder类 , 该类其实是对ThreadLocal的封装 ,存储SecurityContext 对象

public class SecurityContextHolder {
	// ~ Static fields/initializers
	// =====================================================================================

	public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
	public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
	public static final String MODE_GLOBAL = "MODE_GLOBAL";
	public static final String SYSTEM_PROPERTY = "spring.security.strategy";
	private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
	private static SecurityContextHolderStrategy strategy;
	private static int initializeCount = 0;

默认使用MODE_THREADLOCAL模式

默认使用 ThreadLocalSecur1tyContextHolderStrategy 创建 strategy,其内部使用 ThreadLocal对SecurityContext进行存储

private static void initialize() {
    if (!StringUtils.hasText(strategyName)) {
        // Set default
        strategyName = MODE_THREADLOCAL;
    }

	if (!StringUtils.hasText(strategyName)) {
		// Set default
		strategyName = MODE_THREADLOCAL;
	}

	if (strategyName.equals(MODE_THREADLOCAL)) {
		strategy = new ThreadLocalSecurityContextHolderStrategy();
	}
	else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
		strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
	}
	else if (strategyName.equals(MODE_GLOBAL)) {
		strategy = new GlobalSecurityContextHolderStrategy();
	}
	else {
		// Try to load a custom strategy
		try {
			Class<?> clazz = Class.forName(strategyName);
			Constructor<?> customStrategy = clazz.getConstructor();
			strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
		}
		catch (Exception ex) {
			ReflectionUtils.handleReflectionException(ex);
		}
	}

	initializeCount++;
}

需要注意,如果当前线程对应的 ThreadLocal没有任何对象存储,strategy.getContext()会创建并返回一个空的 SecurityContext 对象,并且该空的 SecurityContext 对象会存入 ThreadLocal

/**
 * Obtain the current <code>SecurityContext</code>.
 *
 * @return the security context (never <code>null</code>)
 */
public static SecurityContext getContext() {
	return strategy.getContext();
}

设置当前线程对应的 ThreadLocal的存储

/**
 * Associates a new <code>SecurityContext</code> with the current thread of execution.
 *
 * @param context the new <code>SecurityContext</code> (may not be <code>null</code>)
 */
public static void setContext(SecurityContext context) {
	strategy.setContext(context);
}

清空当前线程对应的 ThreadLocal的存储

/**
 * Explicitly clears the context value from the current thread.
 */
public static void clearContext() {
	strategy.clearContext();
}

ThreadLocalSecurityContextHolderStrategy

/**
 * A <code>ThreadLocal</code>-based implementation of
 * {@link SecurityContextHolderStrategy}.
 *
 * @author Ben Alex
 *
 * @see java.lang.ThreadLocal
 * @see org.springframework.security.core.context.web.SecurityContextPersistenceFilter
 */
final class ThreadLocalSecurityContextHolderStrategy implements
		SecurityContextHolderStrategy {
	// ~ Static fields/initializers
	// =====================================================================================

	private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();

使用 ThreadLocal对 SecurityContext 进行存储

private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();

需要注意,如果当前线程对应的 ThreadLocal没有任何对象存储,getContext()会创建并返回一个空的 SecurityContext 对象,并且该空的 SecurityContext 对象会存入 ThreadLocal

public SecurityContext getContext() {
    SecurityContext ctx = contextHolder.get();

    if (ctx == null) {
        ctx = createEmptyContext();
        contextHolder.set(ctx);
    }

    return ctx;
}

设置当前线程对应的 ThreadLocal的存储

public void setContext(SecurityContext context) {
    Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
    contextHolder.set(context);
}

清空当前线程对应的 ThreadLocal的存储

public void clearContext() {
    contextHolder.remove();
}

创建一个空的 SecurityContext 对象

public SecurityContext createEmptyContext() {
    return new SecurityContextImpl();
}

5.5.1 SecurityContextPersistenceFilter过滤器

前面提到过,在UsernamePasswordAuthenticationFilter过滤器认证成功之后,会在认证成功的处理方法中将已认证的用户信息对象Authentication 封装进SecurityContext,并存入SecurityContextHolder。

之后,响应会通过SecurityContextPersistenceFilter过滤器,该过滤器的位置在所有过滤器的最前面,请求到来先进它,响应返回最后一个通过它,所以在该过滤器中处理已认证的用户信息对象Authentication 与Session 绑定。

认证成功的响应通过SecurityContextPersistenceFilter过滤器时,会从SecurityContextHolder 中取出封装了已认证用户信息对象Authentication 的SecurityContext,放进Session 中。当请求再次到来时,请求首先经过该过滤器,该过滤器会判断当前请求的Session 是否存有SecurityContext 对象,如果有则将该对象取出再次放入SecurityContextHolder 中,之后该请求所在的线程获得认证用户信息,后续的资源访问不需要进行身份认证;当响应再次返回时,该过滤器同样从SecurityContextHolder 取出SecurityContext 对象,放入Session 中。具体源码如下:

public class SecurityContextPersistenceFilter extends GenericFilterBean {
	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());
			}
		}

		HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
				response);
		SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

		try {
			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");
			}
		}
	}
}
  1. 请求到来时,检查当前 Session 中是否存有SecurityContext 对象,如果有,从Session中取出该对象;如果没有,创建一个空的 SecurityContext对象

    SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
    
  2. 将上述获得 SecurityContext对象放入 SecurityContextHolder中

    SecurityContextHolder.setContext(contextBeforeChainExecution);
    
  3. 进入下ー个过滤器

    chain.doFilter(holder.getRequest(), holder.getResponse());
    
  4. 响应返回时,从 SecurityContextHolder 中取出 SecurityContext

    SecurityContext contextAfterChainExecution = SecurityContextHolder
    					.getContext();
    
  5. 移除 SecurityContextHolder 中的SecurityContext 对象

    // Crucial removal of SecurityContextHolder contents - do this before anything
    // else.
    SecurityContextHolder.clearContext();
    
  6. 将取出的 SecurityContext 对象放进 Session

    repo.saveContext(contextAfterChainExecution, holder.getRequest(),
                     holder.getResponse());
    request.removeAttribute(FILTER_APPLIED);
    
    if (debug) {
        logger.debug("SecurityContextHolder now cleared, as request processing completed");
    }
    
posted @ 2021-04-18 23:38  我係死肥宅  阅读(290)  评论(0编辑  收藏  举报