15.4 GitHub 授权登录
我们通过一个 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 了。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论