spring security中的SecurityContext

发布时间 2023-04-20 21:34:11作者: 程序晓猿

这一节来研究下SecurityContext的作用

一、SecurityContext

SecurityContext是spring security中的安全上下文,它是一个接口,先来看下它的定义

public interface SecurityContext extends Serializable {


	//获取当前的Authentication对象
	Authentication getAuthentication();

	//修改当前的Authentication对象
	void setAuthentication(Authentication authentication);
}

Authentication前边的章节已经介绍过了,它抽象描述了当前登录用户的信息,包括用户信息和权限信息等,

而这个SecurityContext就是用来获取和操作Authentication对象的,可以理解为它就是用来保持Authentication对象的。

二、SecurityContextImpl

对于SecurityContext,spring security只提供了一个实现类SecurityContextImpl,我们来简单看下这个类的部分源码

public class SecurityContextImpl implements SecurityContext {
    //内部持有的Authentication对象
    private Authentication authentication;
    
    //2个构造方法
    public SecurityContextImpl() {}

	public SecurityContextImpl(Authentication authentication) {
		this.authentication = authentication;
	}
    
    @Override
	public Authentication getAuthentication() {
		return authentication;
	}
    
    @Override
	public void setAuthentication(Authentication authentication) {
		this.authentication = authentication;
	}
    
    省略其他方法
}

可以看到这个类中持有了一个Authentication对象,提供了get/set方法,并没有什么其他的逻辑了,这也说明了

SecurityContext 这个类的功能很纯粹,就是保持Authentication对象,提供get/set方法。

三、SecurityContextHolder

从名字可以看出这个类就是用来对SecurityContext进行保持的,源码中的注释很形象的描述了此类的功能

//Associates a given SecurityContext with the current execution thread.
//在当前线程的范围内保持SecurityContext
public class SecurityContextHolder {
    
    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);
    //保持SecurityContext的策略类
	private static SecurityContextHolderStrategy strategy;
	private static int initializeCount = 0;
    
    static {
        //这个静态方法中会初始化strategy
		initialize();
	}
    
    private static void initialize() {
        //如果没有指定策略名就给一个默认值
		if (!StringUtils.hasText(strategyName)) {
			// Set default
			strategyName = MODE_THREADLOCAL;
		}
		//根据名称创建对应的策略类对象
		if (strategyName.equals(MODE_THREADLOCAL)) {
            //基于ThreadLocal的实现,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++;
	}
    //设置SecurityContext
    public static void setContext(SecurityContext context) {
		strategy.setContext(context);
	}
    //创建一个空的SecurityContext
    public static SecurityContext createEmptyContext() {
		return strategy.createEmptyContext();
	}
    //获取SecurityContext对象
    public static SecurityContext getContext() {
		return strategy.getContext();
	}
}

四、SecurityContextHolderStrategy

保持SecurityContext时使用的策略

public interface SecurityContextHolderStrategy {

	/**
	 * Clears the current context.
	 */
	void clearContext();

	/**
	 * Obtains the current context.
	 *
	 * @return a context (never <code>null</code> - create a default implementation if
	 * necessary)
	 */
	SecurityContext getContext();

	/**
	 * Sets the current context.
	 *
	 * @param context to the new argument (should never be <code>null</code>, although
	 * implementations must check if <code>null</code> has been passed and throw an
	 * <code>IllegalArgumentException</code> in such cases)
	 */
	void setContext(SecurityContext context);

	/**
	 * Creates a new, empty context implementation, for use by
	 * <tt>SecurityContextRepository</tt> implementations, when creating a new context for
	 * the first time.
	 *
	 * @return the empty context.
	 */
	SecurityContext createEmptyContext();
}

五、ThreadLocalSecurityContextHolderStrategy

这是基于ThreadLocal实现的SecurityContext保持策略,就是把SecurityContext对象保存在当前线程对应的

ThreadLocal中,这样在当前线程的范围内都能获取到,SecurityContext只有一个实现类SecurityContextImpl

,所以这里放的其实是这个对象。

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();
	}
}

六、总结

我们以表单登录的情况来举例,当一个登录请求到达UsernamePasswordAuthenticationFilter 然后被处理认证成功就会使用SecurityContextHolder创建出SecurityContext并保存在ThreadLocal中,

从SecurityContextHolder到SecurityContextHolderStrategy终于完成了对SecurityContext也就是Authentication对象的保持,然后请求继续走其他过滤器的逻辑,在这些后续逻辑中就可以通过SecurityContextHolder来获取SecurityContext对象。

