Как аутентифицировать backend-to-backend с помощью Spring Boot/Keycloak

Я пытаюсь реализовать аутентификацию во всех своих внутренних службах приложения, ориентированного на микросервисы, с использованием Keycloak и Spring Boot с Spring Security и JWT-токенами (настройка только для носителя в Keycloak).

У меня есть серверная служба, которая требует аутентификации для доступа к конечным точкам REST. Эта служба предоставляет данные для веб-интерфейса, а также принимает данные для хранения в базе данных, чтобы их можно было обработать позже. Аутентификация пользователя в пользовательском интерфейсе, а также пользовательский интерфейс для этой серверной службы уже работают.

Затем есть еще одна серверная служба, которая работает в фоновом режиме и вычисляет значения, которые также должны присутствовать в первой упомянутой серверной службе. Поскольку для этого требуется аутентификация, служба, выполняющая вычисления, сначала должна получить токен доступа от Keycloak для аутентификации в другой серверной службе для работы HTTP-сообщения.

Я пытаюсь сделать сообщение HTTP с помощью KeycloakRestTemplate, но когда я вызываю метод .postForObject, я получаю исключение:

Caused by: java.lang.IllegalStateException: Cannot set authorization header because there is no authenticated principal
    at org.keycloak.adapters.springsecurity.client.KeycloakClientRequestFactory.getKeycloakSecurityContext(KeycloakClientRequestFactory.java:70)
    at org.keycloak.adapters.springsecurity.client.KeycloakClientRequestFactory.postProcessHttpRequest(KeycloakClientRequestFactory.java:55)
    at org.springframework.http.client.HttpComponentsClientHttpRequestFactory.createRequest(HttpComponentsClientHttpRequestFactory.java:160)

Похоже, что служба вычислений не извлекает токен аутентификации автоматически перед вызовом другой службы REST. Я провел много исследований в Google обо всех этих конкретных классах Keycloak, но я не нашел, что мне нужно делать.

Может ли кто-нибудь дать мне подсказку? Я также не знаю, какие части конфигурации Spring здесь актуальны, но я предоставлю их, если они вам понадобятся.

РЕДАКТИРОВАТЬ

Мой application.properties сервиса расчета выглядит так:

keycloak.auth-server-url=https://localhost/auth
keycloak.realm=myrealm
keycloak.bearer-only=true
keycloak.resource=backend-service2
keycloak.principal-attribute=preferred_username
keycloak.cors=true
keycloak.realm-key=<PUBKEY>
keycloak.credentials.secret=<SECRET_UUID_STYLE>
keycloak.use-resource-role-mappings=true

ОБНОВИТЬ

Спасибо @Sai prateek и @Xtreme Biker. Это, кажется, ведет меня в правильном направлении.

Я применил это решение, но все еще получаю исключение, я думаю, что конфигурация keycloak неверна. Сейчас у меня три клиента в keycloak: webui, backend-service1, backend-service2.

Webui настроен как: Тип доступа: общедоступный

Серверная служба1 настроена как: Тип доступа: только носитель

Backend-service2 настроен как: Тип доступа: только носитель

Исключение составляет:

2019-02-18 11:15:32.914 DEBUG 22620 --- [  restartedMain] o.s.web.client.RestTemplate              : POST request for "http://localhost:<PORT>/auth/realms/<REALM_NAME>/protocol/openid-connect/token" resulted in 400 (Bad Request); invoking error handler

Exception in thread "restartedMain" java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49)
Caused by: error = "access_denied", error_description = "Access token denied."
    at org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport.retrieveToken(OAuth2AccessTokenSupport.java:142)
    at org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider.obtainAccessToken(ClientCredentialsAccessTokenProvider.java:44)
    at org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainNewAccessTokenInternal(AccessTokenProviderChain.java:148)
    at org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainAccessToken(AccessTokenProviderChain.java:121)
    at org.springframework.security.oauth2.client.OAuth2RestTemplate.acquireAccessToken(OAuth2RestTemplate.java:221)
    at org.springframework.security.oauth2.client.OAuth2RestTemplate.getAccessToken(OAuth2RestTemplate.java:173)
    at org.springframework.security.oauth2.client.OAuth2RestTemplate.createRequest(OAuth2RestTemplate.java:105)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:683)
    at org.springframework.security.oauth2.client.OAuth2RestTemplate.doExecute(OAuth2RestTemplate.java:128)
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:644)
    at org.springframework.web.client.RestTemplate.postForObject(RestTemplate.java:399)
[STRIPPED]


    ... 5 more
Caused by: error = "invalid_client", error_description = "Bearer-only not allowed"
    at org.springframework.security.oauth2.common.exceptions.OAuth2ExceptionJackson2Deserializer.deserialize(OAuth2ExceptionJackson2Deserializer.java:80)
    at org.springframework.security.oauth2.common.exceptions.OAuth2ExceptionJackson2Deserializer.deserialize(OAuth2ExceptionJackson2Deserializer.java:33)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4001)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3072)
    at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:237)
    at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readInternal(AbstractJackson2HttpMessageConverter.java:217)
    at org.springframework.http.converter.AbstractHttpMessageConverter.read(AbstractHttpMessageConverter.java:198)
    at org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport$AccessTokenErrorHandler.handleError(OAuth2AccessTokenSupport.java:237)
    at org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63)
    at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:730)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:688)
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:654)
    at org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport.retrieveToken(OAuth2AccessTokenSupport.java:137)
    ... 18 more

