返回介绍

15.4 GitHub 授权登录

发布于 2025-04-26 13:16:50 字数 16793 浏览 0 评论 0 收藏

我们通过一个 GitHub 授权登录来体验下 OAuth2 认证流程。

15.4.1 准备工作

首先我们需要将第三方应用的信息注册到 GitHub 上,打开 https://github.com/settings/developers 链接,单击 New OAuth App,注册一个新的应用,如图 15-6 所示。

图 15-6 单击 New OAuth App 注册一个新的应用

在打开的页面中,填入应用的基本信息:

- Application name:应用名称。

- Homepage URL:项目主页面。

- Application description:项目描述信息(可选)。

- Authorization callback URL:认证成功后的回调页面,默认的回调 URI 地址模板为{baseUrl}/login/oauth2/code/{registrationId},其中 registrationId 是 ClientRegistration 的唯一标识符,如果这里使用了默认的回调地址,则在接下来的 Spring Boot 项目中就不必提供回调接口了。

如图 15-7 所示,信息填完之后,单击下方的 Register application 按钮完成注册。

图 15-7 注册一个应用

注册成功后,会获取到一个 Client ID 和一个 Client Secret,如图 15-8 所示。

图 15-8 Client ID 和 Client Secret

保存好 Client ID 和 Client Secret,在接下来的项目中我们会用到这两个参数。

准备工作就算完成了。

15.4.2 项目开发

创建一个 Spring Boot 项目,引入 Web、Spring Security 以及 OAuth2 Client 依赖,如图 15-9 所示。

图 15-9 创建项目时添加三个依赖

项目创建成功后,在 application.properties 文件中配置刚刚申请到的 Client ID 和 Client Secret,代码如下:

    spring.security.oauth2.client.registration.github.client-id=aa9e79846df9
    cbc6201f
    spring.security.oauth2.client.registration.github.client-secret=c324b934
    43594fe84d106bb32c904799e1839e6a

接下来提供一个测试接口:

    @RestController
    public class HelloController {
       @GetMapping("/hello")
       public DefaultOAuth2User hello() {
           return ((DefaultOAuth2User) SecurityContextHolder.getContext()
                                             .getAuthentication().getPrincipal());
       }
    }

在测试接口中获取当前登录用户信息。注意,此时的用户对象是 DefaultOAuth2User,将获取到的当前登录用户对象返回。

最后我们再来简单配置一下 Spring Security:

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
       @Override
       protected void configure(HttpSecurity http) throws Exception {
           http.authorizeRequests()
                   .anyRequest().authenticated()
                   .and()
                   .oauth2Login();
       }
    }

这段配置也非常简单,所有接口都需要认证后才能访问,同时调用 oauth2Login() 方法开启 OAuth2 登录。

现在我们的项目就开发完成了。

15.4.3 测试

启动 Spring Boot 项目,访问 http://localhost:8080/hello 地址,由于该地址需要认证后才能访问,此时服务端会返回 302,要求浏览器重定向到 http://localhost:8080/oauth2/authorization/github 页面。不同的第三方登录只是地址的最后一项不同,如果是 Google 第三方登录,则登录页面是/oauth2/authorization/google。

当浏览器去请求该页面时,服务端检测到这是一个授权请求,于是再次返回 302,要求浏览器重定向到 GitHub 授权页面 https://github.com/login/oauth/authorize?response_type=code&client_id=aa9e79846df9cbc6201f&scope=read:user&state=vY8C JuRg2WlROVyo4mBZUn__1ksl6ieBjkmOQyGYA0A%3D&red irect_uri=http://localhost:8080/login/oauth2/code/github,这个 URL 地址的参数比较多,但都是我们前面 15.2.1 小节中介绍的授权码模式中的参数,因此这里不做过多解释。

接下来 GitHub 的授权服务器还会再次要求重定向,但是这就和我们这里的 OAuth2 没有关系了,最终来到 GitHub 认证页面,如图 15-10 所示。

图 15-10 GitHub 认证页面

