下班就开始肝的Spring Security的笔记,学习了| Java Debug 笔记

325 阅读7分钟

前言

本文正在参加「Java主题月 - Java Debug笔记活动」,详情查看 活动链接

学习Spring Security有一段时间了,今天就大致理一理Spring Security核心重要对象,有问题欢迎指出,互相学习👏🏻

SecurityContextHolder

SecurityContextHolderSpring Security 中最核心的组件之一,内部封装了保存应用程序中的安全上下文SecurityContext的逻辑,提供了核心的静态方法暴露getContextsetContext用来设置和获取当前请求线程的安全上下文,主要在SecurityContextPersistenceFilter拦截器中SecurityContextHolderSecurityContext建立链接

SecurityContextHolder和SecurityContext的关联

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
  			//是否执行过
        if (request.getAttribute("__spring_security_scpf_applied") != null) {
            chain.doFilter(request, response);
        } else {
            ...
		
            HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
          	//获取SpringSecurity,默认从HttpSession中获取,获取不到则自动新生成一个SecurityContext
            SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
            boolean var10 = false;

            try {
                var10 = true;
              	//SecurityContextHolder
                SecurityContextHolder.setContext(contextBeforeChainExecution);
                
              	...
                  
                chain.doFilter(holder.getRequest(), holder.getResponse());
                var10 = false;
            } finally {
                if (var10) {
                  ...
                }
            }
						//获取SecurityContext
            SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
          	//清空上下文中的SecurityContext
            SecurityContextHolder.clearContext();
          	//将经过拦截器处理后的SecurityContext重新设置到session中,便于下次请求再次获取
            this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
            request.removeAttribute("__spring_security_scpf_applied");
            this.logger.debug("Cleared SecurityContextHolder to complete request");
        }
    }

核心逻辑实现

  • 默认从HttpSession中获取SecurityContext,没有则自动生成一个新的SecurityContext,设置到SecurityContextHolder中,继续执行后续拦截器
  • 其他拦截器以及核心逻辑执行完成后,获取SecurityContext,清空上下文信息,并且将经过拦截器处理后的SecurityContext重新设置到session中,便于下次请求再次获取

内部如何保存SecurityContext

SecurityContextHolder保存SecurityContext内部默认有三种模式

  • MODE_THREADLOCAL ThreadLocal模式,通过ThreadLocal方式,将上下文保存到当前请求线程
  • MODE_INHERITABLETHREADLOCAL 父子线程ThreadLocal模式,本质上也是通过ThreadLocal方式,将上下文保存到当前请求线程
  • MODE_GLOBAL 全局模式,适用于全局唯一类型

默认在初始化在initialize方法中缺省实现为ThreadLocal模式

public class SecurityContextHolder {
	//ThreadLocal模式
	public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
	
  //父子线程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;

	static {
		initialize();
	}

