JavaSpringSpring Security

Spring Security的核心类

Spring Security的核心类主要包括以下几个:

  • **SecurityContextHolder**:存放身份信息的容器
  • **Authentication**:身份信息的抽象接口
  • **AuthenticationManager**:身份认证器,认证的核心接口
  • **UserDetailsService**:一般用于从数据库中加载身份信息
  • **UserDetails**:相比Authentication,有更详细的身份信息

    SecurityContextHolderSecurityontextAuthentication

    SecurityContextHolder用于存储安全上下文(security context)的信息,即一个存储身份信息,认证信息等的容器。SecurityContextHolder默认使用 ThreadLocal策略来存储认证信息,即一种与线程绑定的策略,每个线程执行时都可以获取该线程中的 安全上下文(security context),各个线程中的安全上下文互不影响。而且如果说要在请求结束后清除安全上下文中的信息,利用该策略Spring Security也可以轻松搞定。
    因为身份信息是与线程绑定的,所以可以在程序的任何地方使用静态方法获取用户信息,一个获取当前登录用户的姓名的例子如下:

    1. Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    2. if (principal instanceof UserDetails) {
    3. String username = ((UserDetails)principal).getUsername();
    4. } else {
    5. String username = principal.toString();
    6. }

    getAuthentication()方法返回了认证信息,准确的说是一个 Authentication实例,Authentication是 Spring Security 中的一个重要接口,直接继承自 Principal类,该接口表示对用户身份信息的抽象,接口源码如下:

    1. public interface Authentication extends Principal, Serializable {
    2. //权限信息列表,默认是 GrantedAuthority接口的一些实现
    3. Collection<? extends GrantedAuthority> getAuthorities();
    4. //密码信息,用户输入的密码字符串,认证后通常会被移除,用于保证安全
    5. Object getCredentials();
    6. //细节信息,web应用中通常的接口为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值
    7. Object getDetails();
    8. //身份信息,返回UserDetails的实现类
    9. Object getPrincipal();
    10. //认证状态,默认为false,认证成功后为 true
    11. boolean isAuthenticated();
    12. //上述身份信息是否经过身份认证
    13. void setAuthenticated(boolean var1) throws IllegalArgumentException;
    14. }

    AuthenticationManagerProviderManagerAuthenticationProvider

    AuthenticationManager是身份认证器,认证的核心接口,接口源码如下:

    1. public interface AuthenticationManager {
    2. /**
    3. * Attempts to authenticate the passed {@link Authentication} object, returning a
    4. * fully populated <code>Authentication</code> object (including granted authorities)
    5. * @param authentication the authentication request object
    6. *
    7. * @return a fully authenticated object including credentials
    8. *
    9. * @throws AuthenticationException if authentication fails
    10. */
    11. Authentication authenticate(Authentication authentication)
    12. throws AuthenticationException;
    13. }

    该接口只有一个 authenticate()方法,用于身份信息的认证,如果认证成功,将会返回一个带了完整信息的Authentication,在之前提到的Authentication所有的属性都会被填充。
    在Spring Security中,AuthenticationManager默认的实现类是 ProviderManagerProviderManager并不是自己直接对请求进行验证,而是将其委派给一个 AuthenticationProvider列表。列表中的每一个 AuthenticationProvider将会被依次查询是否需要通过其进行验证,每个 provider的验证结果只有两个情况:抛出一个异常或者完全填充一个 Authentication对象的所有属性。ProviderManager中的部分源码如下:

    1. public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
    2. //维护一个AuthenticationProvider 列表
    3. private List<AuthenticationProvider> providers = Collections.emptyList();
    4. private AuthenticationManager parent;
    5. //构造器,初始化 AuthenticationProvider 列表
    6. public ProviderManager(List<AuthenticationProvider> providers) {
    7. this(providers, null);
    8. }
    9. public ProviderManager(List<AuthenticationProvider> providers,
    10. AuthenticationManager parent) {
    11. Assert.notNull(providers, "providers list cannot be null");
    12. this.providers = providers;
    13. this.parent = parent;
    14. checkState();
    15. }
    16. public Authentication authenticate(Authentication authentication)
    17. throws AuthenticationException {
    18. Class<? extends Authentication> toTest = authentication.getClass();
    19. AuthenticationException lastException = null;
    20. Authentication result = null;
    21. boolean debug = logger.isDebugEnabled();
    22. // AuthenticationProvider 列表中每个Provider依次进行认证
    23. for (AuthenticationProvider provider : getProviders()) {
    24. if (!provider.supports(toTest)) {
    25. continue;
    26. }
    27. ...
    28. try {
    29. //调用 AuthenticationProvider 的 authenticate()方法进行认证
    30. result = provider.authenticate(authentication);
    31. if (result != null) {
    32. copyDetails(authentication, result);
    33. break;
    34. }
    35. }
    36. ...
    37. catch (AuthenticationException e) {
    38. lastException = e;
    39. }
    40. }
    41. // 如果 AuthenticationProvider 列表中的Provider都认证失败,且之前有构造一个 AuthenticationManager 实现类,那么利用AuthenticationManager 实现类 继续认证
    42. if (result == null && parent != null) {
    43. // Allow the parent to try.
    44. try {
    45. result = parent.authenticate(authentication);
    46. }
    47. ...
    48. catch (AuthenticationException e) {
    49. lastException = e;
    50. }
    51. }
    52. //认证成功
    53. if (result != null) {
    54. if (eraseCredentialsAfterAuthentication
    55. && (result instanceof CredentialsContainer)) {
    56. // Authentication is complete. Remove credentials and other secret data
    57. // from authentication
    58. //成功认证后删除验证信息
    59. ((CredentialsContainer) result).eraseCredentials();
    60. }
    61. //发布登录成功事件
    62. eventPublisher.publishAuthenticationSuccess(result);
    63. return result;
    64. }
    65. // 没有认证成功,抛出一个异常
    66. if (lastException == null) {
    67. lastException = new ProviderNotFoundException(messages.getMessage(
    68. "ProviderManager.providerNotFound",
    69. new Object[] { toTest.getName() },
    70. "No AuthenticationProvider found for {0}"));
    71. }
    72. prepareException(lastException, authentication);
    73. throw lastException;
    74. }

    ProviderManager中的 authenticationManager列表依次去尝试认证,认证成功即返回,认证失败返回null,如果所有的 Provider都认证失败, ProviderManager将会抛出一个 ProviderNotFoundException异常。
    事实上,AuthenticationProvider是一个接口,接口定义如下:

    1. public interface AuthenticationProvider {
    2. //认证方法
    3. Authentication authenticate(Authentication authentication)
    4. throws AuthenticationException;
    5. //该Provider是否支持对应的Authentication
    6. boolean supports(Class<?> authentication);
    7. }

    在 ProviderManager的 Javadoc曾提到,

    If more than one AuthenticationProvider supports the passed Authentication object, the first one able to successfully authenticate the Authentication object determines the result, overriding any possible AuthenticationException thrown by earlier supporting AuthenticationProvider s. On successful authentication, no subsequent AuthenticationProvider s will be tried. If authentication was not successful by any supporting AuthenticationProvider the last thrown AuthenticationException will be rethrown