输入用户名/密码,完成认证后,GitHub 授权服务器又会要求浏览器重定向到提前配置好的 Authorization callback URL 地址上,同时还会携带一个授权码参数 http://localhost:8080/login/oauth2/code/github?code=b15a5ed5198650e47e6a&state=cRYcu1Xg3E0sdlJAlbbi1CNeJT5-PdBOYVEaUkxcF 8g%3D。

当浏览器请求该地址时,客户端会根据这里的授权码 code,向 GitHub 授权服务器的 https://github.com/login/oauth/access_token 接口去请求 Access Token,拿到 Access Token 之后,再向 https://api.github.com/user 地址发送请求,获取用户信息。

前面几步都是浏览器中可见的,最后获取令牌 Access Token 和获取用户信息的过程,是在后端完成的,浏览器将不可见。

所有工作都完成后,最终会自动跳转回 http://localhost:8080/hello 页面,在该页面可以看到用户登录成功后的信息,如图 15-11 所示。

图 15-11 认证成功后获取到的用户信息

15.4.4 原理分析

可以看到,接入 GitHub 第三方登录整个过程非常顺畅,开发者几乎不需要做什么事情,GitHub 上注册应用,项目中配置一下 Client ID 和 Client Secret,然后再开启一下 OAuth2 登录就可以了。

那么 Spring Security 如何得知 GitHub 授权地址、用户接口、令牌接口等信息?

由于用户接口、令牌接口、授权地址等信息一般不会轻易变化,所以 Spring Security 将一些常用的第三方登录如 Google、GitHub、Facebook、Okta 的信息收集起来,保存在一个枚举类 CommonOAuth2Provider 中,当我们在 application.properties 中配置 GitHub 时,就会自动选择枚举类中的 GITHUB。我们来看一下 CommonOAuth2Provider 中关于 GITHUB 信息的定义:

    GITHUB {
       @Override
       public Builder getBuilder(String registrationId) {
           ClientRegistration.Builder builder = getBuilder(registrationId,
                   ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);
           builder.scope("read:user");
           builder.authorizationUri("https://github.com/login/oauth/authorize");
           builder.tokenUri("https://github.com/login/oauth/access_token");
           builder.userInfoUri("https://api.github.com/user");
           builder.userNameAttributeName("id");
           builder.clientName("GitHub");
           return builder;
       }
    },

可以看到,需要用到的地址都提前定义好了。

当我们开启 OAuth2 自动登录之后,在 Spring Security 过滤器链中多了两个过滤器:

(1)OAuth2AuthorizationRequestRedirectFilter

(2)OAuth2LoginAuthenticationFilter

回顾一下 15.2.1 小节所讲的授权码模式工作流程,当用户在没有登录时就去访问 http://localhost:8080/hello 地址,会被自动导入到 GitHub 授权页面,这个过程是由 OAuth2AuthorizationRequestRedirectFilter 过滤器完成的。

接下来用户进行 GitHub 登录,登录成功后,GitHub 授权服务器会调用回调地址,同时返回一个授权码,客户端再根据授权码去 GitHub 授权服务器上获取 Access Token,有了 Access Token 就可以获取用户信息了,这个过程是由 OAuth2LoginAuthenticationFilter 过滤器来完成的。

接下来我们对这里涉及的几个关键类进行简单分析。

OAuth2ClientRegistrationRepositoryConfiguration

OAuth2ClientRegistrationRepositoryConfiguration 是一个配置类,当项目启动时,该类会自动加载,并向 Spring 容器中注册一个 InMemoryClientRegistrationRepository 实例,该实例保存了客户端注册表信息,代码如下:

    @Configuration(proxyBeanMethods = false)
    @EnableConfigurationProperties(OAuth2ClientProperties.class)
    @Conditional(ClientsConfiguredCondition.class)
    class OAuth2ClientRegistrationRepositoryConfiguration {
       @Bean
       @ConditionalOnMissingBean(ClientRegistrationRepository.class)
       InMemoryClientRegistrationRepository
            clientRegistrationRepository(OAuth2ClientProperties properties) {
          List<ClientRegistration> registrations = new ArrayList<>(
                  OAuth2ClientPropertiesRegistrationAdapter
                                 .getClientRegistrations(properties).values());
          return new InMemoryClientRegistrationRepository(registrations);
       }
    }

