SpringSecurity Oauth2实战 - 06 获取用户登录信息并存储到本地线程
创始人
2024-04-04 13:59:09
0

文章目录

    • 1. 获取用户登录信息
      • 1. 用户信息共享的ThreadLocal类 UserInfoShareHolder
      • 2. 写一个拦截器 UserInfoInterceptor
      • 3. 配置拦截器 CommonWebMvcAutoConfiguration
    • 2. 源码分析
      • 1. 认证用户通过access_token访问受限资源
      • 2. 进入过滤器 OAuth2AuthenticationProcessingFilter#doFilter方法
      • 3. 进入拦截器 UserInfoInterceptor#preHandle方法
      • 4. 进入HttpServletRequest#getUserPrincipal方法
      • 5.进入SecurityContextHolderAwareRequestWrapper#getUserPrincipal方法
      • 6. 进入控制器 HelloController#hello方法
    • 3. 源码分析
      • 1. 未认证用户获取access_token
      • 2. 进入过滤器 OAuth2AuthenticationProcessingFilter#doFilter方法
      • 3. 进入拦截器 UserInfoInterceptor#preHandle 方法
      • 4. 进入 HttpServlet3RequestFactory#authenticate方
    • 4. 源码分析

上一讲已经我们分析了/oauth/token认证流程,明白了整个认证过程核心做了哪些事情,这一讲看一下如何配置拦截器判断用户是否登录并获取用户登录信息,同时将获取的登录信息存储到本地线程中,主要分为两点展开说明:

  • 首先,获取用户登录信息并存储到本地线程功能实现;
  • 其次,debug断点分析整个源码流程;

1. 获取用户登录信息

1. 用户信息共享的ThreadLocal类 UserInfoShareHolder

/*** 用户信息共享的ThreadLocal类*/
public class UserInfoShareHolder {private static final ThreadLocal USER_INFO_THREAD_LOCAL = new TransmittableThreadLocal<>();/*** 存储用户信息*/public static void setUserInfo(UserInfo userInfo) {USER_INFO_THREAD_LOCAL.set(userInfo);}/*** 获取用户相关信息*/public static UserInfo getUserInfo() {return USER_INFO_THREAD_LOCAL.get();}/*** 清除ThreadLocal信息*/public static void remove() {USER_INFO_THREAD_LOCAL.remove();}
}

2. 写一个拦截器 UserInfoInterceptor

/*** 拦截器:用户信息本地线程存储*/
public class UserInfoInterceptor extends HandlerInterceptorAdapter {/*** 拦截所有请求,在Controller层方法之前调用*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 判断用户是否被认证,如果没有认证不放行 boolean isAuthenticated = request.authenticate(response);if (!isAuthenticated) {return false;}// 存储用户信息到本地线程Principal userPrincipal = request.getUserPrincipal();OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) userPrincipal;AuthUser ngsocUser = (AuthUser) oAuth2Authentication.getUserAuthentication().getPrincipal();UserInfo userInfo = ngsocUser.getUserInfo();UserInfoShareHolder.setUserInfo(userInfo);// 放行,继续执行Controller层的方法return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserInfoShareHolder.remove();super.afterCompletion(request, response, handler, ex);}
}

3. 配置拦截器 CommonWebMvcAutoConfiguration

/*** 配置拦截器*/
@Configuration
@EnableWebMvc
public class CommonWebMvcAutoConfiguration implements WebMvcConfigurer {@Beanpublic UserInfoInterceptor userInfoInterceptor() {return new UserInfoInterceptor();}@Overridepublic void addInterceptors(@NonNull InterceptorRegistry registry) {// 注意:这里的excludePath不需要自己再加上contextPath, spring会自动加// 添加存储用户信息的拦截器,配置拦截请求路径// 拦截器会拦截所有请求,需要配置放行的请求registry.addInterceptor(userInfoInterceptor())// 放行的请求.excludePathPatterns("/api/v1/login");}
}

2. 源码分析

1. 认证用户通过access_token访问受限资源

@RestController
@RequestMapping("/api/v1")
public class HelloController {@GetMapping("/hello")public String hello(HttpServletRequest request){String username = UserInfoShareHolder.getUserInfo().getUsername();return username;}
}

在这里插入图片描述

在这里插入图片描述

2. 进入过滤器 OAuth2AuthenticationProcessingFilter#doFilter方法

当一个认证用户访问系统的受限资源时,请求首先被OAuth2AuthenticationProcessingFilter过滤器拦截,在该过滤器的doFilter方法中主要做了以下事情:

  • 从请求中提取 token 并获取待认证的Authentication 对象: Authentication authentication = tokenExtractor.extract(request);
  • request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
  • 通过待认证的Authentication对象倒TokenStore中获取完成的Authentication对象:Authentication authResult = authenticationManager.authenticate(authentication);
  • 发布认证成功的事件通知:eventPublisher.publishAuthenticationSuccess(authResult);
  • SecurityContextHolder.getContext().setAuthentication(authResult);
  • 进行过滤器链中的下一个过滤器;

所以在请求一开始就把完成的认证对象的放在SecurityContextHolder.getContext()中,我们就可以从SecurityContextHolder.getContext()中获取Authentication对象了。

在这里插入图片描述

3. 进入拦截器 UserInfoInterceptor#preHandle方法

经过SpringSecurity的一系列过滤器链后,会进入UserInfoInterceptor#preHandle方法

在这里插入图片描述

4. 进入HttpServletRequest#getUserPrincipal方法

public interface HttpServletRequest extends ServletRequest {Principal getUserPrincipal();
}

在这里插入图片描述

5.进入SecurityContextHolderAwareRequestWrapper#getUserPrincipal方法

public class SecurityContextHolderAwareRequestWrapper extends HttpServletRequestWrapper {private Authentication getAuthentication() {//从SecurityContextHolder.getContext()中Authentication对象Authentication auth = SecurityContextHolder.getContext().getAuthentication();if (!trustResolver.isAnonymous(auth)) {return auth;}return null;}@Overridepublic Principal getUserPrincipal() {Authentication auth = getAuthentication();if ((auth == null) || (auth.getPrincipal() == null)) {return null;}return auth;}
}

在这里插入图片描述

6. 进入控制器 HelloController#hello方法

@RestController
@RequestMapping("/api/v1")
public class HelloController {@GetMapping("/hello")public String hello(HttpServletRequest request){String username = UserInfoShareHolder.getUserInfo().getUsername();return username;}
}

在这里插入图片描述

3. 源码分析

假如我们在配置拦截器时,拦截所有请求,不放行/api/v1/login请求会如何?

/*** 配置拦截器*/
@Configuration
@EnableWebMvc
public class CommonWebMvcAutoConfiguration implements WebMvcConfigurer {@Beanpublic UserInfoInterceptor userInfoInterceptor() {return new UserInfoInterceptor();}@Overridepublic void addInterceptors(@NonNull InterceptorRegistry registry) {// 注意:这里的excludePath不需要自己再加上contextPath, spring会自动加// 拦截器会拦截所有请求,需要配置放行的请求registry.addInterceptor(userInfoInterceptor());}
}

1. 未认证用户获取access_token

注意:资源服务器配置类 ResourceServerAutoConfiguration 中已经配置了该请求接口不需要认证就能访问

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerAutoConfiguration extends ResourceServerConfigurerAdapter {@Autowiredprivate TokenStore tokenStore;@Value("${spring.application.name}")private String appName;@Overridepublic void configure(ResourceServerSecurityConfigurer resources) {resources.resourceId(appName);resources.tokenStore(tokenStore);}@Overridepublic void configure(HttpSecurity http) throws Exception {http.authorizeRequests()// 配置不需要认证就可以访问的请求.antMatchers("/api/v1/login").permitAll()// 其他请求必须认证才能访问.anyRequest().authenticated().and().csrf().disable();}
}

在这里插入图片描述

2. 进入过滤器 OAuth2AuthenticationProcessingFilter#doFilter方法

在这里插入图片描述

在这里插入图片描述

3. 进入拦截器 UserInfoInterceptor#preHandle 方法

在这里插入图片描述

结论:资源服务器配置类 ResourceServerAutoConfiguration 中已经配置了该请求接口不需要认证就能访问,但是拦截器UserInfoInterceptor#preHandle 方法会在AuthController#authority方法执行之前执行,在该拦截器中从请求中获取Authentication对象判断用户是否认证,如果用户未认证则不放行,因此就不会执行AuthController#authority方法,所以拦截器必须配置不拦截的请求路径;

4. 进入 HttpServlet3RequestFactory#authenticate方

final class HttpServlet3RequestFactory implements HttpServletRequestFactory {@Overridepublic boolean authenticate(HttpServletResponse response)
throws IOException, ServletException {AuthenticationEntryPoint entryPoint = HttpServlet3RequestFactory.this.authenticationEntryPoint;if (entryPoint == null) {return super.authenticate(response);}if (isAuthenticated()) {return true;}// 异常处理entryPoint.commence(this, response,new AuthenticationCredentialsNotFoundException( "User is not Authenticated"));return false;}}
}

在这里插入图片描述

4. 源码分析

假如我们在配置拦截器时放行了/api/v1/login请求,但是资源服务器中没有放行/api/v1/login请求的认证会如何?

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerAutoConfiguration extends ResourceServerConfigurerAdapter {@Autowiredprivate TokenStore tokenStore;@Value("${spring.application.name}")private String appName;@Overridepublic void configure(ResourceServerSecurityConfigurer resources) {resources.resourceId(appName);resources.tokenStore(tokenStore);}@Overridepublic void configure(HttpSecurity http) throws Exception {http.authorizeRequests()// 所有请求必须认证才能访问.anyRequest().authenticated().and().csrf().disable();}
}

在这里插入图片描述

经过debug发现,请求首先会进入过滤器OAuth2AuthenticationProcessingFilter#doFilter方法,然后判断用户未登录,但是最终不会进入AuthController#authority方法。

相关内容

热门资讯

前端-session、jwt 目录:   (1)session (2&#x...
linux入门---制作进度条 了解缓冲区 我们首先来看看下面的操作: 我们首先创建了一个文件并在这个文件里面添加了...
关于测试,我发现了哪些新大陆 关于测试 平常也只是听说过一些关于测试的术语,但并没有使用过测试工具。偶然看到编程老师...
前缀和与对数器与二分法 1. 前缀和 假设有一个数组,我们想大量频繁的去访问L到R这个区间的和,...
nodejs:本地安装nvm实... 一、背景-使用不同版本node的原因 vue3+ts、nuxt3版本,node...
JAVA集合知识整理 Java集合知识整理 HashMap相关 HashMap的底层数据结构:jdk1.8之...
无刷直流电机介绍及单片机控制实... 无刷直流电机介绍及单片机控制实例前言基本概念优势与劣势使用寿命基本结构使用单片机控制实例电子调速器&...
fwdiary(2) dp2 1.传纸条  AcWing 275. 传纸条 - AcWing 走两条路,走一条最大的...
常用的DOS命令 常用的DOS命令 DOS(Disk Operating System,磁...
<C++> 类和对象(下) 1.const成员函数将const修饰的“成员函数”称之为const成员函数,cons...