大致意思是:

如果有多个 AuthenticationProvider 都支持同一个Authentication 对象,那么第一个 能够成功验证**Authentication**的 Provder 将填充其属性并返回结果,从而覆盖早期支持的 AuthenticationProvider抛出的任何可能的 AuthenticationException。一旦成功验证后,将不会尝试后续的 AuthenticationProvider。如果所有的 AuthenticationProvider都没有成功验证 Authentication,那么将抛出最后一个Provider抛出的AuthenticationException。(AuthenticationProvider可以在Spring Security配置类中配置)

PS
当然有时候有多个不同的 AuthenticationProvider,它们分别支持不同的 Authentication对象,那么当一个具体的 AuthenticationProvier传进入 ProviderManager的内部时,就会在 AuthenticationProvider列表中挑选其对应支持的provider对相应的 Authentication对象进行验证。
不同的登录方式认证逻辑是不一样的,即 AuthenticationProvider会不一样,如果使用用户名和密码登录,那么在Spring Security 提供了一个 AuthenticationProvider的简单实现 DaoAuthenticationProvider,这也是框架最早的 provider,它使用了一个 UserDetailsService来查询用户名、密码和 GrantedAuthority,一般要实现UserDetailsService接口,并在Spring Security配置类中将其配置进去,这样也促使使用DaoAuthenticationProvider进行认证,然后该接口返回一个UserDetails,它包含了更加详细的身份信息,比如从数据库拿取的密码和权限列表,AuthenticationProvider 的认证核心就是加载对应的 UserDetails来检查用户输入的密码是否与其匹配,即UserDetails和Authentication两者的密码(关于 UserDetailsServiceUserDetails的介绍在下面小节介绍。)。而如果是使用第三方登录,比如QQ登录,那么就需要设置对应的 **AuthenticationProvider**,这里就不细说了。

认证成功后清除验证信息

在上面ProviderManager的源码中还发现一点,在认证成功后清除验证信息,如下:

  1. if (eraseCredentialsAfterAuthentication
  2. && (result instanceof CredentialsContainer)) {
  3. // Authentication is complete. Remove credentials and other secret data
  4. // from authentication
  5. //成功认证后删除验证信息
  6. ((CredentialsContainer) result).eraseCredentials();
  7. }