可以看到,clientRegistrationRepository 方法的参数实际上就是我们在 application.properties 中配置的 GitHub 的 Client ID 和 Client Secret。接下来调用 getClientRegistrations 方法,会将 CommonOAuth2Provider 枚举类中预设的 GitHub 信息和用户配置的 GitHub 信息合并然后返回。如果 application.properties 中只是配置了 GitHub 信息,则这里的 registrations 集合中就只有一项;如果 application.properties 中还配置了 Facebook、Google 等信息,则 registrations 集合中就包含多项。

OAuth2AuthorizationRequestRedirectFilter

OAuth2AuthorizationRequestRedirectFilter 过滤器主要是判断当前请求是否是授权请求,如果是授权请求,则进行重定向到 GitHub 授权页面,否则执行下一个过滤器。

我们来看一下该过滤器的 doFilterInternal 方法:

首先调用 authorizationRequestResolver.resolve 方法将当前请求解析为一个 OAuth2AuthorizationRequest 对象:如果当前请求是授权请求(如 http://localhost:8080/oauth2/authorization/github),则根据 InMemoryClientRegistrationRepository 中保存的客户端注册表信息,构造一个 OAuth2AuthorizationRequest 对象并返回;如果当前请求不是授权请求,而是一个普通请求,则这里返回的 OAuth2AuthorizationRequest 对象为 null。

如果获取到的 authorizationRequest 对象不为 null,即当前请求是授权请求,则调用 sendRedirectForAuthorization 方法进行重定向,重定向的地址就是 GitHub 的授权地址(即枚举类 CommonOAuth2Provider 中 authorizationUri 方法所配置的地址)。当然这里的地址会在该地址上再自动加上 response_type、client_id、scope、state 以及 redirect_uri 参数(这些参数都可以从枚举类中获取)。另外,在重定向之前,还会将当前授权请求保存到一个 Map 集合中,并将 Map 集合保存到 HttpSession 中,以备后续使用。

OAuth2LoginAuthenticationFilter

通过前面的讲解,可能有读者会疑惑,GitHub 授权服务器登录成功后的回调地址是 http://localhost:8080/login/oauth2/code/github,但是我们的项目中并没有定义这样一个接口,为什么还能调用成功呢?这就是 OAuth2LoginAuthenticationFilter 过滤器所起的作用了!

OAuth2LoginAuthenticationFilter 继承自 AbstractAuthenticationProcessingFilter,它目前的角色相当于我们之前所讲的 UsernamePasswordAuthenticationFilter 过滤器的角色。在 AbstractAuthenticationProcessingFilter 过滤器中会拦截下认证请求进行处理。我们来看一下 AbstractAuthenticationProcessingFilter#doFilter 方法:

    public void doFilter(ServletRequest req, ServletResponse res,
                      FilterChain chain)throws IOException, ServletException {
    //省略
       if (!requiresAuthentication(request, response)) {
           chain.doFilter(request, response);
           return;
       }
    //省略
    }

如果使用了 OAuth2 登录,这里的逻辑就是判断当前请求接口是否是/login/oauth2/code/*格式,如果是,说明这是一个认证请求,将该请求拦截下来交给 OAuth2LoginAuthenticationFilter#attemptAuthentication 方法去处理;如果不是,则继续执行下一个过滤器。

我们来看一下 OAuth2LoginAuthenticationFilter#attemptAuthentication 方法:

注释 1 是对请求参数进行校验,请求必须包含授权码 code 和 state 两个参数,否则会抛出异常。

注释 2 是从 HttpSession 中获取在 OAuth2AuthorizationRequestRedirectFilter 过滤器中保存的授权请求,如果获取到的对象为 null,则抛出异常。

注释 3 是检查当前注册应用中是否有授权请求时的应用,如果没有,则抛出异常。

注释 4 是构造一个未经认证的 OAuth2LoginAuthenticationToken 对象,并调用 authenticate 方法进行认证。认证成功后,最终封装成一个 OAuth2AuthenticationToken 对象并返回。这一块读者可以回顾本书 3.1.4 小节的分析,流程都是一样的,只是实现细节有所差异而已。需要注意的是,这里认证时调用的 AuthenticationProvider 是 OAuth2LoginAuthenticationProvider。

OAuth2LoginAuthenticationProvider

OAuth2LoginAuthenticationProvider 负责最终的校验工作,作用类似 3.1.2 小节所讲的 DaoAuthenticationProvider,我们来看一下它的 authenticate 方法:

注释 1 是判断是否为 OpenID Connect 认证,如果是,则返回 null,请求交给 OidcAuthorizationCodeAuthenticationProvider 去处理。

注释 2 是根据授权码 code 去请求 https://github.com/login/oauth/access_token 接口获取 Access Token,这一步调用到了 OAuth2AuthorizationCodeAuthenticationProvider#authenticate 方法,并在该方法中调用 DefaultAuthorizationCodeTokenResponseClient#getTokenResponse 方法发起网络请求,底层使用的网络请求工具是 RestTemplate。

注释 3 是根据上一步的结果,提取出 accessToken 对象。

注释 4 是根据获取到的 accessToken 对象,向 https://api.github.com/user 地址发起请求,获取用户信息,并最终封装为一个 OAuth2User 对象。

注释 5 是构造一个 OAuth2LoginAuthenticationToken 对象并返回。

我们在 GitHub 上配置的 http://localhost:8080/login/oauth2/ code/github 地址其实类似于登录请求,当 GitHub 授权服务器重定向到该地址时,重定向请求携带了授权码参数,客户端根据授权码获取 Access Token,再根据 Access Token 加载到用户对象,最终构建 OAuth2LoginAuthenticationToken 并返回。

至此,整个 GitHub 授权登录就分析完了,我们再结合 15.2.1 小节中介绍的授权码模式的工作流程,应该就很好理解了。

15.4.5 自定义配置

15.4.5.1 自定义 ClientRegistrationRepository

完全使用自动化配置虽然方便,但是灵活性却降低了。假如我们在 GitHub 上注册 App 时,填写的回调地址不是 http://localhost:8080/login/oauth2/code/github,而是其他地址,此时就需要我们手动配置了。

举个简单例子,假设我们在 GitHub 上注册 App 时填写的回调地址是 http://localhost:8080/authorization_code,那么可以通过如下方式配置客户端。

在 application.properties 文件中修改重定向地址:

    spring.security.oauth2.client.registration.github.client-id=aa9e79846df9
    cbc6201f
    spring.security.oauth2.client.registration.github.client-secret=c324b934
    43594fe84d106bb32c904799e1839e6a
    spring.security.oauth2.client.registration.github.redirect-uri=http://lo
    calhost:8080/authorization_code

然后修改认证请求处理地址:

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
       @Override
       protected void configure(HttpSecurity http) throws Exception {
           http.authorizeRequests()
                   .anyRequest().authenticated()
                   .and()
                   .oauth2Login()
                   .loginProcessingUrl("/authorization_code");
       }
    }

前面我们已经分析过,GitHub 配置的回调地址相当于登录请求链接,默认的 loginProcessingUrl 是/login/oauth2/code/github,所以默认情况下,当重定向到 http://localhost:8080/authorization_code 地址时,该请求会被当成一个普通请求,无法在 OAuth2LoginAuthenticationFilter 过滤器中进行登录处理。在只有修改 loginProcessingUrl 地址才能确保当重定向到 http://localhost:8080/authorization_code 地址时,该请求会在 AbstractAuthenticationProcessingFilter#doFilter 方法中被认定为一个登录请求,进而将请求交给 OAuth2LoginAuthenticationFilter#attemptAuthentication 方法去处理,以完成登录操作。

这是一种自定义配置的方式。

我们也可以使用 Java 代码,完成更加丰富、更加灵活的配置。

使用 Java 代码配置时,可以删除 application.properties 中的所有配置,然后修改配置类,代码如下:

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
       @Override
       protected void configure(HttpSecurity http) throws Exception {
           http.authorizeRequests()
                   .anyRequest().authenticated()
                   .and()
                   .oauth2Login()
                   .loginProcessingUrl("/authorization_code");
       }
       @Bean
       public ClientRegistrationRepository clientRegistrationRepository() {
           return new
           InMemoryClientRegistrationRepository(githubClientRegistration());
       }
       private ClientRegistration githubClientRegistration() {
           return ClientRegistration.withRegistrationId("github")
           .clientId("aa9e79846df9cbc6201f")
           .clientSecret("c324b93443594fe84d106bb32c904799e1839e6a")
           .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
           .userNameAttributeName("id")
           .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
           .redirectUriTemplate("http://localhost:8080/authorization_code")
           .scope("read:user")
           .authorizationUri("https://github.com/login/oauth/authorize")
           .tokenUri("https://github.com/login/oauth/access_token")
           .userInfoUri("https://api.github.com/user")
           .clientName("GitHub")
           .build();
       }
    }

我们只需要向 Spring 容器中注册一个 ClientRegistrationRepository 实例,然后在该实例中提供 GitHub 的配置信息即可,此时 OAuth2ClientRegistrationRepositoryConfiguration 配置类自动配置的 ClientRegistrationRepository 实例就会失效。

这种配置方式非常直观也非常灵活,所有需要的配置信息现在都摆出来了,需要修改哪个直接修改即可。

15.4.5.2 自定义用户

默认情况下,GitHub 返回的用户信息被包装成一个 DefaultOAuth2User 对象,但是 DefaultOAuth2User 是通过一个 Map 集合来保存 GitHub 用户信息,这样解析起来并不方便,因此我们也可以自定义用户对象。

自定义用户对象实现 OAuth2User 接口即可,代码如下:

    public class GitHubOAuth2User implements OAuth2User {
       private List<GrantedAuthority> authorities =
               AuthorityUtils.createAuthorityList("ROLE_USER");
       private Map<String, Object> attributes;
       private String id;
       private String name;
       private String login;
       private String email;
       @Override
       public Collection<? extends GrantedAuthority> getAuthorities() {
           return this.authorities;
       }
       @Override
       public Map<String, Object> getAttributes() {
           if (this.attributes == null) {
               this.attributes = new HashMap<>();
               this.attributes.put("id", this.getId());
               this.attributes.put("name", this.getName());
               this.attributes.put("login", this.getLogin());
               this.attributes.put("email", this.getEmail());
           }
           return attributes;
       }
       public String getId() {
           return this.id;
       }
       public void setId(String id) {
           this.id = id;
       }
       @Override
       public String getName() {
           return this.name;
       }
       public void setName(String name) {
           this.name = name;
       }
       public String getLogin() {
           return this.login;
       }
       public void setLogin(String login) {
           this.login = login;
       }
       public String getEmail() {
           return this.email;
       }
       public void setEmail(String email) {
           this.email = email;
       }
    }

这里定义的 id、name、login 以及 email 属性,都是 GitHub 返回的用户信息中所包含的,如果还想映射其他用户信息,则继续定义相应的属性即可。最后在配置类中使用该自定义用户对象:

    protected void configure(HttpSecurity http) throws Exception {
       http.authorizeRequests()
               .anyRequest().authenticated()
               .and()
               .oauth2Login()
               .userInfoEndpoint()
               .customUserType(GitHubOAuth2User.class,"github")
               .and()
               .loginProcessingUrl("/authorization_code");
    }

配置完成后,在通过 Access Token 去加载用户信息这一环节中,将不再使用 DefaultOAuth2UserService 类去完成加载,而是使用 CustomUserTypesOAuth2UserService,该类支持自定义用户对象。

配置完成后,重启项目完成认证,此时再从 SecurityContextHolder 中提取出来的用户对象就不再是 DefaultOAuth2User,而是 GitHubOAuth2User 了。

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。