Перенос 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) должен использовать код авторизации. Ваше приложение не является клиентом, если оно принимает токены доступа, оно является сервером ресурсов.