spring-securyty-02-认证流程


spring-securyty-02-认证流程

本章来粗略的讲解一下,当一个项目配置了spring-securyty默认认证的流程。

1. 简单的认证流程

  1. 用户名和密码被过滤器获取到,封装成Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。

  2. AuthenticationManager 身份管理器负责验证这个Authentication

  3. 认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例。

  4. SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。

    2. 核心组件

    spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。本节不涉及源码流程,只是先熟悉几个核心类,再仔细了解是如何把这几个核心类串联起来的。

    2.1 SecurityContextHolder

SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保存在SecurityContextHolder中。
SecurityContextHolder默认使用ThreadLocal 策略来存储认证信息。
看到ThreadLocal 也就意味着,这是一种与线程绑定的策略。Spring Security在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。
SecurityContextHolder解决的是用户认证信息存储的问题。

public static SecurityContext getContext() {
		return strategy.getContext(); //strategy为具体的SecurityContext方式,
                                    //initialize方法中初始化
	}

	/**
	 * Primarily for troubleshooting purposes, this method shows how many times the class
	 * has re-initialized its <code>SecurityContextHolderStrategy</code>.
	 *
	 * @return the count (should be one unless you've called
	 * {@link #setStrategyName(String)} to switch to an alternate strategy.
	 */
	public static int getInitializeCount() {
		return initializeCount;
	}

	private static void initialize() {
		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++;
  • 可以看到默认的存储用户信息的实现 ThreadLocalSecurityContextHolderStrategy
    final class ThreadLocalSecurityContextHolderStrategy implements
    		SecurityContextHolderStrategy {
    	// ~ Static fields/initializers
    	// =====================================================================================
    
    	private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
    
    	// ~ Methods
    	// ========================================================================================================
    
    	public void clearContext() {
    		contextHolder.remove();
    	}
    
    	public SecurityContext getContext() {
    		SecurityContext ctx = contextHolder.get();
    
    		if (ctx == null) {
    			ctx = createEmptyContext();
    			contextHolder.set(ctx);
    		}
    
    		return ctx;
    	}
    
    	public void setContext(SecurityContext context) {
    		Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
    		contextHolder.set(context);
    	}
    
    	public SecurityContext createEmptyContext() {
    		return new SecurityContextImpl();
    	}
    }

    2.2 Authentication对象

public interface Authentication extends Principal, Serializable {

	Collection<? extends GrantedAuthority> getAuthorities();


	Object getCredentials();


	Object getDetails();


	boolean isAuthenticated();

	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
Authentication 继承于Principal  
public interface Principal {
    boolean equals(Object var1);

    String toString();

    int hashCode();

    String getName();

    default boolean implies(Subject var1) {
        return var1 == null ? false : var1.getPrincipals().contains(this);
    }
}

<1> Authentication是spring security包中的接口,直接继承自Principal类,而Principal是位于java.security包中的。可以见得,Authentication在spring security中是最高级别的身份/认证的抽象。
<2> 由这个顶级接口,我们可以得到用户拥有的权限信息列表,密码,用户细节信息,用户身份信息,认证信息。
接口详细解读如下:

  • getAuthorities(),权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系列字符串。
  • getCredentials(),Credentials 中文意为可信的,密码信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
  • getDetails(),细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值。
  • getPrincipal(),最重要的身份信息,表示用户是谁的接口。大部分情况下返回的是UserDetails接口的实现类,也是框架中的常用接口之一。UserDetails接口将会在下面的小节重点介绍。

2.3 AuthenticationManager

初次接触可能会被AuthenticationManager,ProviderManager ,AuthenticationProvider …这么多相似的Spring认证类搞得晕头转向,但只要稍微梳理一下就可以理解清楚它们的联系和设计者的用意。

  • AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名+密码登录,同时允许用户使用邮箱+密码,手机号码+密码登录,甚至,可能允许用户使用指纹登录(还有这样的操作?没想到吧)。
  • 所以说AuthenticationManager一般不直接认证,AuthenticationManager接口的常用实现类ProviderManager 内部会维护一个List列表,存放多种认证方式,实际上这是委托者模式的应用(Delegate)。
  • 也就是说,核心的认证入口始终只有一个:AuthenticationManager,不同的认证方式:用户名+密码(UsernamePasswordAuthenticationToken),邮箱+密码,手机号码+密码登录则对应了三个AuthenticationProvider
  • 在默认策略下,只需要通过一个AuthenticationProvider的认证,即可被认为是登录成功。

注意这里用了委托模式,可以学一下哦。
ProviderManager中比较重要的方法如下。

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
		InitializingBean {

    // 维护一个AuthenticationProvider列表
    private List<AuthenticationProvider> providers = Collections.emptyList();

    public Authentication authenticate(Authentication authentication)
          throws AuthenticationException {
       Class<? extends Authentication> toTest = authentication.getClass();
       AuthenticationException lastException = null;
       Authentication result = null;

       // 依次认证
       for (AuthenticationProvider provider : getProviders()) {
          if (!provider.supports(toTest)) {//这个方法用于验证是否支持验证方式,如果不支持就去看下一个。
             continue;
          }
          try {
             result = provider.authenticate(authentication);

             if (result != null) {
                copyDetails(authentication, result);
                break;
             }
          }
          ...
          catch (AuthenticationException e) {
             lastException = e;
          }
       }
       // 如果有Authentication信息,则直接返回
       if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
              	 //移除密码
				((CredentialsContainer) result).eraseCredentials();
			}
             //发布验证成功,登录成功事件
			eventPublisher.publishAuthenticationSuccess(result);
			return result;
	   }
	   ...
       //执行到此,说明没有认证成功,包装异常信息
       if (lastException == null) {
          lastException = new ProviderNotFoundException(messages.getMessage(
                "ProviderManager.providerNotFound",
                new Object[] { toTest.getName() },
                "No AuthenticationProvider found for {0}"));
       }
       prepareException(lastException, authentication);
       throw lastException;
    }
}