从 Spring Security 3.1之后,在请求认证成功后 ProviderManager将会删除 Authentication中的认证信息,准确的说,一般删除的是 密码信息,这可以保证密码的安全。跟了一下源码,实际上执行删除操作的步骤如下:

  1. public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
  2. public void eraseCredentials() {
  3. super.eraseCredentials();
  4. //使密码为null
  5. this.credentials = null;
  6. }
  7. }
  8. public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {
  9. ...
  10. public void eraseCredentials() {
  11. //擦除密码
  12. this.eraseSecret(this.getCredentials());
  13. this.eraseSecret(this.getPrincipal());
  14. this.eraseSecret(this.details);
  15. }
  16. private void eraseSecret(Object secret) {
  17. if (secret instanceof CredentialsContainer) {
  18. ((CredentialsContainer)secret).eraseCredentials();
  19. }
  20. }
  21. }

从源码就可以看出实际上就是擦除密码操作。

UserDetailsService 和 UserDetails

UserDetailsService简单说就是加载对应的UserDetails的接口(一般从数据库),而UserDetails包含了更详细的用户信息,定义如下:

  1. public interface UserDetails extends Serializable {
  2. Collection<? extends GrantedAuthority> getAuthorities();
  3. String getPassword();
  4. String getUsername();
  5. boolean isAccountNonExpired();
  6. boolean isAccountNonLocked();
  7. boolean isCredentialsNonExpired();
  8. boolean isEnabled();
  9. }

UserDetails 接口与 Authentication接口相似,它们都有 username、authorities。它们的区别如下:

  • AuthenticationgetCredentials()UserDetails 中的 getPassword() 不一样,前者是用户提交的密码凭证,后者是用户正确的密码,(一般是从数据库中载入的密码),AuthenticationProvider就会对两者进行对比。
  • Authentication 中的 getAuthorities() 实际上是由 UserDetailsgetAuthorities()传递形成的。
  • Authentication 中的 getUserDetails() 中的 UserDetails 用户详细信息是经过 AuthenticationProvider认证之后填充的。

    认证过程样本示例

    下面来看一个官方文档提供的例子,代码如下:

    1. public class SpringSecuriryTestDemo {
    2. private static AuthenticationManager am = new SampleAuthenticationManager();
    3. public static void main(String[] args) throws IOException {
    4. BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
    5. while (true) {
    6. System.out.println("Please enter your username:");
    7. String name = in.readLine();
    8. System.out.println("Please enter your password:");
    9. String password = in.readLine();
    10. try {
    11. Authentication request = new UsernamePasswordAuthenticationToken(name, password);
    12. Authentication result = am.authenticate(request);
    13. SecurityContextHolder.getContext().setAuthentication(request);
    14. break;
    15. } catch (AuthenticationException e) {
    16. System.out.println("Authentication failed: " + e.getMessage());
    17. }
    18. }
    19. System.out.println("Successfully authenticated. Security context contains: " + SecurityContextHolder.getContext().getAuthentication());
    20. }
    21. static class SampleAuthenticationManager implements AuthenticationManager {
    22. static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();
    23. static {
    24. AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
    25. }
    26. @Override
    27. public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    28. if (authentication.getName().equals(authentication.getCredentials())) {
    29. return new UsernamePasswordAuthenticationToken(authentication.getName(), authentication.getCredentials(), AUTHORITIES);
    30. }
    31. throw new BadCredentialsException("Bad Credentials");
    32. }
    33. }
    34. }

    测试如下:

    1. Please enter your username:
    2. pjmike
    3. Please enter your password:
    4. 123
    5. Authentication failed: Bad Credentials
    6. Please enter your username:
    7. pjmike
    8. Please enter your password:
    9. pjmike
    10. Successfully authenticated.
    11. Security context contains: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d0230:
    12. Principal: pjmike;
    13. Credentials: [PROTECTED];
    14. Authenticated: true; Details: null;
    15. Granted Authorities: ROLE_USER

    上面的例子很简单,不是源码,只是为了演示认证过程编写的Demo,而且也缺少过滤器链,但是麻雀虽小,五脏俱全,基本包括了Spring Security的核心组件,表达了Spring Security 认证的基本思想。解读一下:

  • 用户名和密码被封装到 UsernamePasswordAuthentication的实例中(该类是 Authentication接口的实现)

  • Authentication传递给 AuthenticationManager进行身份验证
  • 认证成功后,AuthenticationManager会返回一个完全填充的 Authentication实例,该实例包含权限信息,身份信息,细节信息,但是密码通常会被移除
  • 通过调用 SecurityContextHolder.getContext().setAuthentication(…)传入上面返回的填充了信息的 Authentication对象

通过上面一个简单示例,大致明白了Spring Security的基本思想,但是要真正理清楚Spring Security的认证流程这还不够,需要深入源码去探究。

Spring Security 的认证过程及相关过滤器

Spring Security 的核心之一就是它的过滤器链,就从它的过滤器链入手,下图是Spring Security 过滤器链的一个执行过程,本文将依照该过程来逐步的剖析其认证过程
浅析 Spring Security 核心组件 - 图1