但是当前请求完成后,下次请求时还能从ThreadLocal中获取到吗?因为下次请求不是同一个线程处理的,所以显然不能,而我们实际使用spring security时登录成功后后续的请求都是不需要登录的也就是SecurityContext在多次请求间被保持住了,我们需要研究下这是怎么实现的。

七、SecurityContextPersistenceFilter

这个过滤器最终会被添加到DefaultSecurityFilterChain中,用来处理多次请求间SecurityContext的还原问题。

先来简单看下这个类的源码

/*
Populates the SecurityContextHolder with information obtained from the configured SecurityContextRepository prior to the request and stores it back in the repository once the request has completed and clearing the context holder. By default it uses an HttpSessionSecurityContextRepository. 
上面这段注释说明了登录成功后此过滤器会利用SecurityContextRepository把SecurityContext持久化,
下次访问时此过滤器会根据当前请求信息利用SecurityContextRepository把SecurityContext读取出来,
这样实现登录成功后的后续请求可以直接获取SecurityContext的功能。
*/
public class SecurityContextPersistenceFilter extends GenericFilterBean {
    //这个属性定义了SecurityContext的存储策略,可以保存在session等地方
    private SecurityContextRepository repo;
    
    //两个构造方法
    public SecurityContextPersistenceFilter() {
		this(new HttpSessionSecurityContextRepository());
	}

	public SecurityContextPersistenceFilter(SecurityContextRepository repo) {
		this.repo = repo;
	}
    
    //此过滤器的核心逻辑,我们只需要关注SecurityContext相关的核心逻辑
    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);
        //利用repo从请求中加载SecurityContext信息,
        //如果还没有登录过这里获取到的就是一个空的SecurityContext
		SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

		try {
            //把加载到的SecurityContext放到SecurityContextHolder中
			SecurityContextHolder.setContext(contextBeforeChainExecution);
			//放行请求执行后续的过滤器
			chain.doFilter(holder.getRequest(), holder.getResponse());

		}
		finally {
            //这里表示请求已经结束了,从SecurityContextHolder中再次获取SecurityContext,
            //如果当前请求是一个登录请求,在登录成功后会去更新SecurityContextHolder中的
            //SecurityContext
			SecurityContext contextAfterChainExecution = SecurityContextHolder
					.getContext();
			// Crucial removal of SecurityContextHolder contents - do this before anything
			// else.
            //请求结束前清空SecurityContextHolder
			SecurityContextHolder.clearContext();
            //把最新的SecurityContext利用repo进行持久化,下次请求时再上边的try里边的
            //contextBeforeChainExecution就能获取到这次保存的结果。
			repo.saveContext(contextAfterChainExecution, holder.getRequest(),
					holder.getResponse());
			request.removeAttribute(FILTER_APPLIED);

			if (debug) {
				logger.debug("SecurityContextHolder now cleared, as request processing completed");
			}
		}
	}
}

所以我们知道了这个过滤器SecurityContextPersistenceFilter 内部利用SecurityContextRepository来保存和加载SecurityContext。

所以接着看下SecurityContextRepository

八、SecurityContextRepository

public interface SecurityContextRepository {

	
	SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder);

	
	void saveContext(SecurityContext context, HttpServletRequest request,
			HttpServletResponse response);

	/**
	 * Allows the repository to be queried as to whether it contains a security context
	 * for the current request.
	 *
	 * @param request the current request
	 * @return true if a context is found for the request, false otherwise
	 */
	boolean containsContext(HttpServletRequest request);
}

这里边定义了loadContext,saveContext,containsContext这几个方法。

spring security提供了基于session的实现,也就是把SecurityContext保存在session中,如果我们的应用不使用session,就可以使用自己的实现,通过SecurityContextConfigurer#securityContextRepository方法来提供。

8.1 HttpSessionSecurityContextRepository

我们重点关注下期中的saveContext方法

