SpringSecurity(七)用户数据获取之SpringSecurityContextHolder深度剖析(上)

登录用户数据获取

登录成功之后,在后续的业务逻辑中,我们可能还需要获取登录成功用户的用户对象,如果我们不使用任何安全框架,我们可以将用户信息保存在HttpSession中,需要的话就从HttpSession中获取数据。在SpringSecurity中,用户登录信息本质上还是保存在HttpSession中,但是为了方便使用,SpringSecurity对HttpSession中的用户信息进行了封装,封装之后,我们在想获取用户登录数据就会有两种不同的思路:

  • 从SecurityContextHolder中获取
  • 从当前请求对象中获取。

这里列出的两种方式是主流的做法,当然也可以使用一些非主流的方法获取登录成功后的用户信息,例如直接从HttpSession中获取用户登录数据。。

无论是哪种获取方式,都离不开一个重要的对象:Authentication。在SpringSecurity中,Authentication对象主要有两个方面的功能:

(1)、作为AuthenticationManager的输入参数,提供用户身份认证的凭证,当它作为一个输入参数时,它的isAuthentication方法返回false,表示用户还未认证。

(2)、代表已经经过认证的用户,此时的Authentication可以从SecurityContext中获取。


一个Authentication对象主要包含三个方面的信息:

(1)、principal:定义认证的用户。如果用户使用用户名/密码的方式登录,principal通常就算一个UserDetails对象。

(2)、credentials:登录凭证,一般就是指密码,当用户登录成功之后,登录凭证会被自动擦除,以防止泄露。

(3)、authorities:用户被授予的权限信息。

Java中本身提供了Principal接口用来描述认证主题,Principal可以代表一个公司、个人或者登录ID,SpringSecurity中定义了Authentication接口用来规范登录用户信息,Authentication继承自Principal:

public interface Authentication extends Principal, Serializable {
	
	Collection<? extends GrantedAuthority> getAuthorities();
	
	Object getCredentials();
	
	Object getDetails();

	Object getPrincipal();
	
	boolean isAuthenticated();

	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;

}

这里接口中的定义方法都很好理解:

  • getAuthorities方法:获取用户权限
  • getCredentials方法:用来获取用户凭证,一般来说就是密码。
  • getDetails方法:用来获取用户的详细信息,可能是当前的请求之类。
  • getPrincipal方法:用来获取当前用户信息信息,可能是一个用户名,也可能是一个用户对象。
  • isAuthenticated方法:当前用户是否认证成功。

可以看到,在SpringSecurity中,只要获取到Authentication对象,就可以获取到登录用户的详细信息。

不同的认证方式对应不同的Authentication实例,SpringSecurity中的Authentication实例类如下:
在这里插入图片描述

(1)、AbstractAuthenticationToken:该类实现了Authentication和CredentialsContainer两个接口,在AbstractAuthenticationToken中对Authentication接口定义的各个数据获取方法进行了实现,CredentialsContainer则提供了登录凭证擦除方法。一般登录成功后,为了防止用户信息泄露,可以将登录凭证(例如密码)擦除。

(2)、RememberMeAuthenticationToken:如果用户使用RememberMe的方式登录,登录信息封装在该类中。

(3)、TestingAuthenticationToken:单元测试时封装的用户对象。

(4)、AnonymousAuthenticationToken:匿名登录时封装的用户对象。

(5)、UsernamePasswordAuthenticationToken:表单登录时封装的用户对象。

(6)、RunAsUserToken:替换验证身份时封装的用户对象。

(7)、JaasAuthenticationToken:JAAS认证时封装的用户对象。

(8)、PreAuthenticatedAuthenticationToken:Pre-Authentication场景下封装的用户对象。

在这些Authentication的实例中,最常用的有两个:UsernamePasswordAuthenticationToken和RememberMeAuthenticationToken。在上几篇文章中我们案例的认证对象就是UsernamePasswordAuthenticationToken。

了解了Authentication对象之后,接下来我们来看一下如何在登录成功后获取用户的登录信息,即Authentication对象。


从SecurityContextHolder中获取

我们增加一个UserController,如下:

@RestController
public class UserController {


    @GetMapping("/user")
    public void userInfo() {
        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String name = authentication.getName();
        final Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        System.out.println("name = "+ name);
        System.out.println("authorities = " + authorities);
    }
}

配完成之后,启动项目登录成功后访问/user接口,可以看到打印的信息如下。