核心过滤器链简介

Spring Security 中的过滤器有很多,一般正常的项目中都有十几个过滤器,有时候还包含自定义的过滤器,当然不可能对每一个过滤器都进行分析,需要抓住重点,找比较关键的几个过滤器,它们在认证过程中扮演着重要角色,下面列举几个核心的过滤器:

  • **SecurityContextPersistenceFilter**:整个Spring Security 过滤器链的开端,它有两个作用:一是当请求到来时,检查Session中是否存在SecurityContext,如果不存在,就创建一个新的SecurityContext。二是请求结束时将SecurityContext放入 Session中,并清空 SecurityContextHolder
  • **UsernamePasswordAuthenticationFilter**:继承自抽象类 AbstractAuthenticationProcessingFilter,当进行表单登录时,该Filter将用户名和密码封装成一个 UsernamePasswordAuthentication进行验证。
  • **AnonymousAuthenticationFilter**:匿名身份过滤器,当前面的Filter认证后依然没有用户信息时,该Filter会生成一个匿名身份——AnonymousAuthenticationToken。一般的作用是用于匿名登录。
  • **ExceptionTranslationFilter**:异常转换过滤器,用于处理 FilterSecurityInterceptor抛出的异常。
  • **FilterSecurityInterceptor**:过滤器链最后的关卡,从 SecurityContextHolder中获取 Authentication,比对用户拥有的权限和所访问资源需要的权限。

    表单登录认证过程

    当访问一个受保护的资源时,如果之前没有进行登录认证,那么系统将返回一个登录表单或者一个响应结果提示要先进行登录操作。这里的分析过程只针对表单登录,所以先在表单中填写用户名和密码进行登录验证。
    上面已经简述了一堆核心过滤器,这里先从 SecurityContextPersistenceFilter这个过滤器的开端开始分析整个表单登录的认证过程。

    SecurityContextPersistenceFilter

    当填写表单完毕后,点击登录按钮,请求先经过 SecurityContextPersistenceFilter过滤器,在前面就曾提到,该Filter有两个作用,其中之一就是在请求到来时,创建 SecurityContext安全上下文,来看看它内部是如何做的,部分源码如下:

    1. public class SecurityContextPersistenceFilter extends GenericFilterBean {
    2. static final String FILTER_APPLIED = "__spring_security_scpf_applied";
    3. //安全上下文存储的仓库
    4. private SecurityContextRepository repo;
    5. private boolean forceEagerSessionCreation = false;
    6. public SecurityContextPersistenceFilter() {
    7. //使用HttpSession来存储 SecurityContext
    8. this(new HttpSessionSecurityContextRepository());
    9. }
    10. public SecurityContextPersistenceFilter(SecurityContextRepository repo) {
    11. this.repo = repo;
    12. }
    13. public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
    14. throws IOException, ServletException {
    15. HttpServletRequest request = (HttpServletRequest) req;
    16. HttpServletResponse response = (HttpServletResponse) res;
    17. // 如果是第一次请求,request中肯定没有 FILTER_APPLIED属性
    18. if (request.getAttribute(FILTER_APPLIED) != null) {
    19. // 确保每个请求只应用一次过滤器
    20. chain.doFilter(request, response);
    21. return;
    22. }
    23. final boolean debug = logger.isDebugEnabled();
    24. // 在request 设置 FILTER_APPLIED 属性为 true,这样同一个请求再次访问时,就直接进入后续Filter的操作
    25. request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
    26. if (forceEagerSessionCreation) {
    27. HttpSession session = request.getSession();
    28. if (debug && session.isNew()) {
    29. logger.debug("Eagerly created session: " + session.getId());
    30. }
    31. }
    32. // 封装 requset 和 response
    33. HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
    34. response);
    35. // 从存储安全上下文的仓库中载入 SecurityContext 安全上下文,其内部是从 Session中获取上下文信息
    36. SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
    37. try {
    38. //安全上下文信息设置到 SecurityContextHolder 中,以便在同一个线程中,后续访问 SecurityContextHolder 能获取到 SecuritContext
    39. SecurityContextHolder.setContext(contextBeforeChainExecution);
    40. //进入下一个过滤器操作
    41. chain.doFilter(holder.getRequest(), holder.getResponse());
    42. }
    43. finally {
    44. // 请求结束后,清空安全上下文信息
    45. SecurityContext contextAfterChainExecution = SecurityContextHolder
    46. .getContext();
    47. // Crucial removal of SecurityContextHolder contents - do this before anything
    48. // else.
    49. SecurityContextHolder.clearContext();
    50. //将安全上下文信息存储到 Session中,相当于登录态的维护
    51. repo.saveContext(contextAfterChainExecution, holder.getRequest(),
    52. holder.getResponse());
    53. request.removeAttribute(FILTER_APPLIED);
    54. if (debug) {
    55. logger.debug("SecurityContextHolder now cleared, as request processing completed");
    56. }
    57. }
    58. }
    59. public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {
    60. this.forceEagerSessionCreation = forceEagerSessionCreation;
    61. }
    62. }

    请求到来时,利用HttpSessionSecurityContextRepository读取安全上下文。这里是第一次请求,读取的安全上下文中是没有 Authentication身份信息的,将安全上下文设置到 SecurityContextHolder之后,进入下一个过滤器。
    请求结束时,同样利用HttpSessionSecurityContextRepository该存储安全上下文的仓库将认证后的SecurityContext放入 Session中,这也是登录态维护的关键,具体的操作这里就不细说了。

    UsernamePasswordAuthenticationFilter

    经过 SecurityContextPersistenceFilter过滤器后来到 UsernamePasswordAuthenticationFilter过滤器,因为假定的是第一次请求,所以 SecurityContext并没有包含认证过的 Authentication从此过滤器开始的操作对于表单登录来说是非常关键的,包含了表单登录的核心认证步骤,下面画了一张在此过滤器中的认证过程图:
    filter-process
    UsernamePasswordAuthenticationFilter的父类是 AbstractAuthenticationProcessingFilter,首先进入父类的 foFilter方法,部分源码如下:

    1. public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
    2. implements ApplicationEventPublisherAware, MessageSourceAware {
    3. ...
    4. public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
    5. throws IOException, ServletException {
    6. HttpServletRequest request = (HttpServletRequest) req;
    7. HttpServletResponse response = (HttpServletResponse) res;
    8. ...
    9. Authentication authResult;
    10. try {
    11. //调用子类 UsernamePasswordAuthenticationFilter 的 attemptAuthentication 方法
    12. authResult = attemptAuthentication(request, response);
    13. if (authResult == null) {
    14. // return immediately as subclass has indicated that it hasn't completed
    15. // authentication
    16. //子类未完成认证,立刻返回
    17. return;
    18. }
    19. sessionStrategy.onAuthentication(authResult, request, response);
    20. }
    21. catch (InternalAuthenticationServiceException failed) {
    22. logger.error(
    23. "An internal error occurred while trying to authenticate the user.",
    24. failed);
    25. unsuccessfulAuthentication(request, response, failed);
    26. return;
    27. }
    28. catch (AuthenticationException failed) {
    29. //认证失败
    30. unsuccessfulAuthentication(request, response, failed);
    31. return;
    32. }
    33. // 认证成功
    34. if (continueChainBeforeSuccessfulAuthentication) {
    35. //继续调用下一个 Filter
    36. chain.doFilter(request, response);
    37. }
    38. //将成功认证后的Authentication写入 SecurityContext中
    39. successfulAuthentication(request, response, chain, authResult);
    40. }
    41. }

    doFilter方法中一个核心就是调用子类 UsernamePasswordAuthenticationFilterattemptAuthentication方法,该方法进入真正的认证过程,并返回认证后的 Authentication,该方法的源码如下:

    1. public Authentication attemptAuthentication(HttpServletRequest request,
    2. HttpServletResponse response) throws AuthenticationException {
    3. //必须是POST请求
    4. if (postOnly && !request.getMethod().equals("POST")) {
    5. throw new AuthenticationServiceException(
    6. "Authentication method not supported: " + request.getMethod());
    7. }
    8. //获取表单中的用户名和密码
    9. String username = obtainUsername(request);
    10. String password = obtainPassword(request);
    11. if (username == null) {
    12. username = "";
    13. }
    14. if (password == null) {
    15. password = "";
    16. }
    17. username = username.trim();
    18. //将用户名和密码封装成一个 UsernamePasswordAuthenticationToken
    19. UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
    20. username, password);
    21. // Allow subclasses to set the "details" property
    22. setDetails(request, authRequest);
    23. //核心部分,交给内部的AuthenticationManager去认证,并返回认证后的 Authentication
    24. return this.getAuthenticationManager().authenticate(authRequest);
    25. }

    该方法中有一个关键点就是 his.getAuthenticationManager().authenticate(authRequest),调用内部的 AuthenticationManager去认证,在之前就介绍过**AuthenticationManager**,它是身份认证的核心接口,它的实现类是 **ProviderManager**,而 **ProviderManager**又将请求委托给一个 **AuthenticationProvider**列表,列表中的每一个 **AuthenticationProvider**将会被依次查询是否需要通过其进行验证,每个 **provider**的验证结果只有两个情况:抛出一个异常或者完全填充一个 Authentication对象的所有属性
    下面来分析一个关键的 AuthenticationProvider,它就是 DaoAuthenticationProvider,它是框架最早的provider,也是最最常用的 provider。大多数情况下会依靠它来进行身份认证,它的父类是 AbstractUserDetailsAuthenticationProvider,认证过程首先会调用父类的 authenticate方法,核心源码如下:

    1. public Authentication authenticate(Authentication authentication)
    2. throws AuthenticationException {
    3. Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
    4. messages.getMessage(
    5. "AbstractUserDetailsAuthenticationProvider.onlySupports",
    6. "Only UsernamePasswordAuthenticationToken is supported"));
    7. // Determine username
    8. String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
    9. : authentication.getName();
    10. boolean cacheWasUsed = true;
    11. UserDetails user = this.userCache.getUserFromCache(username);
    12. if (user == null) {
    13. cacheWasUsed = false;
    14. try {
    15. //1 调用子类 DaoAuthenticationProvider 的 retrieveUser()方法获取 UserDetails
    16. user = retrieveUser(username,
    17. (UsernamePasswordAuthenticationToken) authentication);
    18. }
    19. //没拿到UserDetails会抛出异常信息
    20. catch (UsernameNotFoundException notFound) {
    21. logger.debug("User '" + username + "' not found");
    22. if (hideUserNotFoundExceptions) {
    23. throw new BadCredentialsException(messages.getMessage(
    24. "AbstractUserDetailsAuthenticationProvider.badCredentials",
    25. "Bad credentials"));
    26. }
    27. else {
    28. throw notFound;
    29. }
    30. }
    31. Assert.notNull(user,
    32. "retrieveUser returned null - a violation of the interface contract");
    33. }
    34. try {
    35. //2 对UserDetails的一些属性进行预检查,即判断用户是否锁定,是否可用以及用户是否过期
    36. preAuthenticationChecks.check(user);
    37. //3 对UserDetails附加的检查,对传入的Authentication与从数据库中获取的UserDetails进行密码匹配
    38. additionalAuthenticationChecks(user,
    39. (UsernamePasswordAuthenticationToken) authentication);
    40. }
    41. catch (AuthenticationException exception) {
    42. if (cacheWasUsed) {
    43. // There was a problem, so try again after checking
    44. // we're using latest data (i.e. not from the cache)
    45. cacheWasUsed = false;
    46. user = retrieveUser(username,
    47. (UsernamePasswordAuthenticationToken) authentication);
    48. preAuthenticationChecks.check(user);
    49. additionalAuthenticationChecks(user,
    50. (UsernamePasswordAuthenticationToken) authentication);
    51. }
    52. else {
    53. throw exception;
    54. }
    55. }
    56. //4 对UserDetails进行后检查,检查UserDetails的密码是否过期
    57. postAuthenticationChecks.check(user);
    58. if (!cacheWasUsed) {
    59. this.userCache.putUserInCache(user);
    60. }
    61. Object principalToReturn = user;
    62. if (forcePrincipalAsString) {
    63. principalToReturn = user.getUsername();
    64. }
    65. //5 上面所有检查成功后,用传入的用户信息和获取的UserDetails生成一个成功验证的Authentication
    66. return createSuccessAuthentication(principalToReturn, authentication, user);
    67. }

    从上面一大串源码中,提取几个关键的方法:

  • **retrieveUser(…)**:调用子类 DaoAuthenticationProviderretrieveUser()方法获取 UserDetails

  • **preAuthenticationChecks.check(user)**:对从上面获取的UserDetails进行预检查,即判断用户是否锁定,是否可用以及用户是否过期
  • **additionalAuthenticationChecks(user,authentication)**:对UserDetails附加的检查,对传入的Authentication与获取的UserDetails进行密码匹配
  • **postAuthenticationChecks.check(user)**:对UserDetails进行后检查,即检查UserDetails的密码是否过期
  • **createSuccessAuthentication(principalToReturn, authentication, user)**:上面所有检查成功后,利用传入的Authentication和获取的UserDetails生成一个成功验证的Authentication