	private static void initialize() {
		if (!StringUtils.hasText(strategyName)) {
			// Set default	,默认ThreadLocal模式
			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++;
	}

Authentication

Authentication本质上是一个抽象接口,定义了身份验证的一系列服务抽象

  • getAuthorities 当前授权的权限列表
  • getCredentials 密码信息
  • getDetails 登录信息。访问者的ip地址和sessionId的值等
  • getPrincipal 登录之后返回UserDetails的实现,一般是org.springframework.security.core.userdetails.User
public interface Authentication extends Principal, Serializable {

	/**
	 * 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;

}

Authentication的抽象实现为AbstractAuthenticationToken,主要定义了抽象中的实体变量定义,比如

private final Collection<GrantedAuthority> authorities;

private Object details;

private boolean authenticated = false;

AbstractAuthenticationToken默认的实现为UsernamePasswordAuthenticationToken,本质上跟AbstractAuthenticationToken没太大差别,是其具体实现

UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter是SpringSecurity在web应用中重要的Filter,其主要功能用来通过拿到request请求中的userNamepassword,生成对应的UsernamePasswordAuthenticationToken认真实体对象,通过AuthenticationManager认证管理器来进行认证登录,并且返回一个填充了授权信息的Authentication对象

UsernamePasswordAuthenticationFilter核心实现在于其attemptAuthentication,尝试认证并返回一个填充了授权信息的Authentication对象

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    if (this.postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    } else {
    		//获取用户名,密码信息
        String username = this.obtainUsername(request);
        username = username != null ? username : "";
        username = username.trim();
        String password = this.obtainPassword(request);
        password = password != null ? password : "";
        //生成UsernamePasswordAuthenticationToken对象
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        this.setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}
  • 非Post请求不支持,抛出异常
  • request对象中获取用户名密码,生成UsernamePasswordAuthenticationToken认证对象,
  • 通过认证管理器AuthenticationManager进行认证

AuthenticationManager

AuthenticationManager只有authenticate一个方法,authenticate方法定义了认证的基本服务,authenticate方法的入参和出参都是同一个对象,从authenticate方法的描述中可以得知,尝试去认证一个Authentication对象,返回一个被填充了granted authorities的对象,也是就说需要在authenticate方法的实现中填充granted authorities对象的其他信息包含授权和用户个人信息等等

authenticate可能抛出以下异常AuthenticationException

    *  DisabledException 		账户禁用
    *  LockedException	       账户锁定
    *  BadCredentialsException  密码错误
    *  UsernameNotFoundException  账户不存在
    *  等等...
public interface AuthenticationManager {

	/**
	 * Attempts to authenticate the passed {@link Authentication} object, returning a
	 * fully populated <code>Authentication</code> object (including granted authorities)
	 * if successful.
	 * <p>
	 * An <code>AuthenticationManager</code> must honour the following contract concerning
	 * exceptions:
	 * <ul>
	 * <li>A {@link DisabledException} must be thrown if an account is disabled and the
	 * <code>AuthenticationManager</code> can test for this state.</li>
	 * <li>A {@link LockedException} must be thrown if an account is locked and the
	 * <code>AuthenticationManager</code> can test for account locking.</li>
	 * <li>A {@link BadCredentialsException} must be thrown if incorrect credentials are
	 * presented. Whilst the above exceptions are optional, an
	 * <code>AuthenticationManager</code> must <B>always</B> test credentials.</li>
	 * </ul>
	 * Exceptions should be tested for and if applicable thrown in the order expressed
	 * above (i.e. if an account is disabled or locked, the authentication request is
	 * immediately rejected and the credentials testing process is not performed). This
	 * prevents credentials being tested against disabled or locked accounts.
	 * @param authentication the authentication request object
	 * @return a fully authenticated object including credentials
	 * @throws AuthenticationException if authentication fails
	 */
	Authentication authenticate(Authentication authentication) throws AuthenticationException;

ProviderManager

providerManagerAuthenticationManager的具体实现,在providerManagerauthenticate方法实现中,主要是遍历所有的AuthenticationProvider的实现,通过provider.supports方法识别当前传入的authentication对象实现是否是当前provider所支持的,如果不支持则跳过,直到找到一个匹配的

Class<? extends Authentication> toTest = authentication.getClass();
for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}
			......
			try {
				result = provider.authenticate(authentication);
				if (result != null) {
					....
				}
			}
			catch (){
        ....
      }
		}

AuthenticationProvider

在Spring Security中,提供了多种认证方式,包括DAO认证,简单用户密码认证,三方认证,匿名认证等,AuthenticationProvider表示认证方式,可选多种认证方式,默认Spring Security中已经提供了多种实现,包括

  • DaoAuthenticationProvider
  • RemoteAuthenticationProvider
  • AnonymousAuthenticationProvider
  • TestingAuthenticationProvider
  • 等等

在扩展DB验证的时候我们常使用DaoAuthenticationProviderDaoAuthenticationProvider看名字就知道跟数据库打交道用来校验识别登录认证信息的

image-20210405122645960

DaoAuthenticationProvider

DaoAuthenticationProviderretrieveUser方法主要通过

其最终目的,就是根据 UsernamePasswordAuthenticationToken,获取到 username,然后调用 UserDetailsService 检索用户详细信息。这里面就是我们熟悉的UserDetailsService扩展服务

@Override
	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);
		}
	}

肝就完事了!