在这里插入图片描述

这里为了方便,我们在Controller中获取登录用户信息,可以发现SecurityContextHolder.getContext()是一个静态方法,也就意味着我们随时随地都可以获取到登录用户信息,在service层也可以获取到登录用户信息(实际中,大部分情况也都是在service层获取)

获取登录用户信息的代码很简单,那么SecurityContextHolder到底是什么?它里面的数据又是从何来的?


SecurityContextHolder

SecurityContextHolder中存储的是SecurityContext,SecurityContext中存储的则是Authenticaion,三者的关系如下:
在这里插入图片描述

这里清楚的描述了三者之间的关系。

首先在SpringSecurityContextHolder中存放的是SecurityContext,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";

	private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";

(1)、MODE_THREADLOCAL:这种存放策略是将SecurityContext存放在ThreadLocal中,大家知道ThreadLocal的特点是在哪个线程中存储就要在哪个线程中读取,这其实非常适合web应用,因为在默认情况下,一个请求无论经过多少Filter到达Servlet,都是由一个线程来处理的,这也是SecurityContextHolder默认的存储策略,这种存储策略意味着如果在具体的业务代码中,开启了子线程,在子线程去获取登录用户数据,就会获取不到。

(2)、MODE_INHERITABLETHREADLOCAL:这种存储模式适用于多线程环境,如果希望在子线程中也能够获取到登录用户数据,那么可以使用这种存储模式。

(3)、MODE_GLOBAL:这种存储模式实际上是将数据保存在一个静态变量中,在JAVAWeb开发中,这种模式很少使用到。

SpringSecurity中定义了SecurityContextHolderStrategy接口用来规范存储策略中的方法,我们来看下:

public interface SecurityContextHolderStrategy {

	void clearContext();

	SecurityContext getContext();

	void setContext(SecurityContext context);
    
    SecurityContext createEmptyContext();

}

接口中一共定义了四个方法:

(1)、clearContext:用来清除存储的SecurityContext对象。

(2)、getContext:用来获取存储SecurityContext对象。

(3)、setContext:用来设置存储的SecurityContext对象。

(4)、createEmptyContext:用来创建一个空的SecurityContext对象。

在SpringSecurity中,SecurityContextHolderStrategy接口一共有三个实现类,对应了三种不同存储策略。

在这里插入图片描述

每一个实现类都对应了不同的实现策略,我们先来看一下ThreadLocalSecurityContextHolderStrategy:

final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {

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

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

	@Override
	public SecurityContext getContext() {
		SecurityContext ctx = contextHolder.get();
		if (ctx == null) {
			ctx = createEmptyContext();
			contextHolder.set(ctx);
		}
		return ctx;
	}

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

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

}

ThreadLocalSecurityContextHolderStrategy实现了SecurityContextHolderStrategy接口并实现了接口中的方法,存储数据的载体是一个ThreadLocal,所以针对SecurityContext的清空,获取以及存储,都是在ThreadLocal中进行操作,例如清空就是调用ThreadLocal的remove方法。SecurityContext是一个接口,它只有一个实现类SecurityContextImpl,所以创建就直接新建一个SecurityContextImpl对象即可。


在来看InheritableThreadLocalSecurityContextHolderStrategy:

final class InheritableThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {

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

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

	@Override
	public SecurityContext getContext() {
		SecurityContext ctx = contextHolder.get();
		if (ctx == null) {
			ctx = createEmptyContext();
			contextHolder.set(ctx);
		}
		return ctx;
	}

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

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

}

InheritableThreadLocalSecurityContextHolderStrategy和ThreadLocalSecurityContextHolderStrategy的实现策略基本一致,不同的是存储数据的载体变了,在InheritableThreadLocalSecurityContextHolderStrategy中存储数据的载体变成了InheritableThreadLocal。InheritableThreadLocal继承自ThreadLocal,但是多了一个特性,就是在子线程创建的一瞬间,会自动父线程数据复制到子线程中。该策略就是利用了这已特性,实现了在子线程中获取登录用户信息的功能。


最后再来看GlobalSecurityContextHolderStrategy

final class GlobalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {

	private static SecurityContext contextHolder;

	@Override
	public void clearContext() {
		contextHolder = null;
	}

	@Override
	public SecurityContext getContext() {
		if (contextHolder == null) {
			contextHolder = new SecurityContextImpl();
		}
		return contextHolder;
	}

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

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

}

