Как я могу использовать запланированную задачу с клиентом, который также предоставляет веб-страницу с помощью keycloak?

Я использую Spring Boot и Keycloak для разработки веб-приложения. Затем я написал запланированную задачу, в которой я использую KeycloakRestTemplate, чтобы запрашивать некоторые данные у другого приложения, как вы можете видеть ниже:

    @Override
    @Scheduled(cron="0 50 09 * * MON-FRI")
    public void concludiCommessa() {

        try {
            FDto[] ftts = new ObjectMapper().readValue(restTemplate.getForEntity(URI.create(MY_URL), String.class).getBody(), FDto[].class);

             ..............................
            }
        } catch (RestClientException | IOException e) {
        }
    }

Если я запускаю его на сервере, у меня возникает следующая ошибка:

2018-04-18 09:50:00.067 ERROR 2503 --- [pool-8-thread-1] o.s.s.s.TaskUtils$LoggingErrorHandler    : Unexpected error occurred in scheduled task.

java.lang.IllegalStateException: Cannot set authorization header because there is no authenticated principal
    at org.keycloak.adapters.springsecurity.client.KeycloakClientRequestFactory.getKeycloakSecurityContext(KeycloakClientRequestFactory.java:70) ~[keycloak-spring-security-adapter-3.4.2.Final.jar:3.4.2.Final]
    at org.keycloak.adapters.springsecurity.client.KeycloakClientRequestFactory.postProcessHttpRequest(KeycloakClientRequestFactory.java:55) ~[keycloak-spring-security-adapter-3.4.2.Final.jar:3.4.2.Final]
    at org.springframework.http.client.HttpComponentsClientHttpRequestFactory.createRequest(HttpComponentsClientHttpRequestFactory.java:207) ~[spring-web-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.http.client.support.HttpAccessor.createRequest(HttpAccessor.java:85) ~[spring-web-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:656) ~[spring-web-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:636) ~[spring-web-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:336) ~[spring-web-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at it.edile.service.api.ApiServiceImpl.concludiCommessa(ApiServiceImpl.java:287) ~[classes/:0.0.1-SNAPSHOT]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_161]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_161]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_161]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_161]
    at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:65) ~[spring-context-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) ~[spring-context-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:81) [spring-context-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [na:1.8.0_161]
    at java.util.concurrent.FutureTask.run(FutureTask.java:266) [na:1.8.0_161]
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_161]
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) [na:1.8.0_161]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_161]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_161]
    at java.lang.Thread.run(Thread.java:748) [na:1.8.0_161]

Почему?

Как передать участника, если я использую асинхронную задачу?

EDIT Это моя конфигурация безопасности:

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
    auth.authenticationProvider(keycloakAuthenticationProvider());
}

@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
    return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}

@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public KeycloakRestTemplate keycloakRestTemplate() {
    return new KeycloakRestTemplate(keycloakClientRequestFactory);
}

@Bean
public KeycloakConfigResolver keycloakConfigResolver() {
    return new KeycloakSpringBootConfigResolver();
}

EDIT Это мои свойства keycloak:

#######################################
#             KEYCLOAK                #
#######################################
keycloak.realm=MY_REALM
keycloak.auth-server-url=MY_URL/auth
keycloak.ssl-required=external
keycloak.resource=EdilGest
keycloak.credentials.jwt.client-key-password=PWD
keycloak.credentials.jwt.client-keystore-file=classpath:CLIENT.jks
keycloak.credentials.jwt.client-keystore-password=PWD
keycloak.use-resource-role-mappings=true
keycloak.principal-attribute=preferred_username

ИЗМЕНИТЬ:

Сейчас я пытаюсь использовать учетную запись службы, но в данный момент она не работает... Читаю здесь: https://www.keycloak.org/docs/latest/server_admin/index.html#_service_accounts

Я должен отправить запрос типа:

POST /auth/realms/demo/protocol/openid-connect/token
    Authorization: Basic cHJvZHVjdC1zYS1jbGllbnQ6cGFzc3dvcmQ=
    Content-Type: application/x-www-form-urlencoded

    grant_type=client_credentials

для keycloak, но как я могу отправить его с помощью Spring? и как я могу установить jks вместо клиента и секрета?

ИЗМЕНИТЬ 2

