Перейти к основному содержимому

Spring Security и OpenID Connect (устаревшая версия)

· 6 мин. чтения

Обратите внимание, что этот контент устарел и использует устаревший стек OAuth. Взгляните на последнюю поддержку OAuth в Spring Security .

1. Обзор

В этом кратком руководстве мы сосредоточимся на настройке OpenID Connect с реализацией Spring Security OAuth2.

OpenID Connect — это простой уровень идентификации, созданный поверх протокола OAuth 2.0.

А если конкретнее, узнаем, как аутентифицировать пользователей с помощью реализации OpenID Connect от Google .

2. Конфигурация Maven

Во-первых, нам нужно добавить следующие зависимости в наше приложение Spring Boot:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>

3. Идентификационный токен

Прежде чем мы углубимся в детали реализации, давайте кратко рассмотрим, как работает OpenID и как мы будем с ним взаимодействовать.

На данный момент, конечно, важно уже иметь представление об OAuth2, поскольку OpenID построен на основе OAuth.

Во-первых, чтобы использовать функциональность идентификации, мы воспользуемся новой областью действия OAuth2 под названием openid . Это приведет к появлению дополнительного поля в нашем токене доступа — « id_token ».

id_token — это JWT (веб- токен JSON), который содержит идентификационную информацию о пользователе, подписанную поставщиком идентификационных данных (в нашем случае Google).

Наконец, как сервер (код авторизации) , так и неявные потоки являются наиболее часто используемыми способами получения id_token , в нашем примере мы будем использовать серверный поток .

3. Конфигурация клиента OAuth2

Далее давайте настроим наш клиент OAuth2 следующим образом:

@Configuration
@EnableOAuth2Client
public class GoogleOpenIdConnectConfig {
@Value("${google.clientId}")
private String clientId;

@Value("${google.clientSecret}")
private String clientSecret;

@Value("${google.accessTokenUri}")
private String accessTokenUri;

@Value("${google.userAuthorizationUri}")
private String userAuthorizationUri;

@Value("${google.redirectUri}")
private String redirectUri;

@Bean
public OAuth2ProtectedResourceDetails googleOpenId() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
details.setClientId(clientId);
details.setClientSecret(clientSecret);
details.setAccessTokenUri(accessTokenUri);
details.setUserAuthorizationUri(userAuthorizationUri);
details.setScope(Arrays.asList("openid", "email"));
details.setPreEstablishedRedirectUri(redirectUri);
details.setUseCurrentUri(false);
return details;
}

@Bean
public OAuth2RestTemplate googleOpenIdTemplate(OAuth2ClientContext clientContext) {
return new OAuth2RestTemplate(googleOpenId(), clientContext);
}
}

А вот application.properties :

google.clientId=<your app clientId>
google.clientSecret=<your app clientSecret>
google.accessTokenUri=https://www.googleapis.com/oauth2/v3/token
google.userAuthorizationUri=https://accounts.google.com/o/oauth2/auth
google.redirectUri=http://localhost:8081/google-login

Обратите внимание, что:

  • Сначала вам необходимо получить учетные данные OAuth 2.0 для веб-приложения Google из Google Developers Console .
  • Мы использовали openid для получения id_token .
  • мы также использовали адрес электронной почты дополнительной области , чтобы включить адрес электронной почты пользователя в идентификационную информацию id_token .
  • URI перенаправления http://localhost:8081/google-login такой же, как и в нашем веб-приложении Google.

4. Пользовательский фильтр подключения OpenID

Теперь нам нужно создать собственный OpenIdConnectFilter для извлечения аутентификации из id_token — следующим образом:

public class OpenIdConnectFilter extends AbstractAuthenticationProcessingFilter {

public OpenIdConnectFilter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
setAuthenticationManager(new NoopAuthenticationManager());
}
@Override
public Authentication attemptAuthentication(
HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
OAuth2AccessToken accessToken;
try {
accessToken = restTemplate.getAccessToken();
} catch (OAuth2Exception e) {
throw new BadCredentialsException("Could not obtain access token", e);
}
try {
String idToken = accessToken.getAdditionalInformation().get("id_token").toString();
String kid = JwtHelper.headers(idToken).get("kid");
Jwt tokenDecoded = JwtHelper.decodeAndVerify(idToken, verifier(kid));
Map<String, String> authInfo = new ObjectMapper()
.readValue(tokenDecoded.getClaims(), Map.class);
verifyClaims(authInfo);
OpenIdConnectUserDetails user = new OpenIdConnectUserDetails(authInfo, accessToken);
return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
} catch (InvalidTokenException e) {
throw new BadCredentialsException("Could not obtain user details from token", e);
}
}
}

А вот и наш простой OpenIdConnectUserDetails :

public class OpenIdConnectUserDetails implements UserDetails {
private String userId;
private String username;
private OAuth2AccessToken token;

public OpenIdConnectUserDetails(Map<String, String> userInfo, OAuth2AccessToken token) {
this.userId = userInfo.get("sub");
this.username = userInfo.get("email");
this.token = token;
}
}