到这里,如果不纠结于AuthenticationProvider的实现细节以及安全相关的过滤器,认证相关的核心类其实都已经介绍完毕了:身份信息的存放容器SecurityContextHolder,身份信息的抽象Authentication,身份认证器AuthenticationManager及其认证流程。姑且在这里做一个分隔线。下面来介绍下AuthenticationProvider接口的具体实现。

但是我要纠结一下,看一个认证器的具体实现。

3. 认证器的具体实现

3.1 DaoAuthenticationProvider

这一小节看一下具体的实现
DaoAuthenticationProvider 的uml类图如下。
7tROeI.png

  • 在DaoAuthenticationProvider中,对应的方法便是retrieveUser,虽然有两个参数,但是retrieveUser只有第一个参数起主要作用,返回一个UserDetails。
  • 还需要完成UsernamePasswordAuthenticationToken和UserDetails密码的比对,这便是交给additionalAuthenticationChecks方法完成的,如果这个void方法没有抛异常,则认为比对成功。
  • 比对密码的过程,用到了PasswordEncoder和SaltSource,密码加密和盐的概念相信不用我赘述了,它们为保障安全而设计,都是比较基础的概念。
protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
		catch (UsernameNotFoundException ex) {
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
			throw ex;
		}
		catch (Exception ex) {
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}

3.2 UserDetails与UserDetailsService

  • UserDetails这个接口,它代表了最详细的用户信息,这个接口涵盖了一些必要的用户信息字段,具体的实现类对它进行了扩展。
  • 它和Authentication接口很类似,比如它们都拥有username,authorities,区分他们也是本文的重点内容之一。
  • Authentication的getCredentials()与UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形成的。还记得Authentication接口中的getUserDetails()方法吗?其中的UserDetails用户详细信息便是经过了AuthenticationProvider之后被填充的。
public interface UserDetails extends Serializable {

	Collection<? extends GrantedAuthority> getAuthorities();


	String getPassword();


	String getUsername();


	boolean isAccountNonExpired();


	boolean isAccountNonLocked();

	boolean isCredentialsNonExpired();

	
	boolean isEnabled();
}

UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息,仅此而已,记住这一点,可以避免走很多弯路。UserDetailsService常见的实现类有JdbcDaoImpl,InMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户,也可以自己实现UserDetailsService,通常这更加灵活

小结:
认证过程的架构概览图
7t430s.png


Author: 向天歌
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source 向天歌 !
  TOC