Spring Security —05—认证用户数据

5.1登录用户数据获取

5.1.1 SecurityContextHolder

Spring Security 会将登录用户数据保存在 Session 中,但是,为了使用方便,Spring Security在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定:当用户登录成功后,Spring Security 会将登录成功的用户信息保存到 SecurityContextHolder 中。

SecurityContextHolder 中的数据保存默认是通过ThreadLocal 来实现的,使用 ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,Spring Security 会将 SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将 SecurityContexHolder 中的数据清空。以后每当有请求到来时,Spring Security 就会先从 Session 中取出用户登录数据,保存到SecurityContextHolder 中,方便在该请求的后续处理过程中使用,同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将SecurityContextHolder 中的数据清空。

实际上 SecurityContextHolder 中存储是 SecurityContext,在 SecurityContext 中存储是 Authentication
在这里插入图片描述
这种设计是典型的策略设计模式:
SecurityContextHolder.java

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

	/**
	 * Explicitly clears the context value from the current thread.
	 */
	public static void clearContext() {
		strategy.clearContext();
	}

	/**
	 * Obtain the current <code>SecurityContext</code>.
	 * @return the security context (never <code>null</code>)
	 */
	public static SecurityContext getContext() {
		return strategy.getContext();
	}

	/**
	 * 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)} or
	 * {@link #setContextHolderStrategy(SecurityContextHolderStrategy)} to switch to an
	 * alternate strategy).
	 */
	public static int getInitializeCount() {
		return initializeCount;
	}

	/**
	 * Associates a new <code>SecurityContext</code> with the current thread of execution.
	 * @param context the new <code>SecurityContext</code> (may not be <code>null</code>)
	 */
	public static void setContext(SecurityContext context) {
		strategy.setContext(context);
	}

	/**
	 * Changes the preferred strategy. Do <em>NOT</em> call this method more than once for
	 * a given JVM, as it will re-initialize the strategy and adversely affect any
	 * existing threads using the old strategy.
	 * @param strategyName the fully qualified class name of the strategy that should be
	 * used.
	 */
	public static void setStrategyName(String strategyName) {
		SecurityContextHolder.strategyName = strategyName;
		initialize();
	}

	/**
	 * Use this {@link SecurityContextHolderStrategy}.
	 *
	 * Call either {@link #setStrategyName(String)} or this method, but not both.
	 *
	 * This method is not thread safe. Changing the strategy while requests are in-flight
	 * may cause race conditions.
	 *
	 * {@link SecurityContextHolder} maintains a static reference to the provided
	 * {@link SecurityContextHolderStrategy}. This means that the strategy and its members
	 * will not be garbage collected until you remove your strategy.
	 *
	 * To ensure garbage collection, remember the original strategy like so:
	 *
	 * <pre>
	 *     SecurityContextHolderStrategy original = SecurityContextHolder.getContextHolderStrategy();
	 *     SecurityContextHolder.setContextHolderStrategy(myStrategy);
	 * </pre>
	 *
	 * And then when you are ready for {@code myStrategy} to be garbage collected you can
	 * do:
	 *
	 * <pre>
	 *     SecurityContextHolder.setContextHolderStrategy(original);
	 * </pre>
	 * @param strategy the {@link SecurityContextHolderStrategy} to use
	 * @since 5.6
	 */
	public static void setContextHolderStrategy(SecurityContextHolderStrategy strategy) {
		Assert.notNull(strategy, "securityContextHolderStrategy cannot be null");
		SecurityContextHolder.strategyName = MODE_PRE_INITIALIZED;
		SecurityContextHolder.strategy = strategy;
		initialize();
	}

	/**
	 * Allows retrieval of the context strategy. See SEC-1188.
	 * @return the configured strategy for storing the security context.
	 */
	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 + "]";
	}

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

  • MODE INHERITABLETHREADLOCAL:这种存储模式适用于多线程环境,如果希望在子线程中也能够获取到登录用户数据,那么就可以使用这种存储模式,使用时需要在VM
    Options单独进行配置(实际上是将父线程的用户数据复制一份到子线程)。

  • MODE GLOBAL:这种存储模式实际上是将数据保存在一个全局静态变量中,在 JavaWeb开发中,这种模式很少使用到(了解即可)。

  • 其中的获取SecurityContext 是根据策略获取的,也就是程序的运行参数设置的是那种策略模式

在这里插入图片描述

  • 通过读取系统设置的参数然后进行匹配对strategy实例化不同的实现类,使用不同的策略模式

在这里插入图片描述

5.1.2SecurityContextHolderStrategy

  • SecurityContextHolderStrategy通过策略模式去获取SecurityContext
private static SecurityContextHolderStrategy strategy;

在这里插入图片描述

  • 进入SecurityContextHolderStrategy
 public interface SecurityContextHolderStrategy {
     void clearContext();
     SecurityContext getContext();
     void setContext(SecurityContext context);
     SecurityContext createEmptyContext();
 }
  • SecurityContextHolderStrategy

通过 SecurityContextHolder 可以得知,SecurityContextHolderStrategy 接口用来定义存储策略方法:

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

clearContext:该方法用来清除存储的 SecurityContext对象。
getContext:该方法用来获取存储的 SecurityContext 对象。
setContext:该方法用来设置存储的 SecurityContext 对象。
createEmptyContext:该方法则用来创建一个空的 SecurityContext 对象。

  • 三种模式对应SecurityContextHolderStrategy 的三种实现类

MODE THREADLOCAL:ThreadLocalSecurityContextHolderStrategy

MODE INHERITABLETHREADLOCAL:InheritableThreadLocalSecurityContextHolderStrategy

