У меня есть старый шлюз Spring Cloud, работающий с сервером Keyclock. У меня нет веб-интерфейса для входа в систему, потому что проект представляет собой Rest API. OAuth 2.0 используется с паролем типа Grant.
Я хочу перейти на OAuth 2.1, но пароль типа Grant устарел.
Можете ли вы посоветовать в моем случае, как лучше всего перенести проект, чтобы снова иметь имя пользователя и пароль для выдачи токена, чтобы аутентифицировать пользователей и делать запросы к API?
Глядя на это руководство https://connect2id.com/learn/oauth-2-1 Я думаю, что тип гранта носителя JWT является хорошим кандидатом?
Что, если я создам свой собственный тип гранта, аналогичный типу гранта пароля?
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;
}
}
Не могли бы вы показать мне расширенный пример использования spring-boot-starter-oauth2-resource-server
с client-credentials
, пожалуйста?
Прочитайте мой ответ еще раз: ресурс-сервер не заботится о потоке, который клиент использовал для получения токена доступа. Запрос, авторизованный с помощью токена доступа Bearer, — это все, что имеет значение для сервера ресурсов. Обратитесь к документу вашего сервера авторизации, чтобы узнать, как объявить клиента с учетными данными клиента. Перейдите по ссылке в моем ответе на образец с spring-boot-starter-oauth2-resource-server
.
У меня есть еще один вопрос, есть ли подходящее решение для этого варианта использования: клиент использует токен, созданный вручную каким-то администратором и вручную отправленный пользователю. Этот токен используется для отправки запросов в заголовок запросов API с использованием «Basic ...», и токен используется без истечения срока действия по умолчанию. Если возможно, токен каждый раз аутентифицируется на сервере авторизации.
Это зависит от вашего сервера авторизации. Обратитесь к его документу. Насколько мне известно, Keycloak не позволяет этого, и, учитывая, насколько легко настроить клиент с учетными данными клиента для автоматического получения токена доступа с сервера авторизации, я не вижу допустимого варианта использования.
Краткое изложение соответствующих типов грантов может быть следующим:
Если у вас нет более конкретных требований, предоставление учетных данных клиента может показаться лучшим вариантом.
Поскольку вы упомянули пользователей, но у вас нет пользовательского интерфейса, я предполагаю, что они не являются пользователями веб-клиента. Если это так, клиент (например, приложение javascript/SPA) должен использовать код авторизации. Ваше приложение не является клиентом, если оно принимает токены доступа, оно является сервером ресурсов.
Спасибо за ответ. Я тоже думаю, что Client credentials
пока это вариант решения. У меня есть еще один вопрос, есть ли подходящее решение для этого варианта использования: клиент использует токен, созданный вручную каким-то администратором и вручную отправленный пользователю. Этот токен используется для отправки запросов в заголовок запросов API с использованием «Basic ...», и токен используется без истечения срока действия по умолчанию. Если возможно, токен каждый раз аутентифицируется на сервере авторизации.
По вашему описанию трудно сказать, описываете ли вы «ключ API» или что-то еще. Если он использует «Basic», это просто базовый HTTP, что означает, что имя пользователя/пароль закодированы в токен. Было бы небезопасно, если бы вы хотели сохранить пароль в секрете от пользователя. Вы всегда можете попросить оператора/администратора использовать свои собственные учетные данные клиента для создания токена, но обычно не рекомендуется делиться ими с кем-то еще. Я считаю, что у каждого клиента должен быть свой секрет.
Просто интересно, почему этот вопрос помечен как spring-authorization-server? Кажется, это вообще не относится к Spring или SAS.