关于Spring Security 对于每一次请求是如何初始化上下文的


一、起因

同事发现在项目中新启的线程没有办法访问Spring Security的上下文

二、猜想

上下文是线程隔离的,估计是用ThreadLocal这种数据结构存

三、分析

像上述的猜想,我们一般都能想到,这可能是线程隔离造成的,这突然让我意识到,每次对后端接口的请求,都是一个单独的线程,它们是怎么通过认证的。

难道它们每次请求都要初始化一个上下文出来。这不就相当于每次请求,都做了一次认证吗?这样也太消耗性能了。于是我开始分析。

1.共识

首先我们要达成几点共识

  • 项目中使用的是Spring Security+Oauth2
  • Spring Security 是通过过滤器(Filter)来实现认证校验的
  • Spring Security 是处理认证的,Oauth2是处理授权的,这里不展开讲,它们结合在一块的话,可以简单理解为,认证完后会颁发一个token,这个token对应着其身份,角色,权限等信息。

2.登录状态下如何验证请求的token和如何初始化上下文

当我们在登录状态下,已经获取了这个token,然后随便调取一个接口
请求会经过一个过滤器OAuth2AuthenticationProcessingFilter,让我们去掉不必要的代码,聚焦看一doFilter()方法

/**
* A pre-authentication filter for OAuth2 protected resources. Extracts an OAuth2 token from the incoming request and
* uses it to populate the Spring Security context with an {@link OAuth2Authentication} (if used in conjunction with an
* {@link OAuth2AuthenticationManager}).
* 
* @author Dave Syer
* 
*/
public class OAuth2AuthenticationProcessingFilter implements Filter, InitializingBean {

   public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
   		ServletException {

   	final HttpServletRequest request = (HttpServletRequest) req;
   	final HttpServletResponse response = (HttpServletResponse) res;

   	try {
   		// 1.从request获取token
   		Authentication authentication = tokenExtractor.extract(request);
   		
   		if (authentication == null) {
   			if (stateless && isAuthenticated()) {
   				SecurityContextHolder.clearContext();
   			}
   		} else {
   			// 2.对token进行校验
   			Authentication authResult = authenticationManager.authenticate(authentication);
   			// 3.将认证信息放入上下文中
   			SecurityContextHolder.getContext().setAuthentication(authResult);

   		}
   		
   	} catch (OAuth2Exception failed) {
   		// 认证失败
   		SecurityContextHolder.clearContext();
   		
   		authenticationEntryPoint.commence(request, response, new InsufficientAuthenticationException(failed.getMessage(), failed));

   		return;
   	}

   	// 一切正常,继续执行认证链
   	chain.doFilter(request, response);
   }

}

从上述代码中可以看出校验后,SecurityContextHolder.getContext().setAuthentication(authResult); 负责将认证后的信息初始化到上下文中。
我们在getContext()这个方法中,看到确实是ThreadLocal的数据结构,也就是线程隔离的
在这里插入图片描述
这使得我们在项目的新启线程中需要手动将认证上下文的数据传入。

3.上下文原始数据是从什么地方取得?

上文过滤器中第2步对token进行认证,如下

Authentication authResult = authenticationManager.authenticate(authentication); // authenticationManager类型为OAuth2AuthenticationManager

authenticate(…)方法认证时内部通过tokenServices[DefaultTokenServices].loadAuthentication(String accessTokenValue),其中依赖tokenStore来取token信息,tokenStore根据存储方式不同有不同实现类,如下图
在这里插入图片描述
token可以从内存,jwt,或数据库中获取。
项目中使用的是JdbcTokenStore的形式,也就是从数据库中根据token获取的方式

我们项目中oauht_access_token表中数据
在这里插入图片描述

至此,登录状态下初始化认证上下文的过程已经结束。
它确实是针对每次请求都初始化一个认证上下文出来。我们所能做的优化,就是让这个认证或者校验的过程尽量快速,所以Spring Security也提供了缓存形式的查询实现,以提高效率。

4.登录时如何存储存储token

那Spring Security是怎么将token的相关数据放入到数据库的呢?

这里我仅介绍下我们项目中的实现

  1. 首先Login页面填入用户名密码
  2. 项目中有自己的认证接口
    在这里插入图片描述
    方法中验证了验证码,然后携带用户名密码,通过RestTemplate去访问了Spring Security的TokenEndpoint中的oauth/token
    在这里插入图片描述

方法中具体存储的代码如下

@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
	public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
	Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {


		String clientId = getClientId(principal);
		ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);

		TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

		// 。。。省略一万字 。。。
		
		// 在grant(...)方法中进行了存储
		OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
		if (token == null) {
			throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
		}

		return getResponse(token);

	}

这里不详细展开讲了,看一下栈调用过程吧
在这里插入图片描述
登录后,就可以携带token去访问资源,那时就是第一步中讲的过程了

Spring框架中,Spring上下文(ApplicationContext)是一个负责管理Bean生命周期和提供依赖注入的容器。Spring上下文初始化指的是Spring容器在启动时进行的一系列操作,包括加载配置文件、解析Bean定义、创建和初始化Bean等。 Spring上下文初始化过程主要包括以下几个步骤: 1. 加载配置文件:Spring容器会读取配置文件,可以是XML格式的配置文件(如applicationContext.xml)或基于注解的配置类(如@Configuration注解的类)。 2. 解析Bean定义:在加载配置文件后,Spring容器会解析配置文件或配置类中定义的Bean信息。它会识别和解析Bean的定义、依赖关系、作用域等元数据,并将其存储在内部数据结构中。 3. 创建和初始化Bean:在解析完Bean定义后,Spring容器会根据定义的信息创建和初始化Bean。这包括实例化Bean对象、设置属性值、调用初始化方法等。 4. 注册Bean到容器:创建和初始化的Bean会被注册到Spring容器中,以便其他部分可以通过依赖注入或其他方式获取并使用这些Bean。 5. 执行其他初始化逻辑:除了创建和初始化Bean外,Spring容器还可能执行其他初始化逻辑,如加载资源文件、注册事件监听器、处理AOP代理等。 6. 完成初始化:当所有的Bean都被创建和初始化后,Spring上下文初始化过程完成,容器处于可用状态,可以提供Bean的依赖注入和其他功能。 Spring上下文初始化过程由Spring框架自动完成,开发者无需显式调用。通常,当应用程序启动时,会通过启动类或配置文件指定要使用的Spring上下文,并由框架负责初始化和管理容器。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值