1. 概述
Spring Security 5 引入了一個新的 OAuth2LoginConfigurer 類,我們可以使用它來配置外部授權服務器。
在本教程中,我們將探索 oauth2Login() 元素中可用的各種配置選項。
2. Maven 依賴項
在 Spring Boot 項目中,只需添加 spring-boot-starter-oauth2-client。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>在非 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 作為身份提供商註冊的客户端設置項目以便進行登錄。
3.1. 獲取客户端憑據
要獲取 Google OAuth2 身份驗證的客户端憑據,請前往 Google API 控制枱 的“憑據”部分。
在這裏,我們將為我們的 Web 應用程序創建“OAuth2 客户端 ID”類型的憑據。 這將導致 Google 為我們設置客户端 ID 和密鑰。
我們還需要在 Google 控制枱中配置授權重定向 URI,這是用户成功使用 Google 登錄後將被重定向的路徑。
默認情況下,Spring Boot 將此重定向 URI 配置為 /login/oauth2/code/{registrationId}。
因此,對於 Google,我們將添加此 URI:
http://localhost:8081/login/oauth2/code/google為了獲取用於與 Facebook 進行身份驗證的客户端憑據,我們需要在 Facebook for Developers 網站上註冊一個應用程序,並設置相應的 URI 作為“有效 OAuth 重定向 URI”:
http://localhost:8081/login/oauth2/code/facebook3.2. 安全配置
接下來,我們需要將客户端憑據添加到 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. 其他客户端
請注意,Spring Security 項目除了 Google 和 Facebook 之外,還包含針對 GitHub 和 Okta 的默認配置。
這些默認配置提供了所有必要的認證信息,這使得我們只需輸入客户端憑據即可。
如果我們想使用 Spring Security 中未配置的另一種身份驗證提供者,則需要定義完整的配置,包括授權 URI 和令牌 URI 等信息。 下面 是 Spring Security 中的默認配置,可以幫助您瞭解所需的屬性。
4. 在非啓動項目中的設置
...
4.1. 創建 ClientRegistrationRepository Bean
如果我們的項目不使用 Spring Boot 應用,我們需要定義一個 ClientRegistrationRepository Bean,該 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 創建一個 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());
}如我們所見,我們可以使用 clientRegistrationRepository() 方法來註冊自定義的註冊倉庫。
我們還需要定義一個自定義登錄頁面,因為不再自動生成。關於此內容的更多信息將在下一部分介紹。
讓我們繼續對我們的登錄過程進行進一步自定義。
5. 自定義 oauth2Login()</h2
OAuth 2 流程中存在多個可以利用 oauth2Login() 方法進行定製的元素。
請注意,所有這些元素都具有默認配置,因此不需要進行顯式配置。
讓我們看看如何在我們的配置中進行這些定製。
5.1. 自定義登錄頁面
儘管 Spring Boot 為我們生成了一個默認的登錄頁面,但我們通常會定義自己的自定義頁面。
讓我們從配置新的登錄 URL 用於 oauth2Login() 元素,通過使用 loginPage() 方法開始:
@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";
}
}此方法必須將客户端可用情況及其授權端點映射發送到視圖,該信息將從 ClientRegistrationRepository Bean 中獲取:
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 以將用户重定向到:
.oauth2Login()
.defaultSuccessUrl("/loginSuccess")
.failureUrl("/loginFailure");如果用户訪問過受保護的頁面而尚未進行身份驗證,則登錄後他們將被重定向到該頁面。否則,他們將被重定向到/loginSuccess。
如果希望用户無論是否在訪問受保護的頁面之前,都始終被重定向到/loginSuccess URL,則可以使用方法defaultSuccessUrl(“/loginSuccess”, true)。
要使用自定義處理程序,則必須創建一個實現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();
}我們使用了 Spring 提供的實現方案來處理我們的 Bean,但我們也可以提供一個自定義的實現。
5.4. 自定義 Token Endpoint
Token Endpoint 處理訪問令牌。
讓我們明確配置 tokenEndpoint(),使用默認的響應客户端實現:
.oauth2Login()
.tokenEndpoint()
.accessTokenResponseClient(accessTokenResponseClient());以下是客户端 Bean 的響應:
@Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest>
accessTokenResponseClient() {
return new NimbusAuthorizationCodeTokenResponseClient();
}此配置與默認配置相同,並使用 Spring 實現,其原理是與提供者交換一個授權碼。
當然,我們也可以替換為自定義響應客户端。
5.5. 自定義重定向端點
這是在與外部提供商進行身份驗證後,用於重定向的端點。
讓我們看看如何更改 baseUri 以便為重定向端點進行配置:
.oauth2Login()
.redirectionEndpoint()
.baseUri("/oauth2/redirect")默認 URI 是 login/oauth2/code。
請注意,如果更改它,我們還需要更新每個 ClientRegistration 的 redirectUriTemplate 屬性,並將新的 URI 添加為每個客户端的授權重定向 URI。
5.6. 自定義用户信息端點
用户信息端點是我們獲取用户信息的所在地。
我們可以使用 userInfoEndpoint() 方法自定義此端點。為此,可以使用諸如 userService() 和 customUserType() 等方法來修改用户信息的檢索方式。
6. 訪問用户信息
要實現常見任務之一,即獲取已登錄用户的信息,我們可以向用户信息端點發起請求。
首先,我們需要獲取與當前用户令牌相對應的客户端:
@Autowired
private OAuth2AuthorizedClientService authorizedClientService;
@GetMapping("/loginSuccess")
public String getLoginInfo(Model model, OAuth2AuthenticationToken authentication) {
OAuth2AuthorizedClient client = authorizedClientService
.loadAuthorizedClient(
authentication.getAuthorizedClientRegistrationId(),
authentication.getName());
//...
return "loginSuccess";
}接下來,我們將向客户端的用户信息端點發送請求,並檢索 用户屬性映射:
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 屬性作為 屬性添加,我們可以將其在 視圖中顯示為對用户的歡迎信息:
除了 name 之外, 也包含諸如 、、 和 等屬性。
7. 結論
在本文中,我們學習瞭如何使用 oauth2Login() 元素在 Spring Security 中與 Google 和 Facebook 等不同提供商進行身份驗證。
我們還探討了一些自定義此過程的常見場景。