**retrieveUser(…)**方法
接下来详细说说 retrieveUser(...)方法, DaoAuthenticationProviderretrieveUser() 源码如下:

  1. protected final UserDetails retrieveUser(String username,
  2. UsernamePasswordAuthenticationToken authentication)
  3. throws AuthenticationException {
  4. prepareTimingAttackProtection();
  5. try {
  6. //经过UserDetailsService 获取 UserDetails
  7. UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
  8. if (loadedUser == null) {
  9. throw new InternalAuthenticationServiceException(
  10. "UserDetailsService returned null, which is an interface contract violation");
  11. }
  12. return loadedUser;
  13. }
  14. catch (UsernameNotFoundException ex) {
  15. mitigateAgainstTimingAttack(authentication);
  16. throw ex;
  17. }
  18. catch (InternalAuthenticationServiceException ex) {
  19. throw ex;
  20. }
  21. catch (Exception ex) {
  22. throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
  23. }
  24. }

该方法最核心的部分就是调用内部的UserDetailsServices 加载 UserDetails,UserDetailsServices本质上就是加载UserDetails的接口,UserDetails包含了比Authentication更加详细的用户信息。UserDetailsService常见的实现类有JdbcDaoImpl,**InMemoryUserDetailsManager**,前者从数据库加载用户,后者从内存中加载用户。也可以自己实现**UserDetailsServices**接口,比如是如果是基于数据库进行身份认证,那么可以手动实现该接口,而不用JdbcDaoImpl。
**additionalAuthenticationChecks()**
UserDetails的预检查和后检查比较简单,这里就不细说了,下面来看一下密码匹配校验,代码如下:

  1. protected void additionalAuthenticationChecks(UserDetails userDetails,
  2. UsernamePasswordAuthenticationToken authentication)
  3. throws AuthenticationException {
  4. if (authentication.getCredentials() == null) {
  5. logger.debug("Authentication failed: no credentials provided");
  6. throw new BadCredentialsException(messages.getMessage(
  7. "AbstractUserDetailsAuthenticationProvider.badCredentials",
  8. "Bad credentials"));
  9. }
  10. String presentedPassword = authentication.getCredentials().toString();
  11. //利用 PasswordEncoder编码器校验密码
  12. if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
  13. logger.debug("Authentication failed: password does not match stored value");
  14. throw new BadCredentialsException(messages.getMessage(
  15. "AbstractUserDetailsAuthenticationProvider.badCredentials",
  16. "Bad credentials"));
  17. }
  18. }