Также обратите внимание, что я изменил keycloak.auth-server-url на http://localhost:<PORT>/auth (без HTTPS), чтобы проверка сертификата не завершалась ошибкой из-за того, что самоподписанный сертификат находится в разработке.

Без присутствия пользователя это может оказаться невозможным. Вызовы службы для обслуживания лучше всего аутентифицировать с использованием клиентских сертификатов SSL, но если ваша инфраструктура не способна на это, тогда будет работать простая базовая аутентификация с использованием ключа API (который является паролем с любым другим именем). Хотя это слишком много, чтобы обсуждать здесь.

Andy Brown 15.02.2019 16:30

Кажется, это имеет смысл. Вы знаете хороший учебник / пример? На что именно способна «инфраструктура»? Я думаю, что очень наивной реализацией будет предоставление вычислению закрытого ключа для загрузки при запуске, а другой службе нужен открытый ключ, чтобы он мог проверить подпись службы вычислений? Но что тогда, если закрытый ключ будет скомпрометирован... вы имеете в виду, что инфраструктура должна позаботиться о... обновлении ключей и т. д.?

Christian 15.02.2019 17:33

Вам понадобится инфраструктура PKI для выдачи/отмены/обновления доверенных сертификатов и механизм для распространения указанных сертификатов на серверах. Самоподписанные сертификаты будут работать в dev/test, но вам не нужны они в prod. Хорошей новостью является то, что обмен сертификатами является (необязательной) частью протокола SSL, а аутентификация X.509 является встроенной функцией Spring-Security (множество руководств в Интернете, которые помогут вам сдвинуться с места)

Andy Brown 15.02.2019 17:55

backend-service2 должен быть конфиденциальным Access-Type

ravthiru 18.02.2019 21:30

к сожалению, это тоже не работает. Я получаю ошибку Caused by: error = "access_denied", error_description = "Error requesting access token."

Christian 19.02.2019 08:54

Попробую настроить MCVE на github, следите за обновлениями

Christian 21.02.2019 11:00

Вот MCVE: github.com/CodingSpiderFox/keycloak-backend-to-backend-mcve. Не могли бы вы взглянуть на это?

Christian 21.02.2019 20:03

Еще не решил :/

Christian 28.02.2019 08:49

Решил сам и обновил MCVE тоже на GitHub

Christian 04.03.2019 20:28
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
7
9
7 852
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Похоже, вам не хватает какой-то конфигурации для сервера аутентификации. KeycloakRestTemplate использовал идентификатор клиента, секрет клиента, имя пользователя и пароль для проверки на сервере Keycloak. Вам нужно установить clientid, clientsecret, realm и URL-адрес сервера аутентификации для KeycloakClientCredentialsRestTemplate, например -

@Service
public class MyKeycloakClientCredentialsConfig {

    @Value("${keycloak.realm}")
    private String realm;

    @Value("${keycloak.auth-server-url}")
    private String authServerUrl;

    @Value("${keycloak.resource}")
    private String clientId;

    @Value("${keycloak.credentials.secret}")
    private String clientSecret;

    @Bean
    public KeycloakClientCredentialsRestTemplate createRestTemplate() {
        return new KeycloakClientCredentialsRestTemplate(getClientCredentialsResourceDetails(),
                new DefaultOAuth2ClientContext());
    }

    private ClientCredentialsResourceDetails getClientCredentialsResourceDetails() {
        String accessTokenUri = String.format("%s/realms/%s/protocol/openid-connect/token",
            authServerUrl, realm);
        List<String> scopes = new ArrayList<String>(0); // TODO introduce scopes

        ClientCredentialsResourceDetails clientCredentialsResourceDetails = 
                new ClientCredentialsResourceDetails();

        clientCredentialsResourceDetails.setAccessTokenUri(accessTokenUri);
        clientCredentialsResourceDetails.setAuthenticationScheme(AuthenticationScheme.header);
        clientCredentialsResourceDetails.setClientId(clientId);
        clientCredentialsResourceDetails.setClientSecret(clientSecret);
        clientCredentialsResourceDetails.setScope(scopes);

        return clientCredentialsResourceDetails;
    }

}

Мой шаблон отдыха такой:

public class SampleRestTemplate extends OAuth2RestTemplate {

    public KeycloakClientCredentialsRestTemplate(OAuth2ProtectedResourceDetails resource,
            OAuth2ClientContext context) {
        super(resource, context);
    }

}

он отлично работает для меня.

Я обновил свой вопрос с помощью application.properties. Я не совсем понимаю, как бы я использовал ваш пример для запроса. У вас есть минимальный рабочий пример?

Christian 15.02.2019 17:14

Этот ответ - путь. @pudelwudel вам нужно выполнить вход в систему как клиент (здесь вы действуете как клиент, а не от имени какого-либо пользователя). Вам может быть полезен этот вопрос: stackoverflow.com/questions/46073485/…

Xtreme Biker 16.02.2019 15:04

Теперь я на шаг впереди, пожалуйста, смотрите мой обновленный ответ;)

Christian 18.02.2019 11:34
Ответ принят как подходящий

Хорошо, я сам нашел решение: мне нужно было установить переключатель «Сервисные учетные записи включены» в положение ON в конфигурации клиента для «backend-service2» внутри keycloak.

ты спас мой день! <3

Janitha Madushan 11.11.2021 07:03

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