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));
    ...
}
目录
相关文章
|
22天前
|
JSON 安全 Java
什么是JWT?如何使用Spring Boot Security实现它?
什么是JWT?如何使用Spring Boot Security实现它?
81 5
|
21天前
|
XML 安全 Java
|
1月前
|
监控 Java 应用服务中间件
Spring Boot整合Tomcat底层源码分析
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置和起步依赖等特性,大大简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是其与Tomcat的整合。
57 1
|
1月前
|
Java 开发者 微服务
手写模拟Spring Boot自动配置功能
【11月更文挑战第19天】随着微服务架构的兴起,Spring Boot作为一种快速开发框架,因其简化了Spring应用的初始搭建和开发过程,受到了广大开发者的青睐。自动配置作为Spring Boot的核心特性之一,大大减少了手动配置的工作量,提高了开发效率。
53 0
|
2月前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
59 4
|
2月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,包括版本兼容性、安全性、性能调优等方面。
178 1
|
2月前
|
Java API 数据库
Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐
本文通过在线图书管理系统案例,详细介绍如何使用Spring Boot构建RESTful API。从项目基础环境搭建、实体类与数据访问层定义,到业务逻辑实现和控制器编写,逐步展示了Spring Boot的简洁配置和强大功能。最后,通过Postman测试API,并介绍了如何添加安全性和异常处理,确保API的稳定性和安全性。
45 0
|
1月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,创建并配置 Spring Boot 项目,实现后端 API;然后,使用 Ant Design Pro Vue 创建前端项目,配置动态路由和菜单。通过具体案例,展示了如何快速搭建高效、易维护的项目框架。
116 62
|
21天前
|
XML Java 数据格式
Spring Core核心类库的功能与应用实践分析
【12月更文挑战第1天】大家好,今天我们来聊聊Spring Core这个强大的核心类库。Spring Core作为Spring框架的基础,提供了控制反转(IOC)和依赖注入(DI)等核心功能,以及企业级功能,如JNDI和定时任务等。通过本文,我们将从概述、功能点、背景、业务点、底层原理等多个方面深入剖析Spring Core,并通过多个Java示例展示其应用实践,同时指出对应实践的优缺点。
47 14
|
1月前
|
消息中间件 缓存 Java
手写模拟Spring Boot启动过程功能
【11月更文挑战第19天】Spring Boot自推出以来,因其简化了Spring应用的初始搭建和开发过程,迅速成为Java企业级应用开发的首选框架之一。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,帮助读者深入理解其工作机制。
43 3