Spring Security的OAuth2 Client功能核心源码分析

简介: 本文对Spring Security的OAuth2 Client部分源码简单讲解

前一个章节我们充分地对Spring Security OAuth2 Client功能进行了扩展开发,从而支持了QQ登录,并同时实现了多个OAuth服务商共存的效果。要想弄明白这些扩展方式背后的原理以及官方的封装思路,就需要对Spring Security的OAuth2 Client部分源码有一定的了解。

当Spring Boot 2.0工程引入Spring Security关于OAuth2 Client的依赖包时,Spring Boot的自动配置策略会在Spring Security的过滤链中插入专门用于处理OAuth2 Client逻辑的过滤器,具体的自动配置逻辑可以参考 org.springframework.boot.autoconfigure.security包下的自动配置类。

当@EnableWebSecurity(debug = true)的debug属性被设置为true时,控制台日志将打印每一条url所经过的过滤器链:

Security filter chain: [
  WebAsyncManagerIntegrationFilter
  SecurityContextPersistenceFilter
  HeaderWriterFilter
  CsrfFilter
  LogoutFilter
  OAuth2AuthorizationRequestRedirectFilter
  OAuth2LoginAuthenticationFilter
  DefaultLoginPageGeneratingFilter
  RequestCacheAwareFilter
  SecurityContextHolderAwareRequestFilter
  AnonymousAuthenticationFilter
  SessionManagementFilter
  ExceptionTranslationFilter
  FilterSecurityInterceptor
]

OAuth2客户端认证流程涉及三个特有的核心过滤器:

  • OAuth2AuthorizationRequestRedirectFilter,通过重定向到authorization-uri端口地址来启动授权码(或隐式授权码)模式流程,获取code。
  • DefaultLoginPageGeneratingFilter,用于生成默认登录页面。
  • OAuth2LoginAuthenticationFilter,核心的OAuth2登录过滤器,首先从url中提取到code,接着使用code获取access_token,借助access_token便可以进一步获取到用户信息,最终构建出OAuth2AuthenticationToken认证对象,表明认证成功。

这三个过滤器是实现OAuth2客户端流程(flow)的核心逻辑入口。

OAuth2AuthorizationRequestRedirectFilter

核心处理逻辑在doFilterInternal方法中:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
    // 如果是发起授权码模式的请求
    if (this.shouldRequestAuthorization(request, response)) {
        try {
            //重定向请求OAuth2服务提供商提供的获取code的接口
            this.sendRedirectForAuthorization(request, response);
        } catch (Exception failed) {
            this.unsuccessfulRedirectForAuthorization(request, response, failed);
        }
        return;
    }

    filterChain.doFilter(request, response);
}

其中sendRedirectForAuthorization方法是发送请求的具体逻辑:

private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException {

    ...
    if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationRequest.getGrantType())) {
        this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
    }
    ...
}

OAuth2AuthorizationRequest对象表示OAuth2的授权请求,OAuth2AuthorizationRequestRedirectFilter把授权请求先缓存在authorizationRequestRepository中,并重定向到authorization-uri,之后的逻辑便转交给了下一个过滤器OAuth2LoginAuthenticationFilter。

OAuth2LoginAuthenticationFilter

从接收code到构建OAuth2AuthenticationToken对象,OAuth2LoginAuthenticationFilter过滤器承载了最核心的OAuth客户端认证逻辑,我们主要关注的是其中的attemptAuthentication方法:

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
        throws AuthenticationException, IOException, ServletException {
    // 如果url路径没有中有code和state参数或者有error参数,说明获取code失败,直接抛出异常
    if (!this.authorizationResponseSuccess(request) && !this.authorizationResponseError(request)) {
        OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
        throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
    }
    // 从authorizationRequestRepository取出在OAuth2AuthorizationRequestRedirectFilter存入的OAuth2AuthorizationRequest对象
    OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.loadAuthorizationRequest(request);
    if (authorizationRequest == null) {
        OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE);
        throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
    }
    this.authorizationRequestRepository.removeAuthorizationRequest(request);

    String registrationId = (String) authorizationRequest.getAdditionalParameters().get(OAuth2ParameterNames.REGISTRATION_ID);
    ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
    if (clientRegistration == null) {
        OAuth2Error oauth2Error = new OAuth2Error(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE,
                "Client Registration not found with Id: " + registrationId, null);
        throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
    }
    // 正式获取code
    OAuth2AuthorizationResponse authorizationResponse = this.convert(request);

    // 构建请求access_token的请求对象
    OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(
            clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
    authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));

    // 具体逻辑实现代码,通过code获取access_token,通过access_token获取用户信息,构建OAuth2LoginAuthenticationToken认证对象
    OAuth2LoginAuthenticationToken authenticationResult =
        (OAuth2LoginAuthenticationToken) this.getAuthenticationManager().authenticate(authenticationRequest);

    // 将OAuth2LoginAuthenticationToken转化为OAuth2AuthenticationToken认证对象,融入Spring Security上下文逻辑
    OAuth2AuthenticationToken oauth2Authentication = new OAuth2AuthenticationToken(
        authenticationResult.getPrincipal(),
        authenticationResult.getAuthorities(),
        authenticationResult.getClientRegistration().getRegistrationId());

    OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
        authenticationResult.getClientRegistration(),
        oauth2Authentication.getName(),
        authenticationResult.getAccessToken());

    this.authorizedClientService.saveAuthorizedClient(authorizedClient, oauth2Authentication);

    return oauth2Authentication;
}

代码流程本身很清晰,但需要注意的是,使用code交换access_token以及通过access_token获取用户信息的逻辑并不在此处实现,在代码中可以看得出来:

OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this.getAuthenticationManager().authenticate(authenticationRequest);

