Keamanan Musim Semi 5 - Login OAuth2

1. Ikhtisar

Spring Security 5 memperkenalkan kelas OAuth2LoginConfigurer baru yang dapat kita gunakan untuk mengonfigurasi Server Otorisasi eksternal.

Pada artikel ini, kita akan menjelajahi beberapa dari berbagai pilihan konfigurasi yang tersedia untuk oauth2Login () elemen .

2. Ketergantungan Maven

Dalam proyek Spring Boot, yang kita butuhkan hanyalah menambahkan starter spring-boot-starter-oauth2-client :

 org.springframework.boot spring-boot-starter-oauth2-client 2.3.3.RELEASE 

Dalam proyek non-Boot, selain dependensi standar Spring dan Spring Security, kita juga perlu menambahkan dependensi spring-security-oauth2-client dan spring-security-oauth2-jose secara eksplisit :

 org.springframework.security spring-security-oauth2-client 5.3.4.RELEASE   org.springframework.security spring-security-oauth2-jose 5.3.4.RELEASE 

3. Pengaturan Klien

Dalam proyek Spring Boot, yang perlu kita lakukan hanyalah menambahkan beberapa properti standar untuk setiap klien yang ingin kita konfigurasi.

Mari kita siapkan proyek kita untuk masuk dengan klien yang terdaftar di Google dan Facebook sebagai penyedia otentikasi.

3.1. Mendapatkan Kredensial Klien

Untuk mendapatkan kredensial klien untuk autentikasi Google OAuth2, buka Konsol API Google - bagian "Kredensial".

Di sini kita akan membuat kredensial jenis "ID Klien OAuth2" untuk aplikasi web kita. Hal ini menyebabkan Google menyiapkan id dan rahasia klien untuk kita.

Kami juga harus mengonfigurasi URI pengalihan resmi di Konsol Google, yang merupakan jalur tujuan pengalihan pengguna setelah mereka berhasil masuk dengan Google.

Secara default, Spring Boot mengonfigurasi URI pengalihan ini sebagai / login / oauth2 / code / {RegistrationId}. Oleh karena itu, untuk Google kami akan menambahkan URI:

//localhost:8081/login/oauth2/code/google

Untuk mendapatkan kredensial klien untuk autentikasi dengan Facebook, kita perlu mendaftarkan aplikasi di situs Facebook untuk Pengembang dan menyiapkan URI yang sesuai sebagai "URI pengalihan OAuth yang valid":

//localhost:8081/login/oauth2/code/facebook

3.3. Konfigurasi Keamanan

Selanjutnya, kita perlu menambahkan kredensial klien ke file application.properties . Properti Keamanan Musim Semi diawali dengan "spring.security.oauth2.client.registration" diikuti dengan nama klien, lalu nama properti klien:

spring.security.oauth2.client.registration.google.client-id= spring.security.oauth2.client.registration.google.client-secret= spring.security.oauth2.client.registration.facebook.client-id= spring.security.oauth2.client.registration.facebook.client-secret=

Menambahkan properti ini untuk setidaknya satu klien akan mengaktifkan kelas Oauth2ClientAutoConfiguration yang mengatur semua kacang yang diperlukan.

Konfigurasi keamanan web otomatis sama dengan mendefinisikan elemen oauth2Login () sederhana :

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

Di sini, kita bisa melihat elemen oauth2Login () digunakan dengan cara yang mirip dengan elemen httpBasic () dan formLogin () yang sudah dikenal .

Sekarang, ketika kami mencoba mengakses URL yang dilindungi, aplikasi akan menampilkan halaman login yang dibuat secara otomatis dengan dua klien:

3.4. Klien Lainnya

Perhatikan bahwa selain Google dan Facebook, proyek Keamanan Musim Semi juga berisi konfigurasi default untuk GitHub dan Okta. Konfigurasi default ini menyediakan semua informasi yang diperlukan untuk otentikasi, yang memungkinkan kami untuk hanya memasukkan kredensial klien.

Jika kami ingin menggunakan penyedia autentikasi lain yang tidak dikonfigurasi di Spring Security, kami perlu menentukan konfigurasi lengkapnya, dengan informasi seperti URI otorisasi dan URI token. Berikut adalah tampilan konfigurasi default di Spring Security untuk mengetahui properti yang diperlukan.