Обратите внимание, что:

  • Spring Security JwtHelper для декодирования id_token .
  • id_token всегда содержит поле « sub» , которое является уникальным идентификатором пользователя.
  • id_token также будет содержать поле « электронная почта », так как мы добавили область электронной почты в наш запрос.

4.1. Проверка идентификатора токена

В приведенном выше примере мы использовали метод decodeAndVerify () JwtHelper для извлечения информации из id_token, а также для ее проверки.

Первым шагом для этого является проверка того, что он был подписан одним из сертификатов, указанных в документе Google Discovery .

Они меняются примерно раз в день, поэтому для их чтения воспользуемся служебной библиотекой jwks-rsa :

<dependency>
<groupId>com.auth0</groupId>
<artifactId>jwks-rsa</artifactId>
<version>0.3.0</version>
</dependency>

Давайте добавим URL-адрес, содержащий сертификаты, в файл application.properties :

google.jwkUrl=https://www.googleapis.com/oauth2/v2/certs

Теперь мы можем прочитать это свойство и построить объект RSAVerifier :

@Value("${google.jwkUrl}")
private String jwkUrl;

private RsaVerifier verifier(String kid) throws Exception {
JwkProvider provider = new UrlJwkProvider(new URL(jwkUrl));
Jwk jwk = provider.get(kid);
return new RsaVerifier((RSAPublicKey) jwk.getPublicKey());
}

Наконец, мы также проверим утверждения в токене декодированного идентификатора:

public void verifyClaims(Map claims) {
int exp = (int) claims.get("exp");
Date expireDate = new Date(exp * 1000L);
Date now = new Date();
if (expireDate.before(now) || !claims.get("iss").equals(issuer) ||
!claims.get("aud").equals(clientId)) {
throw new RuntimeException("Invalid claims");
}
}

Метод verifyClaims() проверяет, что токен id был выдан Google и срок его действия не истек.

Более подробную информацию об этом вы можете найти в документации Google .

5. Конфигурация безопасности

Далее, давайте обсудим нашу конфигурацию безопасности:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private OAuth2RestTemplate restTemplate;

@Bean
public OpenIdConnectFilter openIdConnectFilter() {
OpenIdConnectFilter filter = new OpenIdConnectFilter("/google-login");
filter.setRestTemplate(restTemplate);
return filter;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterAfter(new OAuth2ClientContextFilter(),
AbstractPreAuthenticatedProcessingFilter.class)
.addFilterAfter(OpenIdConnectFilter(),
OAuth2ClientContextFilter.class)
.httpBasic()
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/google-login"))
.and()
.authorizeRequests()
.anyRequest().authenticated();
}
}

Обратите внимание, что:

  • Мы добавили наш собственный OpenIdConnectFilter после OAuth2ClientContextFilter.
  • Мы использовали простую конфигурацию безопасности, чтобы перенаправить пользователей на « /google-login » для аутентификации в Google.

6. Пользовательский контроллер

Далее, вот простой контроллер для тестирования нашего приложения:

@Controller
public class HomeController {
@RequestMapping("/")
@ResponseBody
public String home() {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
return "Welcome, " + username;
}
}

Пример ответа (после перенаправления в Google для подтверждения полномочий приложения):

Welcome, example@gmail.com

7. Пример процесса подключения OpenID

Наконец, давайте взглянем на пример процесса аутентификации OpenID Connect.

Во-первых, мы собираемся отправить запрос аутентификации :

https://accounts.google.com/o/oauth2/auth?
client_id=sampleClientID
response_type=code&
scope=openid%20email&
redirect_uri=http://localhost:8081/google-login&
state=abc

Ответ ( после утверждения пользователем ) представляет собой перенаправление на:

http://localhost:8081/google-login?state=abc&code=xyz

Далее мы собираемся обменять код на Access Token и id_token :

POST https://www.googleapis.com/oauth2/v3/token 
code=xyz&
client_id= sampleClientID&
client_secret= sampleClientSecret&
redirect_uri=http://localhost:8081/google-login&
grant_type=authorization_code

Вот пример ответа:

{
"access_token": "SampleAccessToken",
"id_token": "SampleIdToken",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "SampleRefreshToken"
}

Наконец, вот как выглядит информация фактического id_token :

{
"iss":"accounts.google.com",
"at_hash":"AccessTokenHash",
"sub":"12345678",
"email_verified":true,
"email":"example@gmail.com",
...
}

Таким образом, вы можете сразу увидеть, насколько полезна информация о пользователе внутри токена для предоставления идентификационной информации нашему собственному приложению.

8. Заключение

В этом кратком вводном руководстве мы узнали, как аутентифицировать пользователей с помощью реализации OpenID Connect от Google.

И, как всегда, вы можете найти исходный код на GitHub .