这个方法实际上是调用DaoAuthenticationProvideradditionalAuthenticationChecks方法,内部调用加密解密器进行密码匹配,如果匹配失败,则抛出一个 BadCredentialsException异常
最后通过createSuccessAuthentication(..)方法生成一个成功认证的 Authentication,简单说就是组合获取的UserDetails和传入的Authentication,得到一个完全填充的Authentication
Authentication最终一步一步向上返回,到AbstractAuthenticationProcessingFilter过滤器中,将其设置到 SecurityContextHolder

AnonymousAuthenticationFilter

匿名认证过滤器,它主要是针对匿名登录,如果前面的Filter,比如UsernamePasswordAuthenticationFilter执行完毕后,SecurityContext依旧没有用户信息,那么AnonymousAuthenticationFilter才会起作用,生成一个匿名身份信息——AnonymousAuthenticationToken

ExceptionTranslationFilter

ExceptionTranslationFilter 简单的说就是处理 FilterSecurityInterceptor 抛出的异常,其内部 doFilter方法源码如下:

  1. public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
  2. throws IOException, ServletException {
  3. HttpServletRequest request = (HttpServletRequest) req;
  4. HttpServletResponse response = (HttpServletResponse) res;
  5. try {
  6. //直接进入下一个Filter
  7. chain.doFilter(request, response);
  8. logger.debug("Chain processed normally");
  9. }
  10. catch (IOException ex) {
  11. throw ex;
  12. }
  13. //真正的作用在这里,处理抛出的异常
  14. catch (Exception ex) {
  15. // Try to extract a SpringSecurityException from the stacktrace
  16. Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
  17. RuntimeException ase = (AuthenticationException) throwableAnalyzer
  18. .getFirstThrowableOfType(AuthenticationException.class, causeChain);
  19. //这里会处理 FilterSecurityInterceptor 抛出的AccessDeniedException
  20. if (ase == null) {
  21. ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
  22. AccessDeniedException.class, causeChain);
  23. }
  24. if (ase != null) {
  25. if (response.isCommitted()) {
  26. throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
  27. }
  28. handleSpringSecurityException(request, response, chain, ase);
  29. }
  30. else {
  31. // Rethrow ServletExceptions and RuntimeExceptions as-is
  32. if (ex instanceof ServletException) {
  33. throw (ServletException) ex;
  34. }
  35. else if (ex instanceof RuntimeException) {
  36. throw (RuntimeException) ex;
  37. }
  38. // Wrap other Exceptions. This shouldn't actually happen
  39. // as we've already covered all the possibilities for doFilter
  40. throw new RuntimeException(ex);
  41. }
  42. }
  43. }

