Spring Security - OAuth2 登录
1、概览
Spring Security 5 开始,引入了一个新的 OAuth2LoginConfigurer 类,可以用它来配置外部授权服务器(Authorization Server)。
本文主要带你了解 oauth2Login() 方法的一些可用配置选项。
2、Maven 依赖
在 Spring Boot 项目中,只需添加 spring-boot-starter-oauth2-client Starter 即可:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
    <version>2.3.3.RELEASE</version>
</dependency>
在非 Spring Boot 项目中,除了标准的 Spring 和 Spring Security 依赖外,还需要显式添加 spring-security-oauth2-client 和 spring-security-oauth2-jose 依赖:
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-client</artifactId>
    <version>5.3.4.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-jose</artifactId>
    <version>5.3.4.RELEASE</version>
</dependency>
3、客户端设置
在 Spring Boot 项目中,只需为每个要配置的客户端添加几个标准属性即可。
接下来,我们要配置使用 Google 和 Facebook 作为 Authentication Provider 注册的客户端登录。
3.1、获取客户端凭证
要获取 Google OAuth2 身份认证的客户端凭证,请访问 Google API 控制台 的 “Credentials” 部分。
在此,为 Web 应用创建 “OAuth2 Client ID” 类型的凭证。这样,Google 就会为我们设置一个 “client id” 和 “secret”。
还必须在 Google 控制台中配置 Authorized Redirect URI(授权重定向 URI),这是用户成功登录 Google 后重定向到的路径。
默认情况下,Spring Boot 会将重定向 URI 配置为 /login/oauth2/code/{registrationId}。
因此,为 Google 添加这个 URI:
http://localhost:8081/login/oauth2/code/google
要获取用于 Facebook 身份认证的客户端凭证,需要在 Facebook for Developers 网站上注册一个应用,并将相应的 URI 设置为 “Valid OAuth redirect URI”:
http://localhost:8081/login/oauth2/code/facebook
3.2、Security 配置
接下来,需要在 application.properties 文件中添加客户端凭证。
Spring Security 属性的前缀是 spring.security.oauth2.client.registration,然后是客户端名称和客户端属性名称:
spring.security.oauth2.client.registration.google.client-id=<your client id>
spring.security.oauth2.client.registration.google.client-secret=<your client secret>
spring.security.oauth2.client.registration.facebook.client-id=<your client id> 
spring.security.oauth2.client.registration.facebook.client-secret=<your client secret>
为至少一个客户端添加这些属性将启用 Oauth2ClientAutoConfiguration 类,该类会设置所有必要的 Bean。
自动 Web 安全配置相当于定义一个简单的 oauth2Login() 元素:
@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests()
         .anyRequest().authenticated()
         .and()
         .oauth2Login();
        return http.build();
    }
}
在这里,可以看到 oauth2Login() 元素的使用方式与已知的 httpBasic() 和 formLogin() 元素类似。
现在,当我们尝试访问受保护的 URL 时,应用会显示一个自动生成的登录页面,其中有两个客户端:

3.3、其他客户端
注意,除了 Google 和 Facebook 之外,Spring Security 项目还包含 GitHub 和 Okta 的默认配置。这些默认配置提供了所有必要的身份认证信息,因此我们只需输入客户端凭证即可。
如果想使用 Spring Security 中未配置的其他 Authentication Provider,就需要定义完整的配置,包括 Authorization URI 和 Token URI 等信息。下面 来看看 Spring Security 的默认配置,以了解所需的属性。
4、非 Spring Boot 项目
4.1、创建 ClientRegistrationRepository Bean
如果使用的不是 Spring Boot 应用,则需要定义一个 ClientRegistrationRepository Bean,其中包含授权服务器所拥有的客户端信息的内部表示:
@Configuration
@EnableWebSecurity
@PropertySource("classpath:application.properties")
public class SecurityConfig {
    private static List<String> clients = Arrays.asList("google", "facebook");
    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        List<ClientRegistration> registrations = clients.stream()
          .map(c -> getRegistration(c))
          .filter(registration -> registration != null)
          .collect(Collectors.toList());
        
        return new InMemoryClientRegistrationRepository(registrations);
    }
}
如上,创建了一个 InMemoryClientRegistrationRepository,其中包含一个 ClientRegistration 对象列表。
4.2、构建 ClientRegistration 对象
构建这些对象的 getRegistration() 方法如下:
private static String CLIENT_PROPERTY_KEY 
  = "spring.security.oauth2.client.registration.";
