Перенос Spring Cloud с OAuth 2.0 на OAuth 2.1

У меня есть старый шлюз Spring Cloud, работающий с сервером Keyclock. У меня нет веб-интерфейса для входа в систему, потому что проект представляет собой Rest API. OAuth 2.0 используется с паролем типа Grant.

Я хочу перейти на OAuth 2.1, но пароль типа Grant устарел.

Можете ли вы посоветовать в моем случае, как лучше всего перенести проект, чтобы снова иметь имя пользователя и пароль для выдачи токена, чтобы аутентифицировать пользователей и делать запросы к API?

Глядя на это руководство https://connect2id.com/learn/oauth-2-1, я думаю, что тип гранта носителя JWT является хорошим кандидатом?

Что, если я создам свой собственный тип гранта, аналогичный типу гранта пароля?

2 ответа

API REST, защищенные с помощью OAuth2, являются серверами ресурсов. Настройте свои приложения Spring как таковые .

Поток , используемый клиентами для получения токена доступа, не имеет отношения к серверам ресурсов . Не создавайте свои собственные. Клиенты используют:

  • authorization-codeдействовать от имени пользователя (физического лица, вошедшего в систему)
  • client-credentialsесли это программа, которой вы разрешаете выполнять запросы , не связанные с пользователем (пакетный процесс или любая другая доверенная служба)

Настройка сервера ресурсов для Keycloak с помощью начальных библиотек spring-boot, указанных выше, может быть такой простой, как:

      <dependency>
    <groupId>com.c4-soft.springaddons</groupId>
    <artifactId>spring-addons-webmvc-jwt-resource-server</artifactId>
    <version>6.0.4</version>
</dependency>
      @EnableMethodSecurity
public static class WebSecurityConfig { }
      com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/master
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,ressource_access.some-client.roles

com.c4-soft.springaddons.security.cors[0].path=/some-api

Настройка другого сервера авторизации OIDC, отличного от Keycloak, — это просто вопрос редактирования местоположения эмитента и утверждений властей.

Вы также можете использоватьspring-boot-starter-oauth2-resource-serverнепосредственно, как описано в первом уроке . Стартеры Spring-addons - это просто тонкие обертки вокруг него, которые сохраняют довольно много java conf. Вот что вам нужно написать, чтобы добиться того же, что и выше:

      @EnableWebSecurity
@EnableMethodSecurity
@Configuration
public class SecurityConfig {

    interface Jwt2AuthoritiesConverter extends Converter<Jwt, Collection<? extends GrantedAuthority>> {
    }

    @SuppressWarnings("unchecked")
    @Bean
    Jwt2AuthoritiesConverter authoritiesConverter() {
        // This is a converter for roles as embedded in the JWT by a Keycloak server
        // Roles are taken from both realm_access.roles & resource_access.{client}.roles
        return jwt -> {
            final var realmAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("realm_access", Map.of());
            final var realmRoles = (Collection<String>) realmAccess.getOrDefault("roles", List.of());

            final var resourceAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("resource_access", Map.of());
            // We assume here you have "spring-addons-confidential" and
            // "spring-addons-public" clients configured with "client roles" mapper in
            // Keycloak
            final var confidentialClientAccess = (Map<String, Object>) resourceAccess
                    .getOrDefault("spring-addons-confidential", Map.of());
            final var confidentialClientRoles = (Collection<String>) confidentialClientAccess.getOrDefault("roles",
                    List.of());
            final var publicClientAccess = (Map<String, Object>) resourceAccess.getOrDefault("spring-addons-public",
                    Map.of());
            final var publicClientRoles = (Collection<String>) publicClientAccess.getOrDefault("roles", List.of());

            return Stream
                    .concat(realmRoles.stream(),
                            Stream.concat(confidentialClientRoles.stream(), publicClientRoles.stream()))
                    .map(SimpleGrantedAuthority::new).toList();
        };
    }

    interface Jwt2AuthenticationConverter extends Converter<Jwt, AbstractAuthenticationToken> {
    }

    @Bean
    Jwt2AuthenticationConverter authenticationConverter(
            Converter<Jwt, Collection<? extends GrantedAuthority>> authoritiesConverter) {
        return jwt -> new JwtAuthenticationToken(jwt, authoritiesConverter.convert(jwt));
    }

    @Bean
    SecurityFilterChain filterChain(
            HttpSecurity http,
            Converter<Jwt, AbstractAuthenticationToken> authenticationConverter,
            ServerProperties serverProperties)
            throws Exception {

        // Enable OAuth2 with custom authorities mapping
        http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(authenticationConverter);

        // Enable anonymous
        http.anonymous();

        // Enable and configure CORS
        http.cors().configurationSource(corsConfigurationSource());

        // State-less session (state in access-token only)
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        // Disable CSRF because of state-less session-management
        http.csrf().disable();

        // Return 401 (unauthorized) instead of 403 (redirect to login) when
        // authorization is missing or invalid
        http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> {
            response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"Restricted Content\"");
            response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
        });

        // If SSL enabled, disable http (https only)
        if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
            http.requiresChannel().anyRequest().requiresSecure();
        } else {
            http.requiresChannel().anyRequest().requiresInsecure();
        }

        // Route security: authenticated to all routes but actuator and Swagger-UI
        // @formatter:off
        http.authorizeHttpRequests()
            .requestMatchers("/actuator/health/readiness", "/actuator/health/liveness", "/v3/api-docs", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
            .anyRequest().authenticated();
        // @formatter:on

        return http.build();
    }

    CorsConfigurationSource corsConfigurationSource() {
        // Very permissive CORS config...
        final var configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setExposedHeaders(Arrays.asList("*"));

        // Limited to API routes (neither actuator nor Swagger-UI)
        final var source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/greet/**", configuration);

        return source;
    }
}

Краткое изложение соответствующих типов грантов может быть следующим:

  • Пароль владельца ресурса: используется клиентом для сбора пароля пользователя и обмена его на сервере авторизации на токен доступа. Существует не так много хороших вариантов использования этого типа гранта, учитывая проблемы с безопасностью, и его следует заменить кодом авторизации. Устарело в OAuth 2.1.
  • Код авторизации: используется клиентом для инициирования потока на основе браузера с участием конечного пользователя для получения токена доступа с областями, предоставленными сервером авторизации и согласием пользователя. Предпочтительно в любое время, когда есть пользователь. Среды с ограниченным вводом (например, устройство без клавиатуры) могут альтернативно использовать грант авторизации устройства.
  • Учетные данные клиента: используются клиентом, владеющим секретом, выданным сервером авторизации, для получения токена доступа от своего имени. Полезно во многих архитектурах между службами с API без сохранения состояния (серверы ресурсов).
  • Носитель JWT: используется клиентом, владеющим JWT (утверждением), выданным одним сервером авторизации, для получения (обмена) токена доступа с другого сервера авторизации или с того же сервера авторизации, но с другими областями/полномочиями. Как получается исходный JWT, не уточняется. Может использоваться (например) для распространения учетных данных клиента для одного сервера аутентификации, который может выдавать токены, которые можно использовать для получения токенов доступа с одного или многих других серверов аутентификации в распределенной системе.

Если у вас нет более конкретных требований, предоставление учетных данных клиента может показаться лучшим вариантом.

Поскольку вы упомянули пользователей, но у вас нет пользовательского интерфейса, я предполагаю, что они не являются пользователями веб-клиента. Если это так, клиент (например, приложение javascript/SPA) должен использовать код авторизации. Ваше приложение не является клиентом, если оно принимает токены доступа, оно является сервером ресурсов.

Другие вопросы по тегам