Как реализовать авторизацию через сторонние сервисы (OAuth2/OpenID Connect) в 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

Ответ 18+ 🔞

Слушай, я тут подумал, ну что за пиздец творится с этой авторизацией в вебе? Все эти OAuth, OpenID, JWT... Просто ёперный театр какой-то! Но если разобраться, то, блядь, не так страшен чёрт, как его малюют. Вот смотри, как это в Java, на Spring'е, делается, чтобы не обосраться.

1. Вот этот самый Spring Security OAuth2 Client (делай так, не еби мозг)

Ну, типа, главный фильтр, который всё решает. Создаёшь конфиг и пишешь правила. Главное — не накосячить с матчерами путей, а то получится, что все в твой /admin зайдут, как к себе домой.

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

Вот видишь? Разрешил всем ходить на главную, логин и ошибки. А на всё остальное — только авторизованным. И подключил вход через OAuth2. Вроде ничего сложного, да? Хуй с ним, разберёшься.

2. А это — сервис, который разбирается, кто пришёл

Вот это уже поинтереснее. Прилетает пользователь от Гугла или Гитхаба, а ты должен понять, кто это такой, и либо создать ему запись в своей базе, либо обновить старую. Типа, встречаем по одёжке — атрибутам.

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

Смотри, логика простая: смотрим, от кого пришёл запрос (registrationId). Если от Гугла — лезем в атрибуты, вытаскиваем почту и имя. И тут же, блядь, в свою базу его пихаем или обновляем. А на выходе создаём стандартного юзера Spring'а с ролью USER. Всё, пиздец, готово.

3. Конфигурация в application.yml — тут всё красиво

Тут просто копируй и подставляй свои ключи. Главное — не закоммить их в публичный репозиторий, а то будет овердохуища веселья, когда на твоём аккаунте начнут майнить биткоины.

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

Видишь? Для Гугла просим стандартные scope: openid, profile, email. Для Гитхаба — свои. А Keycloak — это вообще отдельная песня, self-hosted штука, но тоже настраивается.

4. А если прилетает просто JWT-токен? (OpenID Connect)

Бывает, что фронт сам авторизуется и присылает тебе уже готовый токен. Твоя задача — проверить, не поддельный ли он. Для этого нужен JwtDecoder, который сходит по известному адресу и возьмёт публичные ключи для проверки подписи.

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

Вот и вся магия. Настроил Resource Server, указал, какой декодер использовать, и Spring сам будет проверять каждый входящий токен. Удобно, блядь!

5. Таблица провайдеров, чтобы не путаться

Смотри, какие бывают варианты, и чем они отличаются. А то начнёшь к Гитхабу с openid лезть — он тебе, сука, ничего не даст.

Провайдер Протокол Основные 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, ролевая модель

Запомни главное: Гугл и Мелкомягкие — используют OpenID Connect (то есть, помимо доступа, они ещё и личность подтверждают). А Гитхаб — старый добрый OAuth2, только для делегирования доступа. Не перепутай, а то будешь как дурак выглядеть.

6. И напоследок — как не обосраться с безопасностью

Тут, блядь, внимание, это важно:

  • PKCE — это must have для мобилок и одностраничников. Без него — как без штанов.
  • State parameter — всегда включай. Это чтобы тебя не наебали CSRF-атакой.
  • Токены — Access token храни в оперативке, а Refresh token — в защищённой, HttpOnly куке. Не выёбывайся с LocalStorage.
  • Валидация JWT — всегда проверяй issuer (кто выпустил) и audience (для кого). А то прилетит токен от левого провайдера, а ты его примешь.
  • Обновление токенов — Spring умеет это делать автоматически через OAuth2AuthorizedClientManager. Используй, не изобретай велосипед.

Вот и всё, ебать мои старые костыли. Вроде ничего сложного, да? Главное — начать, а там уже по ходу дела разберёшься, где и какую хуйню поправить. Удачи, и не накосячь!