@Autowired
private Environment env;
private ClientRegistration getRegistration(String client) {
    String clientId = env.getProperty(
      CLIENT_PROPERTY_KEY + client + ".client-id");
    if (clientId == null) {
        return null;
    }
    String clientSecret = env.getProperty(
      CLIENT_PROPERTY_KEY + client + ".client-secret");
 
    if (client.equals("google")) {
        return CommonOAuth2Provider.GOOGLE.getBuilder(client)
          .clientId(clientId).clientSecret(clientSecret).build();
    }
    if (client.equals("facebook")) {
        return CommonOAuth2Provider.FACEBOOK.getBuilder(client)
          .clientId(clientId).clientSecret(clientSecret).build();
    }
    return null;
}
如上,从类似的 application.properties 文件中读取客户端凭证。然后,使用 Spring Security 中已定义的 CommonOauth2Provider 枚举来为 Google 和 Facebook 客户端提供其余的客户端属性。
每个 ClientRegistration 实例对应一个客户端。
4.3、注册 ClientRegistrationRepository
最后,必须基于 ClientRegistrationRepository Bean 创建一个 OAuth2AuthorizedClientService Bean,并使用 oauth2Login() 元素对两者进行注册:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeRequests().anyRequest().authenticated()
      .and()
      .oauth2Login()
      .clientRegistrationRepository(clientRegistrationRepository())
      .authorizedClientService(authorizedClientService());
    return http.build();
}
@Bean
public OAuth2AuthorizedClientService authorizedClientService() {
 
    return new InMemoryOAuth2AuthorizedClientService(
      clientRegistrationRepository());
}
如上,可以使用 oauth2Login() 的 clientRegistrationRepository() 方法来注册自定义 RegistrationRepository。
还必须定义一个自定义登录页面,因为它不会再自动生成。
5、自定义 oauth2Login()
OAuth 2 会用到几个元素,可以使用 oauth2Login() 方法对其进行自定义。
注意,所有这些元素在 Spring Boot 中都有默认配置,不需要显式配置。
接下来,看看如何在配置中自定义这些功能。
5.1、自定义登录页面
尽管 Spring Boot 会自动生成一个默认登录页面,但我们通常还是希望定义自己的自定义页面。
首先,使用 loginPage() 方法为 oauth2Login() 元素配置一个新的 Login URL:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeRequests()
      .antMatchers("/oauth_login")
      .permitAll()
      .anyRequest()
      .authenticated()
      .and()
      .oauth2Login()
      .loginPage("/oauth_login");
    return http.build();
}
如上,将登录 URL 设置为 /oauth_login。
接下来,定义一个 LoginController,其中包含一个映射到该 URL 的方法:
@Controller
public class LoginController {
    private static String authorizationRequestBaseUri
      = "oauth2/authorization";
    Map<String, String> oauth2AuthenticationUrls
      = new HashMap<>();
    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;
    @GetMapping("/oauth_login")
    public String getLoginPage(Model model) {
        // ...
        return "oauth_login";
    }
}
该方法需要向视图发送一个包含可用客户端及其授权端点的 Map,可以从 ClientRegistrationRepository Bean 中获取该 Map。
public String getLoginPage(Model model) {
    Iterable<ClientRegistration> clientRegistrations = null;
    ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository)
      .as(Iterable.class);
    if (type != ResolvableType.NONE && 
      ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) {
        clientRegistrations = (Iterable<ClientRegistration>) clientRegistrationRepository;
    }
    clientRegistrations.forEach(registration -> 
      oauth2AuthenticationUrls.put(registration.getClientName(), 
      authorizationRequestBaseUri + "/" + registration.getRegistrationId()));
    model.addAttribute("urls", oauth2AuthenticationUrls);
    return "oauth_login";
}
最后,需要定义 oauth_login.html 页面:
<h3>Login with:</h3>
<p th:each="url : ${urls}">
    <a th:text="${url.key}" th:href="${url.value}">Client</a>
</p>
这是一个简单的 HTML 页面,显示与每个客户端进行身份认证的链接。
添加一些样式后,就可以改变登录页面的外观了:

5.2、自定义身份认证成功和失败行为
可以用不同的方法控制身份认证后的行为:
- defaultSuccessUrl()和- failureUrl()将用户重定向到给定的 URL
- successHandler()和- failureHandler()以在身份认证过程后运行自定义逻辑
来看看如何设置自定义 URL,将用户重定向至指定 URL:
.oauth2Login()
  .defaultSuccessUrl("/loginSuccess")
  .failureUrl("/loginFailure");