FilterSecurityInterceptor

FilterSecurityInterceptor 过滤器是最后的关卡,之前的请求最终会来到这里,它的大致工作流程就是

  • 封装请求信息
  • 从系统中读取配置信息,即资源所需的权限信息
  • SecurityContextHolder中获取之前认证过的 Authentication对象,即表示当前用户所拥有的权限
  • 然后根据上面获取到的三种信息,传入一个权限校验器中,对于当前请求来说,比对用户拥有的权限和资源所需的权限。若比对成功,则进入真正系统的请求处理逻辑,反之,会抛出相应的异常

下面画一张简易的流程图来阐述 FilterSecurityInterceptor的执行过程,如下:
filter_processs
根据上图内容,再来看看 FilterSecurityInterceptor的源码:

  1. public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
  2. Filter {
  3. ...
  4. public void doFilter(ServletRequest request, ServletResponse response,
  5. FilterChain chain) throws IOException, ServletException {
  6. // 封装request、response请求
  7. FilterInvocation fi = new FilterInvocation(request, response, chain);
  8. //调用核心方法
  9. invoke(fi);
  10. }
  11. ...
  12. public void invoke(FilterInvocation fi) throws IOException, ServletException {
  13. if ((fi.getRequest() != null)
  14. && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
  15. && observeOncePerRequest) {
  16. // filter already applied to this request and user wants us to observe
  17. // once-per-request handling, so don't re-do security checking
  18. fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
  19. }
  20. else {
  21. // 判断当前请求之前是否经历过该过滤器
  22. if (fi.getRequest() != null && observeOncePerRequest) {
  23. // 如果当前请求已经经历过这个安全过滤器判断,那么不再执行后续逻辑,直接往下走,调用请求的处理方法
  24. fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
  25. }
  26. //调用父类的方法,执行授权判断逻辑
  27. InterceptorStatusToken token = super.beforeInvocation(fi);
  28. try {
  29. fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
  30. }
  31. finally {
  32. super.finallyInvocation(token);
  33. }
  34. super.afterInvocation(token, null);
  35. }
  36. }
  37. }