MODE GLOBAL:GlobalSecurityContextHolderStrategy

在这里插入图片描述

5.1.3SecurityContext

  • 通过SecurityContextHolderStrategy的getContext返回的SecurityContext
public interface SecurityContext extends Serializable {

	/**
	 * Obtains the currently authenticated principal, or an authentication request token.
	 * @return the <code>Authentication</code> or <code>null</code> if no authentication
	 * information is available
	 */
	Authentication getAuthentication();

	/**
	 * Changes the currently authenticated principal, or removes the authentication
	 * information.
	 * @param authentication the new <code>Authentication</code> token, or
	 * <code>null</code> if no further authentication information should be stored
	 */
	void setAuthentication(Authentication authentication);

}
  • SecurityContext 包含的Authentication
public interface Authentication extends Principal, Serializable {
    
    //权限结合,可使用AuthorityUtils.commaSeparatedStringToAuthorityList("admin, ROLE_ADMIN")返回字符串权限集合
    Collection<? extends GrantedAuthority> getAuthorities();
    
    //用户名密码认证时可以理解为密码
    Object getCredentials();
    
    //认证时包含的一些信息。如remoteAddress、sessionId
    Object getDetails();
    
    //用户名密码认证时可理解时用户名
    Object getPrincipal();
    
    //是否被认证,认证为true    
    boolean isAuthenticated();
    
    //设置是否被认证
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
    
}

5.1.4 代码中获取认证之后的用户数据

 @RestController
 public class HelloController {
     @RequestMapping("/hello")
     public String hello() {
         System.out.println("Hello Spring Security!");
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();//获得认证信息
         System.out.println("authentication.getName() = " + authentication.getName());
         System.out.println("authentication.getAuthorities() = " + authentication.getAuthorities());
         System.out.println("authentication.getCredentials() = " + authentication.getCredentials());
         System.out.println("authentication.getDetails() = " + authentication.getDetails());
         System.out.println("authentication.getPrincipal() = " + authentication.getPrincipal());User principal = (User) authentication.getPrincipal();//返回值类型为User时需要进行强制转换 org.springframework.security.core.userdetails.User
         System.out.println("principal.getUsername() = " + principal.getUsername());
         System.out.println("principal.getPassword() = " + principal.getPassword());
         System.out.println("principal.getAuthorities() = " + principal.getAuthorities());
         return "hello spring security!";
     }
 }


启动服务,登录成功后访问:http://localhost:8080/hello,获得认证之后的用户数据。

IDEA控制台输出结果:

 Hello Spring Security!
 authentication.getName() = root
 authentication.getAuthorities() = [ROLE_admin, ROLE_super]
 authentication.getCredentials() = null
 authentication.getDetails() = WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=15FEA49E4810C435286FD8B6D188C87F]
 authentication.getPrincipal() = org.springframework.security.core.userdetails.User [Username=root, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_admin, ROLE_super]]
 principal.getUsername() = root
 principal.getPassword() = null
 principal.getAuthorities() = [ROLE_admin, ROLE_super]

5.1.5 多线程情况下获取用户数据

 package com.study.controller;import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;/**
  * @ClassName HelloController
  * @Description TODO
  * @Date 2022/7/6 12:12
  * @Version 1.0
  */
 @RestController
 public class HelloController {
     @RequestMapping("/hello")
     public String hello() {
         System.out.println("Hello Spring Security!");
         new Thread(() -> {
             Authentication Authentication = SecurityContextHolder.getContext().getAuthentication();
             User childPrincipal = (User) Authentication.getPrincipal();
             System.out.println("Principal.getUsername() = " + Principal.getUsername());
             System.out.println("Principal.getPassword() = " + Principal.getPassword());
             System.out.println("Principal.getAuthorities() = " + Principal.getAuthorities());
         }).start();
         return "hello spring security!";
     }
 }


启动服务,访问:http://localhost:8080/hello,获得认证之后的用户数据。
控制台报错

Exception in thread "Thread-2" java.lang.NullPointerException
     at com.study.controller.HelloController.lambda$hello$0(HelloController.java:35)
     at java.lang.Thread.run(Thread.java:750)

从上面可以看到默认策略,是无法在子线程中获取用户信息,如果需要在子线程中获取必须使用第二种策略,默认策略是通过 System.getProperty 加载的,因此我们可以通过增加 VM Options 参数进行修改。

-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL

在这里插入图片描述启动服务,访问:http://localhost:8080/hello,获得认证之后的用户数据。

IDEA控制台输出结果:

 Hello Spring Security!
 Principal.getUsername() = root
 Principal.getPassword() = null
 Principal.getAuthorities() = [ROLE_admin, ROLE_super]

5.1.6 页面上获取用户信息

  • pom.xml引入依赖,可以直接在thymeleaf页面直接获取springsecurity信息
 <dependency>
   <groupId>org.thymeleaf.extras</groupId>
   <artifactId>thymeleaf-extras-springsecurity5</artifactId>
   <version>3.0.4.RELEASE</version>
 </dependency>
  • logout.html页面加入命名空间
 <html lang="en" xmlns:th="https://www.thymeleaf.org" 
 xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
  • logout.html页面中获取用户信息
 <!--获取认证用户名-->
 <ul>
   <li sec:authentication="principal.username"></li>
   <li sec:authentication="principal.authorities"></li>
   <li sec:authentication="principal.accountNonExpired"></li>
   <li sec:authentication="principal.accountNonLocked"></li>
   <li sec:authentication="principal.credentialsNonExpired"></li>
 </ul>

启动服务,登录成功后访问logout.html:http://localhost:8080/logout.html
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值