如果用户在认证前访问了受保护的页面,则登录后会重定向到该页面。否则,用户将被重定向到 /loginSuccess。
如果希望将用户始终重定向到 /loginSuccess URL,而不管他们之前是否访问了受保护的页面,可以使用 defaultSuccessUrl("/loginSuccess", true) 方法。
要使用自定义 Handler,必须创建一个实现 AuthenticationSuccessHandler 或 AuthenticationFailureHandler 接口的类,覆写继承的方法,然后使用 successHandler() 和 failureHandler() 方法设置 Bean。
5.3、自定义授权端点
授权端点是 Spring Security 用来向外部服务器触发授权请求的端点。
首先,为授权端点设置新属性:
.oauth2Login() 
  .authorizationEndpoint()
  .baseUri("/oauth2/authorize-client")
  .authorizationRequestRepository(authorizationRequestRepository());
如上,将 baseUri 修改为 /oauth2/authorize-client,而不是默认的 /oauth2/authorization。
还明确设置了一个必须定义的 authorizationRequestRepository() Bean:
@Bean
public AuthorizationRequestRepository<OAuth2AuthorizationRequest> 
  authorizationRequestRepository() {
 
    return new HttpSessionOAuth2AuthorizationRequestRepository();
}
该 Bean 使用了 Spring 提供的实现,但也可以提供自定义的实现。
5.4、自定义 Token 端点
Token 端点处理 Access Token。
使用默认的响应客户端实现来显式配置 tokenEndpoint():
.oauth2Login()
  .tokenEndpoint()
  .accessTokenResponseClient(accessTokenResponseClient());
下面是响应客户端的 Bean:
@Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> 
  accessTokenResponseClient() {
 
    return new NimbusAuthorizationCodeTokenResponseClient();
}
这个配置与默认配置相同,它使用了基于与 Provider 交换授权代码(Authorization Code)的 Spring 实现。
当然,也可以用自定义响应客户端来代替。
5.5、自定义重定向端点
这是与外部 Provider 进行身份认证后重定向到的端点。
来看看如何更改重定向端点的 baseUri:
.oauth2Login()
  .redirectionEndpoint()
  .baseUri("/oauth2/redirect")
默认 URI 为 login/oauth2/code。
注意,如果更改了它,还必须更新每个 ClientRegistration 的 redirectUriTemplate 属性,并将新 URI 添加为每个客户端的授权重定向 URI。
5.6、自定义用户信息端点
用户信息端点是用来获取用户信息的位置。
使用 userInfoEndpoint() 方法自定义该端点。为此,可以使用 userService() 和 customUserType() 等方法来修改检索用户信息的方式。
6、访问用户信息
我们可能要完成的一项常见任务是查找登录用户的相关信息。为此,可以向用户信息端点发出请求。
首先,必须获取与当前用户 Token 对应的客户端。
@Autowired
private OAuth2AuthorizedClientService authorizedClientService;
@GetMapping("/loginSuccess")
public String getLoginInfo(Model model, OAuth2AuthenticationToken authentication) {
    OAuth2AuthorizedClient client = authorizedClientService
      .loadAuthorizedClient(
        authentication.getAuthorizedClientRegistrationId(), 
          authentication.getName());
    //...
    return "loginSuccess";
}
接下来,向客户端的用户信息端点发起请求,并检索 userAttributes Map:
String userInfoEndpointUri = client.getClientRegistration()
  .getProviderDetails().getUserInfoEndpoint().getUri();
if (!StringUtils.isEmpty(userInfoEndpointUri)) {
    RestTemplate restTemplate = new RestTemplate();
    HttpHeaders headers = new HttpHeaders();
    headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + client.getAccessToken()
      .getTokenValue());
    HttpEntity entity = new HttpEntity("", headers);
    ResponseEntity <map>response = restTemplate
      .exchange(userInfoEndpointUri, HttpMethod.GET, entity, Map.class);
    Map userAttributes = response.getBody();
    model.addAttribute("name", userAttributes.get("name"));
}
通过将 name 属性添加为 Model 属性,可以在 loginSuccess 视图中将其显示为给用户的欢迎信息:

除 name 外,userAttributes Map 还包含 email、family_name、picture 和 locale 等属性。
7、总结
本文介绍了如何使用 Spring Security 来配置 Oauth2 登录。
Ref:https://www.baeldung.com/spring-security-5-oauth2-login
 
				