源码中已经对请求进行了封装,然后进入核心部分, 调用父类的授权判断方法——beforeInvocation(FilterInvocation),源码如下:

  1. protected InterceptorStatusToken beforeInvocation(Object object) {
  2. Assert.notNull(object, "Object was null");
  3. final boolean debug = logger.isDebugEnabled();
  4. if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
  5. throw new IllegalArgumentException(
  6. "Security invocation attempted for object "
  7. + object.getClass().getName()
  8. + " but AbstractSecurityInterceptor only configured to support secure objects of type: "
  9. + getSecureObjectClass());
  10. }
  11. //读取Spring Security的配置信息,将其封装成 ConfigAttribute
  12. Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
  13. .getAttributes(object);
  14. if (attributes == null || attributes.isEmpty()) {
  15. if (rejectPublicInvocations) {
  16. throw new IllegalArgumentException(
  17. "Secure object invocation "
  18. + object
  19. + " was denied as public invocations are not allowed via this interceptor. "
  20. + "This indicates a configuration error because the "
  21. + "rejectPublicInvocations property is set to 'true'");
  22. }
  23. ...
  24. return null; // no further work post-invocation
  25. }
  26. ...
  27. if (SecurityContextHolder.getContext().getAuthentication() == null) {
  28. credentialsNotFound(messages.getMessage(
  29. "AbstractSecurityInterceptor.authenticationNotFound",
  30. "An Authentication object was not found in the SecurityContext"),
  31. object, attributes);
  32. }
  33. //从SecurityContextHolder中获取Authentication
  34. Authentication authenticated = authenticateIfRequired();
  35. // 启动授权匹配
  36. try {
  37. this.accessDecisionManager.decide(authenticated, object, attributes);
  38. }
  39. catch (AccessDeniedException accessDeniedException) {
  40. publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
  41. accessDeniedException));
  42. throw accessDeniedException;
  43. }
  44. ...
  45. }

beforeInvocation的源码比较多,这里只保留了相对核心的部分,从源码就可以看出,拿到配置信息和用户信息后,连同请求信息一同传入AccessDecisionManagerdecide(Authentication authentication, Object object,Collection<ConfigAttribute> configAttributes)方法。该方法是最终执行授权校验逻辑的地方。
AccessDecisionManager 本身是一个接口,它的 实现类是 AbstractAccessDecisionManager,而 AbstractAccessDecisionManager也是一个抽象类,它的实现类有三个,常用的是 AffirmativeBased,最终的授权校验逻辑是 AffirmativeBased 实现的,部分源码如下:

  1. public void decide(Authentication authentication, Object object,
  2. Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
  3. int deny = 0;
  4. //投票器执行投票
  5. for (AccessDecisionVoter voter : getDecisionVoters()) {
  6. int result = voter.vote(authentication, object, configAttributes);
  7. ...
  8. switch (result) {
  9. case AccessDecisionVoter.ACCESS_GRANTED:
  10. return;
  11. case AccessDecisionVoter.ACCESS_DENIED:
  12. deny++;
  13. break;
  14. default:
  15. break;
  16. }
  17. }
  18. if (deny > 0) {
  19. throw new AccessDeniedException(messages.getMessage(
  20. "AbstractAccessDecisionManager.accessDenied", "Access is denied"));
  21. }
  22. ...
  23. }

该方法的逻辑比较简单,就是执行AccessDecisionVoter的校验逻辑,如果校验失败就抛出AccessDeniedException异常。对于AccessDecisionVotervote投票逻辑这里就不细说了,在 Spring Security 3.0以后,一般默认使用 AccessDecisionVoter接口的实现类**WebExpressionVoter**来完成最终的校验过程。