getAuthenticationManager()获取到的就是我们所熟悉的Spring Security认证管理器,它通过适配器模式调用多个Provider对象进行认证,其中也包含了OAuth2认证逻辑所对应的Provider:OAuth2LoginAuthenticationProvider。

DefaultLoginPageGeneratingFilter

当我们没有配置自定义的登录页时,自动配置机制会将DefaultLoginPageGeneratingFilter插入到Spring Security过滤器链中,并在合适的时机为我们生成一个默认的登录视图:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        boolean loginError = isErrorPage(request);
        boolean logoutSuccess = isLogoutSuccess(request);
        if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
            String loginPageHtml = generateLoginPageHtml(request, loginError,
                    logoutSuccess);
            response.setContentType("text/html;charset=UTF-8");
            response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
            response.getWriter().write(loginPageHtml);

            return;
        }

        chain.doFilter(request, response);
    }

OAuth2LoginAuthenticationProvider

OAuth2LoginAuthenticationProvider主要实现了通过code交换access_token以及通过access_token获取用户信息两个核心逻辑,主要代码在authenticate方法中:

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    OAuth2LoginAuthenticationToken authorizationCodeAuthentication =
        (OAuth2LoginAuthenticationToken) authentication;

    ...

    // 验证获取到的授权码是否合法
    OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication
        .getAuthorizationExchange().getAuthorizationRequest();
    OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication
        .getAuthorizationExchange().getAuthorizationResponse();

    if (authorizationResponse.statusError()) {
        throw new OAuth2AuthenticationException(
            authorizationResponse.getError(), authorizationResponse.getError().toString());
    }

    if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
        OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
        throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
    }

    if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) {
        OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE);
        throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
    }

    // 如果授权码合法,则请求access_token
    OAuth2AccessTokenResponse accessTokenResponse =
        this.accessTokenResponseClient.getTokenResponse(
            new OAuth2AuthorizationCodeGrantRequest(
                authorizationCodeAuthentication.getClientRegistration(),
                authorizationCodeAuthentication.getAuthorizationExchange()));

    OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken();

    // 通过accesss_token请求用户信息
    OAuth2User oauth2User = this.userService.loadUser(
        new OAuth2UserRequest(authorizationCodeAuthentication.getClientRegistration(), accessToken));
    ...
}
目录
相关文章
|
12月前
|
JSON 安全 Java
什么是JWT?如何使用Spring Boot Security实现它?
什么是JWT?如何使用Spring Boot Security实现它?
1967 5
|
2月前
|
XML Java 应用服务中间件
【SpringBoot(一)】Spring的认知、容器功能讲解与自动装配原理的入门,带你熟悉Springboot中基本的注解使用
SpringBoot专栏开篇第一章,讲述认识SpringBoot、Bean容器功能的讲解、自动装配原理的入门,还有其他常用的Springboot注解!如果想要了解SpringBoot,那么就进来看看吧!
371 2
|
7月前
|
消息中间件 缓存 NoSQL
基于Spring Data Redis与RabbitMQ实现字符串缓存和计数功能(数据同步)
总的来说,借助Spring Data Redis和RabbitMQ,我们可以轻松实现字符串缓存和计数的功能。而关键的部分不过是一些"厨房的套路",一旦你掌握了这些套路,那么你就像厨师一样可以准备出一道道饕餮美食了。通过这种方式促进数据处理效率无疑将大大提高我们的生产力。
240 32
|
12月前
|
XML 安全 Java
|
监控 Java 应用服务中间件
Spring Boot整合Tomcat底层源码分析
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置和起步依赖等特性,大大简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是其与Tomcat的整合。
320 1
|
7月前
|
安全 Java API
Spring Boot 功能模块全解析:构建现代Java应用的技术图谱
Spring Boot不是一个单一的工具,而是一个由众多功能模块组成的生态系统。这些模块可以根据应用需求灵活组合,构建从简单的REST API到复杂的微服务系统,再到现代的AI驱动应用。
|
6月前
|
监控 安全 Java
Java 开发中基于 Spring Boot 3.2 框架集成 MQTT 5.0 协议实现消息推送与订阅功能的技术方案解析
本文介绍基于Spring Boot 3.2集成MQTT 5.0的消息推送与订阅技术方案,涵盖核心技术栈选型(Spring Boot、Eclipse Paho、HiveMQ)、项目搭建与配置、消息发布与订阅服务实现,以及在智能家居控制系统中的应用实例。同时,详细探讨了安全增强(TLS/SSL)、性能优化(异步处理与背压控制)、测试监控及生产环境部署方案,为构建高可用、高性能的消息通信系统提供全面指导。附资源下载链接:[https://pan.quark.cn/s/14fcf913bae6](https://pan.quark.cn/s/14fcf913bae6)。
971 0
|
Java 开发者 微服务
手写模拟Spring Boot自动配置功能
【11月更文挑战第19天】随着微服务架构的兴起,Spring Boot作为一种快速开发框架,因其简化了Spring应用的初始搭建和开发过程,受到了广大开发者的青睐。自动配置作为Spring Boot的核心特性之一,大大减少了手动配置的工作量,提高了开发效率。
235 0
|
8月前
|
SQL 前端开发 Java
深入理解 Spring Boot 项目中的分页与排序功能
本文深入讲解了在Spring Boot项目中实现分页与排序功能的完整流程。通过实际案例,从Service层接口设计到Mapper层SQL动态生成,再到Controller层参数传递及前端页面交互,逐一剖析每个环节的核心逻辑与实现细节。重点包括分页计算、排序参数校验、动态SQL处理以及前后端联动,确保数据展示高效且安全。适合希望掌握分页排序实现原理的开发者参考学习。
500 4