Моя конфигурация безопасности

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {  

    @Autowired
    public KeycloakClientRequestFactory keycloakClientRequestFactory;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);

        http
            .httpBasic()
            .disable();

        http
        .authorizeRequests()
            .antMatchers("/webjars/**").permitAll()
            .antMatchers("/resources/**").permitAll()
            .anyRequest().hasAuthority("......")
        .and()
        .logout()
            .logoutUrl("/logout")
            .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
            .permitAll()
            .logoutSuccessUrl(mux)
            .invalidateHttpSession(true);

    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(keycloakAuthenticationProvider());
    }

    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public KeycloakRestTemplate keycloakRestTemplate() {
        return new KeycloakRestTemplate(keycloakClientRequestFactory);
    }

    @Bean
    public KeycloakConfigResolver keycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }

    @Bean
    public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(KeycloakAuthenticationProcessingFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(KeycloakPreAuthActionsFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web
           .ignoring()
           .antMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**", "/webjars/**");
    }

}

EDIT 3 Вот что я пробовал... Не работает. У меня та же ошибка: java.lang.IllegalStateException: Cannot set authorization header because there is no authenticated principal

KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("EdilGest.jks"), "EdilGest".toCharArray());

JWTClientCredentialsProvider jwtClientCredentialsProvider = new JWTClientCredentialsProvider();
jwtClientCredentialsProvider.setupKeyPair(keyStoreKeyFactory.getKeyPair("MyClient"));
String token = jwtClientCredentialsProvider.createSignedRequestToken("MyClient", "http://myKeycloak/auth/");

String data = "grant_type=client_credentials" ;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.add(HttpHeaders.AUTHORIZATION, "Bearer " +token);

HttpEntity<String> requestEntity = new HttpEntity<String>(data, headers);
String ftt = keycloakRestTemplate.exchange(URI.create(MyUrl), HttpMethod.POST, requestEntity, String.class).getBody();

Что я делаю не так?


person Teo    schedule 18.04.2018    source источник
comment
Работает ли этот код, если вы вынесете его из метода @Scheduled и обернете его в стандартный метод, который вызывает пользователь?   -  person Xtreme Biker    schedule 19.04.2018
comment
Да, это так! Если я выполнил метод как метод stadarnd, он работает отлично   -  person Teo    schedule 19.04.2018
comment
Где вы настроили учетные данные клиента?   -  person Xtreme Biker    schedule 19.04.2018
comment
Я использую JWT, я добавил конфигурацию в свойствах. Пожалуйста, взгляните   -  person Teo    schedule 19.04.2018
comment
Там нет принципала, потому что ни один пользователь не вошел в систему. Здесь вам нужно войти в свое приложение как клиент. См. мой пост здесь stackoverflow.com/a/46116485/1199132 Я думаю, что в java-адаптерах по-прежнему нет поддержки учетных записей служб.   -  person Xtreme Biker    schedule 19.04.2018
comment
@XtremeBiker да, я знаю это ... Но мне нужно запланировать этот метод, поэтому я не вхожу в систему. Можно ли передать некоторые учетные данные без входа в систему? Или иначе я думаю создать другую конфигурацию безопасности только для запроса api... Например, для api я думаю не использовать SSO с keycloak, а стандартную аутентификацию, таким образом, возможно, я могу использовать Spring Rest Шаблон.. Что вы думаете? Можно ли передать учетные данные с помощью шаблона Spring Rest?   -  person Teo    schedule 19.04.2018
comment
@XtremeBiker хорошо, может быть, мой комментарий совпадает с вашим ответом ...   -  person Teo    schedule 19.04.2018
comment
Лучше использовать сервисный аккаунт ;-)   -  person Xtreme Biker    schedule 19.04.2018
comment
@XtremeBiker, возможно, я не понимаю, как это работает, но я пытаюсь сделать это: keycloakRestTemplate.postForEntity(/realms/REALM/protocol/openid-connect/token, null, String.class).getBody(); и я получаю 403... Как я могу также передать JWT в keycloak?   -  person Teo    schedule 20.04.2018
comment
Вы выполнили все необходимые шаги, указанные здесь для работы с JWT? Используете ли вы URL-адрес для обслуживания открытых ключей клиента или импортируете их в keycloak?   -  person Xtreme Biker    schedule 20.04.2018
comment
@XtremeBiker да, я это сделал .. Как видите, у меня есть все эти свойства внутри application.properties ... У меня есть org.springframework.web.client.HttpClientErrorException: 400 Bad Request сейчас, потому что до того, как я написал неправильный URL-адрес   -  person Teo    schedule 20.04.2018
comment
400 Bad Request: у вас будет сообщение об ошибке на стороне сервера, сообщающее вам, что вы пропустили. Или, если вы проанализируете ответ, вы увидите его в части message.   -  person Xtreme Biker    schedule 20.04.2018
comment
@XtremeBiker Я не нахожу никакого сообщения об ошибке. Я проверил Keycloak на своем сервере, но ничего. Затем я попытался понять, что вы предложили [stackoverflow.com/questions/46073485/ учетные записи), но я не не знаю как реализовать...   -  person Teo    schedule 21.04.2018
comment
Это имеет дополнительную сложность использования JWT в качестве ключа клиента. Сначала вы должны настроить свой код для работы с Аутентификация клиента JWT, которую я никогда не делал сам. Затем вам нужно будет управлять этим потоком аутентификации для каждого из запросов, сделанных в потоке @Async. Я не знаю, есть ли у вас возможность использовать учетные данные клиента, но это было бы проще реализовать. РЕДАКТИРОВАТЬ: похоже, что в адаптере Spring Boot проделана некоторая работа: stackoverflow.com/a/46784443/1199132   -  person Xtreme Biker    schedule 25.04.2018
comment
Из вашего вопроса, по сравнению с документами, я заметил, что вам не хватает, чтобы сообщить адаптеру псевдоним, который будет использоваться в файле хранилища ключей. Я также попытался бы выполнить дальнейшую отладку как на стороне keycloak (запустить сервер с повышенным порогом ведения журнала), так и на клиенте (попробовать установить некоторые точки останова в JWTClientCredentialsProvider) и связанные классы, чтобы попытаться увидеть, что происходит. на.   -  person Xtreme Biker    schedule 29.04.2018


Ответы (3)


Если вы хотите отправить запрос, как показано ниже, через spring

POST /auth/realms/demo/protocol/openid-connect/token
    Authorization: Basic cHJvZHVjdC1zYS1jbGllbnQ6cGFzc3dvcmQ=
    Content-Type: application/x-www-form-urlencoded

    grant_type=client_credentials

что вам нужно, это что-то вроде

RestTemplate template = new RestTemplate();
String uri = "https://host:port/auth/realms/demo/protocol/openid-connect/token";
String data = "grant_type=client_credentials" ;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.add(HttpHeaders.AUTHORIZATION, "Basic " + Base64Utils.encodeToString("clientId:clientSecret".getBytes()));
HttpEntity<String> requestEntity = new HttpEntity<String>(data, headers);

ResponseEntity<JsonNode> result = template.exchange(uri, HttpMethod.POST, requestEntity, JsonNode.class);
JsonNode jn = result.getBody();
String access_token = jn.get("access_token").asText();

Замените clientId и clientSecret фактическими значениями.

Обновление:-

Ссылаясь на ссылку, которую вы упомянули в своем вопросе, вы можете сгенерировать токен носителя jwt (токен клиентского доступа) с помощью самого keycloak. Как только вы получите токен jwt, ваши последующие запросы к серверу ресурсов должны содержать заголовок

Authorization: Bearer <jwt bearer token>

В сообщении @Xtreme Biker в теме Keycloak spring security client предоставление учетных данных , он предоставил пример кода, возможно, как вы можете добиться этого, используя подход перехватчика.

БОЛЬШЕ ОБНОВЛЕНИЙ:-

Согласно документам keycloak - https://www.keycloak.org/docs/3.1/securing_apps/topics/oidc/java/java-adapter-config.html

чтобы установить jks, вы должны иметь следующие свойства в файле application.properties.

keycloak.client-keystore-password=PWD
keycloak.client-keystore=classpath:CLIENT.jks
keycloak.client-key-password=PWD

и в соответствии с - https://www.keycloak.org/docs/3.1/securing_apps/topics/oidc/java/spring-security-adapter.html , как только вы правильно настроите jks, KeycloakRestTemplate сможет добавить правильный заголовок аутентификации в ваш запрос.

ОБНОВЛЕНИЕ 3 :-

После прохождения - https://github.com/keycloak/keycloak-documentation/blob/master/securing_apps/topics/oidc/java/spring-security-adapter.adoc, я твердо верю, что KeycloakRestTemplate должен иметь возможность добавлять необходимые jwt в заголовок авторизации запроса, потому что он использует KeycloakClientRequestFactory, который получает строку токена из KeycloakSecurityContext.

Пожалуйста, попробуйте добавить все конфигурации, предложенные в этом документе, как и все конфигурации компонентов фильтра.

@Bean
    public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(
            KeycloakAuthenticationProcessingFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(
            KeycloakPreAuthActionsFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean keycloakAuthenticatedActionsFilterBean(
            KeycloakAuthenticatedActionsFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean keycloakSecurityContextRequestFilterBean(
        KeycloakSecurityContextRequestFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

и убедитесь, что вы указали все необходимые свойства в файле application.properties.

Поскольку вы отправляете запрос из запланированной задачи, которая может быть асинхронной по своей природе, вам, возможно, придется изменить стратегию в конструкторе вашего SecurityConfig, чтобы вместо ThreadLocal SecurityContext использовалось InheritableThreadLocal, которое передает эту информацию при создании дочерних потоков.

public SecurityConfig ( KeycloakClientRequestFactory keycloakClientRequestFactory ) {
         this.keycloakClientRequestFactory = keycloakClientRequestFactory ;

         // to use principal and authentication together with @ async
         SecurityContextHolder.setStrategyName ( SecurityContextHolder.MODE_INHERITABLETHREADLOCAL ) ;

     }

Ссылка — https://translate.google.co.in/translate?hl=en&sl=de&u=https://blog.codecentric.de/2017/09/keycloak-und-spring-security-teil.-3-коммуникация-через-keycloakresttemplate/&prev=search для более подробной информации.

Надеюсь, это хоть как-то поможет вам в решении вашей проблемы.

person vsoni    schedule 29.04.2018
comment
Спасибо .. Я думаю, что ваш ответ будет полезен, но я использую JWT, поэтому у меня нет клиента и секрета, но вместо этого у меня есть JKS .. Итак, вы знаете, как я могу отправить или использовать JKS запросить токен доступа? - person Teo; 29.04.2018
comment
@Teo - пожалуйста, посмотрите еще несколько комментариев к моему ответу, если это поможет вам каким-то образом решить вашу проблему. - person vsoni; 01.05.2018
comment
извините, но я взял несколько выходных .. Я только что проверил то, что вы предложили, но у меня есть то же исключение: java.lang.IllegalStateException: Cannot set authorization header because there is no authenticated principal - person Teo; 02.05.2018
comment
@Teo - ref - stackoverflow.com/questions /48752674/ . Он смог установить заголовок аутентификации в своей конфигурации. - person vsoni; 02.05.2018
comment
@Teo - возможно, вы можете попробовать добавить свойство keycloak.bearer-only=true в application.properties - person vsoni; 02.05.2018
comment
Я использую конфиденциальный, а не только носитель.. Тогда конфигурация в основном такая же.. пожалуйста, взгляните на мое редактирование.. - person Teo; 02.05.2018
comment
просто для ясности.. то, что я хотел бы получить, это именно то, что вы написали в разделе update... я не могу получить токен доступа из keycloak... - person Teo; 03.05.2018
comment
это еще не работает.. Я добавил то, что вы предложили, но ничего... Я думаю, что, возможно, причина в том, что я не using-bearer only=true.. Но это веб-приложение также обслуживает веб-страницу, поэтому я могу' t установите для этого свойства значение true.. Имеет ли это смысл для вас? - person Teo; 04.05.2018
comment
@Teo - я уверен, что нам не хватает какой-то ссылки ... какой-то точки конфигурации ... в любом случае, если вы не хотите мешать другим частям вашего приложения, вы можете вручную создать токен jwt и установить его для остального шаблона. Поскольку у вас уже есть закрытый ключ, вы можете легко создать токен jwt. - person vsoni; 04.05.2018
comment
@Teo - укажите этот код адаптера, чтобы узнать, как вы можете самостоятельно создать токен jwt. github.com/keycloak/keycloak/blob/master/adapters/oidc/ . Вероятно, вы можете использовать этот класс как есть (этот класс уже является частью вашей библиотеки адаптера), вызвав некоторую функцию, например createSignedRequestToken(), которая может вернуть вам токен jwt. - person vsoni; 04.05.2018
comment
мммм .. и после того, как я его сгенерирую, как я могу установить его в свой KeycloakRestTemplate? - person Teo; 04.05.2018
comment
@Teo - если вы настраиваете заголовок авторизации вручную, вы можете использовать любой из шаблонов RestTemplate или KeycloakRestTemplate. Я уже предоставил образец для добавления основного заголовка авторизации. Для предъявителя это будет headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + jwttoken); - person vsoni; 04.05.2018
comment
Сейчас я чувствую себя очень глупо, но, пожалуйста, взгляните на EDIT 4.. Спасибо! - person Teo; 07.05.2018

По умолчанию SecurityContextHolder использует стратегию THREAD_LOCAL, поэтому другие потоки не наследуют контекст от своего родителя. Посмотрите Javadoc для MODE_INHERITABLETHREADLOCAL, чтобы узнать, соответствует ли он вашим потребностям.

person p3consulting    schedule 25.04.2018
comment
Я не думаю, что это решение или проблема. Во всяком случае, я пытался, но ничего не изменилось. - person Teo; 27.04.2018

Ваш метод, являющийся @Scheduled, выполняется в потоке, отличном от потока вызывающего. SecurityContextHolder, являющийся локальным потоком, не будет распространять контекст безопасности на запланированный поток. Одним из способов может быть изменение стратегии по умолчанию для SecurityContextHolder с THREAD_LOCAL на MODE_INHERITABLETHREADLOCAL путем добавления этой строки в конструктор по умолчанию класса @Configuration.

SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);

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

person Mihai Ratiu    schedule 26.04.2018
comment
Я не думаю, что это решение или проблема. Во всяком случае, я пытался, но ничего не изменилось. - person Teo; 27.04.2018