GlobalSecurityContextHolderStrategy的实现就更简单了,用一个静态变量来保存SecurityContext,所以它也可以在多线程下使用,但是一般在Web开发中,这种存储策略使用少。


最后我们在看一下SecurityContextHolder:

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";

	private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";

	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() {
		initializeStrategy();
		initializeCount++;
	}

	private static void initializeStrategy() {
		if (MODE_PRE_INITIALIZED.equals(strategyName)) {
			Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
					+ ", setContextHolderStrategy must be called with the fully constructed strategy");
			return;
		}
		if (!StringUtils.hasText(strategyName)) {
			// Set default
			strategyName = MODE_THREADLOCAL;
		}
		if (strategyName.equals(MODE_THREADLOCAL)) {
			strategy = new ThreadLocalSecurityContextHolderStrategy();
			return;
		}
		if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
			strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
			return;
		}
		if (strategyName.equals(MODE_GLOBAL)) {
			strategy = new GlobalSecurityContextHolderStrategy();
			return;
		}
		// 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);
		}
	}

	
	public static void clearContext() {
		strategy.clearContext();
	}

	
	public static SecurityContext getContext() {
		return strategy.getContext();
	}

	
	public static int getInitializeCount() {
		return initializeCount;
	}

	
	public static void setContext(SecurityContext context) {
		strategy.setContext(context);
	}

	
	public static void setStrategyName(String strategyName) {
		SecurityContextHolder.strategyName = strategyName;
		initialize();
	}

	
	public static void setContextHolderStrategy(SecurityContextHolderStrategy strategy) {
		Assert.notNull(strategy, "securityContextHolderStrategy cannot be null");
		SecurityContextHolder.strategyName = MODE_PRE_INITIALIZED;
		SecurityContextHolder.strategy = strategy;
		initialize();
	}

	
	public static SecurityContextHolderStrategy getContextHolderStrategy() {
		return strategy;
	}

	/**
	 * Delegates the creation of a new, empty context to the configured strategy.
	 */
	public static SecurityContext createEmptyContext() {
		return strategy.createEmptyContext();
	}

	@Override
	public String toString() {
		return "SecurityContextHolder[strategy='" + strategy.getClass().getSimpleName() + "'; initializeCount="
				+ initializeCount + "]";
	}

}

从这可以看到SecurityContextHolder定义了三个静态常量用来描述三种不同的存储策略,存储策略stragey会在静态代码块中进行初始化,根据不同的strategyName初始化不同的存储策略,strategyName变量表示目前正在使用的存储策略,我们可以通过配置系统变量或者调用setStrategyName来修改SecurityContextHolder中的存储策略,调用setStrategyName后会重新初始化strategy。


默认情况下,如果我们试图从子线程中获取当前登录用户数据,就会获取失败,如下:

我们来修改一下上面/user接口

 @GetMapping("/user")
    public void userInfo() {
        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String name = authentication.getName();
        final Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        System.out.println("name = "+ name);
        System.out.println("authorities = " + authorities);

        new Thread(new Runnable() {
            @Override
            public void run() {
                final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                if(authentication == null){
                    System.out.println("获取用户失败");
                    return;
                }
                String name = authentication.getName();
                final Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
                final String threadName = Thread.currentThread().getName();
                System.out.println(threadName + ":name = "+ name);
                System.out.println(threadName + ":authorities = " + authorities);


            }
        }).start();

    }
}

在这里插入图片描述

可以发现在子线程中获取信息失败。

子线程之所以获取不到用户登录信息,就是因为数据存储在ThreadLocal中,存储和读取不是同一个线程,所以获取不到,如果希望子线程中也能获取到用户信息,可以将SecurityContextHolder中的存储信息策略改为MODE_INHERITABLETHREADLOCAL,这样就支持多线程环境下获取登录用户信息了。

默认的存储策略是通过System.getProperties加载的,因此我们可以通过配置系统变量来修改默认的存储策略,在IDEA中,我们可以添加vm options参数。如下

在这里插入图片描述

-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL

然后可以看到控制台打印,在线程中获取到了登录用户信息如下:

在这里插入图片描述

这有一个问题,既然SecurityContextHolder默认是将用户信息存储在ThreadLocal中,在SpringBoot中不同的请求都是由不同的线程处理的,那为什么每一次请求都还能从SecurityContextHolder中获取到登录用户信息?这个问题有点长,我们在下一篇文章中进行讨论一下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈橙橙丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值