«Обратите внимание, что этот контент устарел и использует устаревший стек 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 Connect

Теперь нам нужно создать собственный фильтр 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 также будет содержать поле «email», так как мы добавили адрес электронной почты в наш запрос.

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() проверяет, что токен идентификатора был выпущен 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, [email protected]

7. Пример процесса OpenID Connect

Наконец, давайте взглянем на пример процесса аутентификации 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

Затем мы собираемся обменять код на Токен доступа и 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"
}

«

{
    "iss":"accounts.google.com",
    "at_hash":"AccessTokenHash",
    "sub":"12345678",
    "email_verified":true,
    "email":"[email protected]",
     ...
}

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

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

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

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