• SpringSecurity Oauth2实战 - 06 拦截器获取用户登录信息并存储到本地线程ThreadLocal



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

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

    1. 获取用户登录信息

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

    /**
     * 用户信息共享的ThreadLocal类
     */
    public class UserInfoShareHolder {
    
        private static final ThreadLocal<UserInfo> 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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    2. 写一个拦截器 UserInfoInterceptor

    /**
     * 拦截器:用户信息本地线程存储
     */
    public class UserInfoInterceptor extends HandlerInterceptorAdapter {
        /**
         * 拦截所有请求,在Controller层方法之前调用
         */
        @Override
        public 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;
        }
        
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            UserInfoShareHolder.remove();
            super.afterCompletion(request, response, handler, ex);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    3. 配置拦截器 CommonWebMvcAutoConfiguration

    /**
     * 配置拦截器
     */
    @Configuration
    @EnableWebMvc
    public class CommonWebMvcAutoConfiguration implements WebMvcConfigurer {
    
        @Bean
        public UserInfoInterceptor userInfoInterceptor() {
            return new UserInfoInterceptor();
        }
    
        @Override
        public void addInterceptors(@NonNull InterceptorRegistry registry) {
            // 注意:这里的excludePath不需要自己再加上contextPath, spring会自动加
            // 添加存储用户信息的拦截器,配置拦截请求路径
            // 拦截器会拦截所有请求,需要配置放行的请求
            registry.addInterceptor(userInfoInterceptor())
                    // 放行的请求
                    .excludePathPatterns("/api/v1/login");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述

    在这里插入图片描述

    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();
    }
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    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;
    	}
    
    	@Override
    	public Principal getUserPrincipal() {
    		Authentication auth = getAuthentication();
    		if ((auth == null) || (auth.getPrincipal() == null)) {
    			return null;
    		}
    		return auth;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述

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

    @RestController
    @RequestMapping("/api/v1")
    public class HelloController {
    
        @GetMapping("/hello")
        public String hello(HttpServletRequest request){
            String username = UserInfoShareHolder.getUserInfo().getUsername();
            return username;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述

    3. 源码分析

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

    /**
     * 配置拦截器
     */
    @Configuration
    @EnableWebMvc
    public class CommonWebMvcAutoConfiguration implements WebMvcConfigurer {
    
        @Bean
        public UserInfoInterceptor userInfoInterceptor() {
            return new UserInfoInterceptor();
        }
    
        @Override
        public void addInterceptors(@NonNull InterceptorRegistry registry) {
            // 注意:这里的excludePath不需要自己再加上contextPath, spring会自动加
            // 拦截器会拦截所有请求,需要配置放行的请求
            registry.addInterceptor(userInfoInterceptor());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    1. 未认证用户获取access_token

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

    @Configuration
    @EnableResourceServer
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class ResourceServerAutoConfiguration extends ResourceServerConfigurerAdapter {
        @Autowired
        private TokenStore tokenStore;
    
        @Value("${spring.application.name}")
        private String appName;
    
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) {
            resources.resourceId(appName);
            resources.tokenStore(tokenStore);
        }
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                // 配置不需要认证就可以访问的请求
                .antMatchers("/api/v1/login").permitAll()
                // 其他请求必须认证才能访问
                .anyRequest().authenticated()
                .and()
                .csrf().disable();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    在这里插入图片描述

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

    在这里插入图片描述

    在这里插入图片描述

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

    在这里插入图片描述

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

    4. 进入 HttpServlet3RequestFactory#authenticate方

    final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
          @Override
          public 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;
          }
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在这里插入图片描述

    4. 源码分析

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

    @Configuration
    @EnableResourceServer
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class ResourceServerAutoConfiguration extends ResourceServerConfigurerAdapter {
        @Autowired
        private TokenStore tokenStore;
    
        @Value("${spring.application.name}")
        private String appName;
    
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) {
            resources.resourceId(appName);
            resources.tokenStore(tokenStore);
        }
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                // 所有请求必须认证才能访问
                .anyRequest().authenticated()
                .and()
                .csrf().disable();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在这里插入图片描述

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

  • 相关阅读:
    Spring Framework6.0 发布了GA版,期待已久的新特性功能一览
    app逆向(8)|app的加固+脱壳和frida+rpc介绍
    【Flutter】IOS运行工程二次启动崩溃问题。
    关于Vue的生命周期
    实现一个todoList可直接操作数据(上移、下移、置顶、置底)
    一言成文大模型:大模型实践之路
    golang中的信号量的实现原理
    nginx配置IP白名单
    HarmonyOS应用开发-网络请求与web组件
    LeetCode每日一题(2181. Merge Nodes in Between Zeros)
  • 原文地址:https://blog.csdn.net/qq_42764468/article/details/127718048