4. Setup dalam Proyek Non-Boot

4.1. Membuat Bean ClientRegistrationRepository

If we're not working with a Spring Boot application, we'll need to define a ClientRegistrationRepository bean that contains an internal representation of the client information owned by the authorization server:

@Configuration @EnableWebSecurity @PropertySource("classpath:application.properties") public class SecurityConfig extends WebSecurityConfigurerAdapter { private static List clients = Arrays.asList("google", "facebook"); @Bean public ClientRegistrationRepository clientRegistrationRepository() { List registrations = clients.stream() .map(c -> getRegistration(c)) .filter(registration -> registration != null) .collect(Collectors.toList()); return new InMemoryClientRegistrationRepository(registrations); } }

Here we're creating an InMemoryClientRegistrationRepository with a list of ClientRegistration objects.

4.2. Building ClientRegistration Objects

Let's see the getRegistration() method that builds these objects:

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; }

Here, we're reading the client credentials from a similar application.properties file, then using the CommonOauth2Provider enum already defined in Spring Security for the rest of the client properties for Google and Facebook clients.

Each ClientRegistration instance corresponds to a client.

4.3. Registering the ClientRegistrationRepository

Finally, we have to create an OAuth2AuthorizedClientService bean based on the ClientRegistrationRepository bean and register both with the oauth2Login() element:

@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated() .and() .oauth2Login() .clientRegistrationRepository(clientRegistrationRepository()) .authorizedClientService(authorizedClientService()); } @Bean public OAuth2AuthorizedClientService authorizedClientService() { return new InMemoryOAuth2AuthorizedClientService( clientRegistrationRepository()); }

As evidenced here, we can use the clientRegistrationRepository() method of oauth2Login() to register a custom registration repository.

We'll also have to define a custom login page, as it won't be automatically generated anymore. We'll see more information on this in the next section.

Let's continue with further customization of our login process.

5. Customizing oauth2Login()

There are several elements that the OAuth 2 process uses and that we can customize using oauth2Login() methods.

Note that all these elements have default configurations in Spring Boot and explicit configuration isn't required.

Let's see how we can customize these in our configuration.

5.1. Custom Login Page

Even though Spring Boot generates a default login page for us, we'll usually want to define our own customized page.

Let's start with configuring a new login URL for the oauth2Login() element by using theloginPage() method:

@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/oauth_login") .permitAll() .anyRequest() .authenticated() .and() .oauth2Login() .loginPage("/oauth_login"); }

Here, we've set up our login URL to be /oauth_login.

Next, let's define a LoginController with a method that maps to this URL:

@Controller public class LoginController { private static String authorizationRequestBaseUri = "oauth2/authorization"; Map oauth2AuthenticationUrls = new HashMap(); @Autowired private ClientRegistrationRepository clientRegistrationRepository; @GetMapping("/oauth_login") public String getLoginPage(Model model) { // ... return "oauth_login"; } }

This method has to send a map of the clients available and their authorization endpoints to the view, which we'll obtain from the ClientRegistrationRepository bean:

public String getLoginPage(Model model) { Iterable clientRegistrations = null; ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository) .as(Iterable.class); if (type != ResolvableType.NONE && ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) { clientRegistrations = (Iterable) clientRegistrationRepository; } clientRegistrations.forEach(registration -> oauth2AuthenticationUrls.put(registration.getClientName(), authorizationRequestBaseUri + "/" + registration.getRegistrationId())); model.addAttribute("urls", oauth2AuthenticationUrls); return "oauth_login"; }

Finally, we need to define our oauth_login.html page:

Login with:

Client

This is a simple HTML page that displays links to authenticate with each client.

After adding some styling to it, we can change the look of the login page:

5.2. Custom Authentication Success and Failure Behavior

We can control the post-authentication behavior by using different methods:

  • defaultSuccessUrl() and failureUrl() – to redirect the user to a given URL
  • successHandler() and failureHandler() – to execute custom logic following the authentication process

Let's see how we can set custom URL's to redirect the user to:

.oauth2Login() .defaultSuccessUrl("/loginSuccess") .failureUrl("/loginFailure");

If the user visited a secured page before authenticating, they will be redirected to that page after logging in; otherwise, they will be redirected to /loginSuccess.

If we want the user to always be sent to the /loginSuccess URL regardless if they were on a secured page before or not, we can use the method defaultSuccessUrl(“/loginSuccess”, true).

To use a custom handler, we would have to create a class that implements the AuthenticationSuccessHandler or AuthenticationFailureHandler interfaces, override the inherited methods, then set the beans using the successHandler() and failureHandler() methods.

5.3. Custom Authorization Endpoint

The authorization endpoint is the endpoint that Spring Security uses to trigger an authorization request to the external server.

First, let's set new properties for the authorization endpoint:

.oauth2Login() .authorizationEndpoint() .baseUri("/oauth2/authorize-client") .authorizationRequestRepository(authorizationRequestRepository());

Here, we've modified the baseUri to /oauth2/authorize-client instead of the default /oauth2/authorization. We're also explicitly setting an authorizationRequestRepository() bean that we have to define:

@Bean public AuthorizationRequestRepository authorizationRequestRepository() { return new HttpSessionOAuth2AuthorizationRequestRepository(); }

In our example, we've used the Spring-provided implementation for our bean, but we could also provide a custom one.

5.4. Custom Token Endpoint

The token endpoint processes access tokens.

Let's explicitly configure the tokenEndpoint()with the default response client implementation:

.oauth2Login() .tokenEndpoint() .accessTokenResponseClient(accessTokenResponseClient());

And here's the response client bean:

@Bean public OAuth2AccessTokenResponseClient accessTokenResponseClient() { return new NimbusAuthorizationCodeTokenResponseClient(); }

This configuration is the same as the default one and is using the Spring implementation which is based on exchanging an authorization code with the provider.

Of course, we could also substitute a custom response client.

5.5. Custom Redirection Endpoint

This is the endpoint to redirect to after authentication with the external provider.

Let's see how we can change the baseUri for the redirection endpoint:

.oauth2Login() .redirectionEndpoint() .baseUri("/oauth2/redirect")

The default URI is login/oauth2/code.

Note that if we change it, we also have to update the redirectUriTemplate property of each ClientRegistration and add the new URI as an authorized redirect URI for each client.

5.6. Custom User Information Endpoint

The user info endpoint is the location we can leverage to obtain user information.

We can customize this endpoint using the userInfoEndpoint() method. For this, we can use methods such as userService() and customUserType() to modify the way user information is retrieved.

6. Accessing User Information

A common task we may want to achieve is finding information about the logged-in user. For this, we can make a request to the user information endpoint.

First, we'll have to get the client corresponding to the current user token:

@Autowired private OAuth2AuthorizedClientService authorizedClientService; @GetMapping("/loginSuccess") public String getLoginInfo(Model model, OAuth2AuthenticationToken authentication) { OAuth2AuthorizedClient client = authorizedClientService .loadAuthorizedClient( authentication.getAuthorizedClientRegistrationId(), authentication.getName()); //... return "loginSuccess"; }

Next, we'll send a request to the client's user info endpoint and retrieve the 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 response = restTemplate .exchange(userInfoEndpointUri, HttpMethod.GET, entity, Map.class); Map userAttributes = response.getBody(); model.addAttribute("name", userAttributes.get("name")); }

Dengan menambahkan properti name sebagai atribut Model , kita dapat menampilkannya dalam tampilan loginSuccess sebagai pesan selamat datang kepada pengguna:

Selain nama, yang userAttributes Peta juga mengandung sifat seperti email, FAMILY_NAME, gambar, lokal.

7. Kesimpulan

Di artikel ini, kita telah melihat bagaimana kita dapat menggunakan elemen oauth2Login () di Spring Security untuk mengautentikasi dengan penyedia yang berbeda seperti Google dan Facebook. Kami juga telah melalui beberapa skenario umum untuk menyesuaikan proses ini.

Kode sumber lengkap dari contoh-contoh tersebut dapat ditemukan di GitHub.