public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
    public void saveContext(SecurityContext context, HttpServletRequest request,
			HttpServletResponse response) {
		SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils
				.getNativeResponse(response,
						SaveContextOnUpdateOrErrorResponseWrapper.class);
		if (responseWrapper == null) {
			throw new IllegalStateException(
					"Cannot invoke saveContext on response "
							+ response
							+ ". You must use the HttpRequestResponseHolder.response after invoking loadContext");
		}
		// saveContext() might already be called by the response wrapper
		// if something in the chain called sendError() or sendRedirect(). This ensures we
		// only call it
		// once per request.
		if (!responseWrapper.isContextSaved()) {
            //真正的保存逻辑,最后会调用到本类的另一个saveContext方法
			responseWrapper.saveContext(context);
		}
	}
    
    protected void saveContext(SecurityContext context) {
			final Authentication authentication = context.getAuthentication();
			HttpSession httpSession = request.getSession(false);

			// See SEC-776
			if (authentication == null || trustResolver.isAnonymous(authentication)) {
				if (logger.isDebugEnabled()) {
					logger.debug("SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.");
				}

				if (httpSession != null && authBeforeExecution != null) {
					// SEC-1587 A non-anonymous context may still be in the session
					// SEC-1735 remove if the contextBeforeExecution was not anonymous
					httpSession.removeAttribute(springSecurityContextKey);
				}
				return;
			}

			if (httpSession == null) {
				httpSession = createNewSessionIfAllowed(context);
			}

			// If HttpSession exists, store current SecurityContext but only if it has
			// actually changed in this thread (see SEC-37, SEC-1307, SEC-1528)
			if (httpSession != null) {
				// We may have a new session, so check also whether the context attribute
				// is set SEC-1561
                 // 这里就是把SecurityContext保存到session里,
                 // 注意保存前有一个判断context是否发生变化的方法 contextChanged,
                 // 如果判断结果是没变化就不会进行保存
				if (contextChanged(context)
						|| httpSession.getAttribute(springSecurityContextKey) == null) {
					httpSession.setAttribute(springSecurityContextKey, context);

					if (logger.isDebugEnabled()) {
						logger.debug("SecurityContext '" + context
								+ "' stored to HttpSession: '" + httpSession);
					}
				}
			}
		}
    
    private boolean contextChanged(SecurityContext context) {
        //只有context发生变化了,或者Authentication发生变化了才会认为是变化了,
        //所以我们如果在应用中修改了Authentication中的用户信息,一定要创建一个新的SecurityContext
        //放到SecurityContextHolder中,这样才会被更新到session中。
			return context != contextBeforeExecution
					|| context.getAuthentication() != authBeforeExecution;
	}
}

8.2 SecurityContextConfigurer

这个configurer用来给DefaultSecurityFilterChain中添加SecurityContextPersistenceFilter,所以这个configurer实际上会被添加到HttpSecurity中,HttpSecurity中提供了一个方法用来添加此configurer

public final class HttpSecurity extends
		AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity>
		implements SecurityBuilder<DefaultSecurityFilterChain>,
HttpSecurityBuilder<HttpSecurity> {
    
    //这个方法就是在给HttpSecurity中添加SecurityContextConfigurer,
    //这个方法会在WebSecurityConfigurerAdapter#getHttp中被调用
    public SecurityContextConfigurer<HttpSecurity> securityContext() throws Exception {
		return getOrApply(new SecurityContextConfigurer<>());
	}
}

接着来看下SecurityContextConfigurer

public final class SecurityContextConfigurer<H extends HttpSecurityBuilder<H>> extends
		AbstractHttpConfigurer<SecurityContextConfigurer<H>, H> {

	/**
	 * Creates a new instance
	 * @see HttpSecurity#securityContext()
	 */
	public SecurityContextConfigurer() {
	}

	/**
	 * Specifies the shared {@link SecurityContextRepository} that is to be used
	 * @param securityContextRepository the {@link SecurityContextRepository} to use
	 * @return the {@link HttpSecurity} for further customizations
	 */
    //这个方法用来给此configurer设置SecurityContextRepository
	public SecurityContextConfigurer<H> securityContextRepository(
			SecurityContextRepository securityContextRepository) {
		getBuilder().setSharedObject(SecurityContextRepository.class,
				securityContextRepository);
		return this;
	}

	@Override
	@SuppressWarnings("unchecked")
	public void configure(H http) throws Exception {

		SecurityContextRepository securityContextRepository = http
				.getSharedObject(SecurityContextRepository.class);
		if (securityContextRepository == null) {
			securityContextRepository = new HttpSessionSecurityContextRepository();
		}
        //这里创建过滤器
		SecurityContextPersistenceFilter securityContextFilter = new SecurityContextPersistenceFilter(
				securityContextRepository);
		SessionManagementConfigurer<?> sessionManagement = http
				.getConfigurer(SessionManagementConfigurer.class);
		SessionCreationPolicy sessionCreationPolicy = sessionManagement == null ? null
				: sessionManagement.getSessionCreationPolicy();
		if (SessionCreationPolicy.ALWAYS == sessionCreationPolicy) {
			securityContextFilter.setForceEagerSessionCreation(true);
		}
		securityContextFilter = postProcess(securityContextFilter);
        //这里添加过滤器
		http.addFilter(securityContextFilter);
	}
}