Spring Security 5 - OAuth2 Login

1. Огляд

Spring Security 5 представляє новий клас OAuth2LoginConfigurer, який ми можемо використовувати для налаштування зовнішнього сервера авторизації.

У цій статті ми розглянемо деякі різні параметри конфігурації, доступні для елемента oauth2Login () .

2. Залежності Мейвена

У проекті Spring Boot все, що нам потрібно, - це додати клієнт spring-boot-starter-oauth2-client :

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

У не завантажувальному проекті, на додаток до стандартних залежностей Spring та Spring Security, нам також потрібно буде явно додати залежності spring-security-oauth2-client та spring-security-oauth2-jose :

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

3. Налаштування клієнтів

У проекті Spring Boot все, що нам потрібно зробити, - це додати кілька стандартних властивостей для кожного клієнта, якого ми хочемо налаштувати.

Давайте налаштуємо наш проект для входу з клієнтами, зареєстрованими в Google та Facebook як постачальники автентифікації.

3.1. Отримання облікових даних клієнта

Щоб отримати облікові дані клієнта для автентифікації Google OAuth2, перейдіть до консолі Google API - розділ “Повноваження”.

Тут ми створимо облікові дані типу “OAuth2 Client ID” для нашої веб-програми. Це призводить до того, що Google встановлює ідентифікатор клієнта та секретний для нас.

Ми також повинні налаштувати авторизований URI переспрямування в консолі Google, тобто шлях, на який будуть перенаправляти користувачів після успішного входу в Google.

За замовчуванням Spring Boot налаштовує цей переспрямований URI як / login / oauth2 / code / {registrationId}. Тому для Google ми додамо URI:

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

Щоб отримати облікові дані клієнта для автентифікації з Facebook, нам потрібно зареєструвати додаток на веб-сайті Facebook for Developers і встановити відповідний URI як “Дійсний URI переспрямування OAuth”:

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

3.3. Конфігурація безпеки

Далі нам потрібно додати облікові дані клієнта до файлу application.properties . Властивості Spring Security мають префікс “spring.security.oauth2.client.registration”, за яким слід ім’я клієнта, а потім ім’я властивості клієнта:

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=

Додавання цих властивостей принаймні для одного клієнта увімкне клас Oauth2ClientAutoConfiguration, який встановлює всі необхідні компоненти .

Автоматична конфігурація веб-безпеки еквівалентна визначенню простого елемента oauth2Login () :

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

Тут ми бачимо, що елемент oauth2Login () використовується подібним чином до вже відомих елементів httpBasic () та formLogin () .

Тепер, коли ми намагаємось отримати доступ до захищеної URL-адреси, програма відобразить автоматично згенеровану сторінку входу з двома клієнтами:

3.4. Інші клієнти

Зауважте, що окрім Google та Facebook, проект Spring Security також містить конфігурації за замовчуванням для GitHub та Okta. Ці конфігурації за замовчуванням надають всю необхідну інформацію для автентифікації, що дозволяє нам вводити лише облікові дані клієнта.

Якщо ми хочемо використовувати іншого постачальника автентифікації, не налаштований у Spring Security, нам потрібно буде визначити повну конфігурацію з такою інформацією, як URI авторизації та URI маркера. Ось погляньте на конфігурації за замовчуванням у Spring Security, щоб мати уявлення про необхідні властивості.

4. Налаштування в незавантажувальному проекті

4.1. Створення ClientRegistrationRepository Bean

Якщо ми не працюємо з програмою Spring Boot, нам потрібно буде визначити компонент ClientRegistrationRepository, який містить внутрішнє представлення інформації про клієнта, що належить серверу авторизації:

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

Тут ми створюємо 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 , а потім використовуємо перелік CommonOauth2Provider, уже визначений у Spring Security, для решти властивостей клієнта для клієнтів Google і Facebook.

Кожен екземпляр ClientRegistration відповідає клієнту.

4.3. Реєстрація ClientRegistrationRepository

Нарешті, ми повинні створити компонент OAuth2AuthorizedClientService на основі компонента ClientRegistrationRepository і зареєструвати обидва елементи за допомогою елемента oauth2Login () :

@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")); }

Додавши властивість name як атрибут Model , ми можемо відобразити його у поданні loginSuccess як вітальне повідомлення для користувача:

Крім назви, userAttributes Карта також містить властивості , такі як електронна пошта, FAMILY_NAME, зображення, мови.

7. Висновок

У цій статті ми побачили, як ми можемо використовувати елемент oauth2Login () у Spring Security для автентифікації у різних постачальників, таких як Google та Facebook. Ми також пройшли деякі поширені сценарії налаштування цього процесу.

Повний вихідний код прикладів можна знайти на GitHub.