java - 如何将计划任务与还使用 keycloak 提供网页的客户端一起使用?

标签 java spring spring-boot resttemplate keycloak

我正在使用 Spring BootKeycloak 开发网络应用程序。 然后我写了一个计划任务,我在其中使用 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]

为什么?

如果我使用的是异步任务,我该如何传递主体?

编辑 这是我的安全配置:

@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();
}

编辑 这是我的 key 斗篷属性:

#######################################
#             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 而不是 client 和 secret?

编辑 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/**");
    }

}

编辑 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();

我做错了什么?

最佳答案

如果你想通过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();

用实际值替换clientIdclientSecret

更新:-

引用您在问题中提到的链接,您可以通过 keycloak 本身生成 jwt 承载 token (客户端访问 token )。获得 jwt token 后,您对资源服务器的后续请求应包含 header

Authorization: Bearer <jwt bearer token>

@Xtreme Biker 关于主题 Keycloak spring security client credential grant 的帖子中,他提供了一些示例代码,或许您可以使用拦截器方法实现此目的。

更多更新:-

根据 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 应该能够为您的请求添加正确的身份验证 header 。

更新 3:-

通过后 - https://github.com/keycloak/keycloak-documentation/blob/master/securing_apps/topics/oidc/java/spring-security-adapter.adoc ,我坚信 KeycloakRestTemplate 应该能够将所需的 jwt token 添加到请求的授权 header 中,因为它使用 KeycloakClientRequestFactoryKeycloakSecurityContext< 获取 token 字符串.

请尝试添加本文档中建议的所有配置,如所有过滤器 bean 配置

@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-kommunikation-via-keycloakresttemplate/&prev=search了解更多详情。

希望这能以某种方式帮助您解决问题。

关于java - 如何将计划任务与还使用 keycloak 提供网页的客户端一起使用?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49900124/

相关文章:

java - 在 JTable 中删除图像之间的空间

java - java中的同步块(synchronized block)

java - Mockito:我们可以使用 Mockito 模拟在测试类中使用 new 关键字调用的对象吗

javascript - 我能够将数据从 Spring boot Controller 传递到 html 页面,但是如何在 Java 脚本中访问数据?

java - 抛硬币游戏保存问题

java - 线程类并将信息传递到主 Activity 类到 TextView 中

Spring MVC 多部分响应

java - 你能@Autowired一个@MessageGateway到RestController中吗

java - 在 Spring Boot 应用程序中加载不同的应用程序上下文

java - JSP 在 Spring Boot 2 中不渲染 java 列表