Как реализовать авторизацию через сторонние сервисы (OAuth2/OpenID Connect) в Java?

«Как реализовать авторизацию через сторонние сервисы (OAuth2/OpenID Connect) в Java?» — вопрос из категории Безопасность, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Архитектура OAuth2/OpenID Connect в Java

1. Spring Security OAuth2 Client (рекомендуемый подход)

@Configuration
public class OAuth2LoginConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/login**", "/error**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/login")
                .userInfoEndpoint(userInfo -> userInfo
                    .userService(customOAuth2UserService)
                )
                .successHandler(authenticationSuccessHandler)
            )
            .logout(logout -> logout
                .logoutSuccessUrl("/")
                .permitAll()
            );
        return http.build();
    }
}

2. Кастомизация User Service

@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) {
        OAuth2User oauth2User = delegate.loadUser(userRequest);

        // Извлечение атрибутов из провайдера
        Map<String, Object> attributes = oauth2User.getAttributes();
        String registrationId = userRequest.getClientRegistration().getRegistrationId();

        if ("google".equals(registrationId)) {
            String email = (String) attributes.get("email");
            String name = (String) attributes.get("name");
            // Сохранение/обновление пользователя в БД
        }

        return new DefaultOAuth2User(
            Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
            attributes,
            "email" // nameAttributeKey
        );
    }
}

3. Конфигурация провайдеров

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_SECRET}
            scope: openid, profile, email
          github:
            client-id: ${GITHUB_CLIENT_ID}
            client-secret: ${GITHUB_SECRET}
            scope: user:email, read:user
        provider:
          keycloak:
            issuer-uri: ${KEYCLOAK_ISSUER_URI}
            user-name-attribute: preferred_username

4. JWT-валидация для OpenID Connect

@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withJwkSetUri("https://idp.example.com/.well-known/jwks.json")
        .build();
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .oauth2ResourceServer(oauth2 -> oauth2
            .jwt(jwt -> jwt.decoder(jwtDecoder()))
        );
    return http.build();
}

5. Основные провайдеры и их особенности

Провайдер Протокол Основные scope Особенности
Google OpenID Connect openid, profile, email Поддержка G Suite, проверка домена
GitHub OAuth2 user, repo Только OAuth2 (не OpenID Connect)
Microsoft OpenID Connect openid, profile, User.Read Интеграция с Azure AD
Keycloak OpenID Connect Настраиваемые Self-hosted, ролевая модель

6. Безопасность и best practices

  • PKCE (Proof Key for Code Exchange) для public clients
  • State parameter для защиты от CSRF
  • Хранение токенов: Access token в памяти, Refresh token в secure cookie
  • Валидация issuer и audience в JWT
  • Автоматическое обновление токенов через OAuth2AuthorizedClientManager