使用 Keycloak 保护 Spring Boot REST 服务时的 NullPointer
Posted
技术标签:
【中文标题】使用 Keycloak 保护 Spring Boot REST 服务时的 NullPointer【英文标题】:NullPointer when securing Spring Boot REST Service with Keycloak 【发布时间】:2021-02-04 04:36:36 【问题描述】:我正在尝试按照文章 https://medium.com/devops-dudes/securing-spring-boot-rest-apis-with-keycloak-1d760b2004e 中的说明使用 Keycloak 保护 Spring Boot REST 服务。我将 keycloak 和 my-service 作为 docker 服务启动(请参阅下面的 docker-compose.yml)
那我先
curl -s -X POST http://localhost:8089/auth/realms/<realm>/protocol/openid-connect/token \
--header "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=password" \
--data-urlencode "client_id=<my-app>" \
--data-urlencode "client_secret=<secret>" \
--data-urlencode "username=<user>" \
--data-urlencode "password=<password>"
然后在将环境变量 $TOKEN 的值设置为上述调用返回的 access_token 后,尝试调用 my-service 上的端点:
curl -s --location --request POST http://localhost:8082/myendpoint --header "Authorization: Bearer $TOKEN"
当我启动最后一个 curl 时,spring-boot 服务会引发 NullPointerException,如下面的堆栈跟踪所示。
spring-boot-starter-parent 版本=1.5.12.RELEASE Keycloak version=3.4.3:Final
以下是KeycloakSecurityConfig
类:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy()
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
@Bean
public KeycloakConfigResolver KeycloakConfigResolver()
return new KeycloakSpringBootConfigResolver();
@Override
protected void configure(HttpSecurity http) throws Exception
super.configure(http);
http.authorizeRequests().anyRequest().permitAll();
http.csrf().disable();
这是pom.xml
文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>my.app</groupId>
<artifactId>my-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>my-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<docker.image.prefix>my.app</docker.image.prefix>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- spring boot actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- spring data rest -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Keycloak -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
<version>3.4.3.Final</version>
</dependency>
</dependencies>
<build>
<finalName>$project.artifactId</finalName>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<configuration>
<mainClass>$main.class</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.2.3</version>
<configuration>
<imageName>$docker.image.prefix/$project.artifactId:$project.version</imageName>
<dockerDirectory>src/main/docker</dockerDirectory>
<resources>
<resource>
<directory>$project.build.directory</directory>
<include>$project.build.finalName.jar</include>
</resource>
</resources>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</pluginRepository>
</pluginRepositories>
</project>
这是docker-compose.yml
文件:
version: '3.1'
services:
my-service-db:
.
.
.
my-service:
image: my.app/my-service:0.0.1-SNAPSHOT
container_name: my-service
ports:
- 8082:8080
links:
- my-service-db
environment:
.
.
.
# keycloak connector values
KEYCLOAK_REALM: <realm>
KEYCLOAK_AUTH_SERVER_URL: keycloak:8089/auth
KEYCLOAK_SSL_REQUIRED: external
KEYCLOAK_RESOURCE: <my-service>
KEYCLOAK_CREDENTIAL_SECRET: <secret>
.
.
.
# KEYCLOAK INFRASTRUCTURE
keycloak-db:
image: mysql:5.6
container_name: keycloak-db
command: "--innodb_use_native_aio=0"
expose:
- "3306"
ports:
- "3308:3306"
volumes:
- ./keycloak-mysql-data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: <password>
MYSQL_ROOT_HOST: "%"
MYSQL_DATABASE: keycloak
MYSQL_USER: <user>
MYSQL_PASSWORD: <password>
keycloak:
container_name: keycloak
# restart: always
image: jboss/keycloak:3.4.3.Final
depends_on:
- keycloak-db
environment:
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: <password>
MYSQL_DATABASE: keycloak
MYSQL_USER: <user>
MYSQL_PASSWORD: <password>
# Workaround for container using legacy Docker links, resulting in
# "WFLYCTL0211: Cannot resolve expression 'jdbc:mysql://$env.MYSQL_PORT_3306_TCP_ADDR:$env.MYSQL_PORT_3306_TCP_PORT
# see: https://issues.redhat.com/browse/KEYCLOAK-3873?page=com.atlassian.jira.plugin.system.issuetabpanels%3Achangehistory-tabpanel
MYSQL_PORT_3306_TCP_ADDR: keycloak-db
MYSQL_PORT_3306_TCP_PORT: "3306"
ports:
- 8089:8080
# - 8443:8443
# - 9990:9990
volumes:
- ./keycloak-data:/data
最后,这是错误堆栈跟踪:
2020-10-21 09:00:31.614 ERROR 1 --- [io-8080-exec-10] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
java.lang.NullPointerException: null
at java.net.URI$Parser.parse(URI.java:3042) ~[na:1.8.0_111]
at java.net.URI.<init>(URI.java:588) ~[na:1.8.0_111]
at java.net.URI.create(URI.java:850) ~[na:1.8.0_111]
at org.apache.http.client.methods.HttpGet.<init>(HttpGet.java:66) ~[httpclient-4.5.5.jar!/:4.5.5]
at org.keycloak.adapters.rotation.JWKPublicKeyLocator.sendRequest(JWKPublicKeyLocator.java:97) ~[keycloak-adapter-core-3.4.3.Final.jar!/:3.4.3.Final]
at org.keycloak.adapters.rotation.JWKPublicKeyLocator.getPublicKey(JWKPublicKeyLocator.java:63) ~[keycloak-adapter-core-3.4.3.Final.jar!/:3.4.3.Final]
at org.keycloak.adapters.rotation.AdapterRSATokenVerifier.getPublicKey(AdapterRSATokenVerifier.java:44) ~[keycloak-adapter-core-3.4.3.Final.jar!/:3.4.3.Final]
at org.keycloak.adapters.rotation.AdapterRSATokenVerifier.verifyToken(AdapterRSATokenVerifier.java:55) ~[keycloak-adapter-core-3.4.3.Final.jar!/:3.4.3.Final]
at org.keycloak.adapters.rotation.AdapterRSATokenVerifier.verifyToken(AdapterRSATokenVerifier.java:37) ~[keycloak-adapter-core-3.4.3.Final.jar!/:3.4.3.Final]
at org.keycloak.adapters.BearerTokenRequestAuthenticator.authenticateToken(BearerTokenRequestAuthenticator.java:99) ~[keycloak-adapter-core-3.4.3.Final.jar!/:3.4.3.Final]
at org.keycloak.adapters.BearerTokenRequestAuthenticator.authenticate(BearerTokenRequestAuthenticator.java:84) ~[keycloak-adapter-core-3.4.3.Final.jar!/:3.4.3.Final]
at org.keycloak.adapters.RequestAuthenticator.authenticate(RequestAuthenticator.java:68) ~[keycloak-adapter-core-3.4.3.Final.jar!/:3.4.3.Final]
at org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter.attemptAuthentication(KeycloakAuthenticationProcessingFilter.java:147) ~[keycloak-spring-security-adapter-3.4.3.Final.jar!/:3.4.3.Final]
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:212) ~[spring-security-web-4.2.5.RELEASE.jar!/:4.2.5.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.2.5.RELEASE.jar!/:4.2.5.RELEASE]
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116) ~[spring-security-web-4.2.5.RELEASE.jar!/:4.2.5.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.2.5.RELEASE.jar!/:4.2.5.RELEASE]
at org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter.doFilter(KeycloakPreAuthActionsFilter.java:84) ~[keycloak-spring-security-adapter-3.4.3.Final.jar!/:3.4.3.Final]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.2.5.RELEASE.jar!/:4.2.5.RELEASE]
at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:66) ~[spring-security-web-4.2.5.RELEASE.jar!/:4.2.5.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.16.RELEASE.jar!/:4.3.16.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.2.5.RELEASE.jar!/:4.2.5.RELEASE]
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105) ~[spring-security-web-4.2.5.RELEASE.jar!/:4.2.5.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.2.5.RELEASE.jar!/:4.2.5.RELEASE]
at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56) ~[spring-security-web-4.2.5.RELEASE.jar!/:4.2.5.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.16.RELEASE.jar!/:4.3.16.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.2.5.RELEASE.jar!/:4.2.5.RELEASE]
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:214) ~[spring-security-web-4.2.5.RELEASE.jar!/:4.2.5.RELEASE]
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:177) ~[spring-security-web-4.2.5.RELEASE.jar!/:4.2.5.RELEASE]
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:347) ~[spring-web-4.3.16.RELEASE.jar!/:4.3.16.RELEASE]
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:263) ~[spring-web-4.3.16.RELEASE.jar!/:4.3.16.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) ~[spring-web-4.3.16.RELEASE.jar!/:4.3.16.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.16.RELEASE.jar!/:4.3.16.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:109) ~[spring-web-4.3.16.RELEASE.jar!/:4.3.16.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.16.RELEASE.jar!/:4.3.16.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:81) ~[spring-web-4.3.16.RELEASE.jar!/:4.3.16.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.16.RELEASE.jar!/:4.3.16.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197) ~[spring-web-4.3.16.RELEASE.jar!/:4.3.16.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.16.RELEASE.jar!/:4.3.16.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.springframework.boot.actuate.autoconfigure.MetricsFilter.doFilterInternal(MetricsFilter.java:106) ~[spring-boot-actuator-1.5.12.RELEASE.jar!/:1.5.12.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.16.RELEASE.jar!/:4.3.16.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198) ~[tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.keycloak.adapters.tomcat.AbstractAuthenticatedActionsValve.invoke(AbstractAuthenticatedActionsValve.java:67) [spring-boot-container-bundle-3.4.3.Final.jar!/:3.4.3.Final]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:496) [tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.keycloak.adapters.tomcat.AbstractKeycloakAuthenticatorValve.invoke(AbstractKeycloakAuthenticatorValve.java:181) [spring-boot-container-bundle-3.4.3.Final.jar!/:3.4.3.Final]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140) [tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81) [tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87) [tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342) [tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:803) [tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) [tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:790) [tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1459) [tomcat-embed-core-8.5.29.jar!/:8.5.29]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-8.5.29.jar!/:8.5.29]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_111]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_111]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.5.29.jar!/:8.5.29]
at java.lang.Thread.run(Thread.java:745) [na:1.8.0_111]
【问题讨论】:
【参考方案1】:我遇到了同样的问题,这与后端服务器无法访问 keycloack 服务器有关。 我的后端服务器的配置:
keycloak:
realm: myrealm
auth-server-url: http://192.168.102.12/auth/
当地址可用时(你可以检查 telnet)一切都开始工作了。
telnet 192.168.102.12 80
我在日志中遇到了与您相同的错误,但您必须在日志中查看上方:
2020-11-27 16:43:38.879 WARN 14688 --- [nio-7113-exec-1] o.keycloak.adapters.KeycloakDeployment : Failed to load URLs from http://192.168.102.12/auth/realms/myrealm/.well-known/openid-configuration
java.net.ConnectException: Connection refused
at java.base/java.net.PlainSocketImpl.socketConnect(Native Method) ~[na:na]
at java.base/java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:399) ~[na:na]
因为这个日志在下面:
2020-11-27 16:43:38.885 ERROR 14688 --- [nio-7113-exec-1] o.a.c.c.C.[.[.[.[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [/mybackendapp] threw exception
java.lang.NullPointerException: null
at java.base/java.net.URI$Parser.parse(URI.java:3104) ~[na:na]
at java.base/java.net.URI.<init>(URI.java:600) ~[na:na]
at java.base/java.net.URI.create(URI.java:881) ~[na:na]
at org.apache.http.client.methods.HttpGet.<init>(HttpGet.java:66) ~[httpclient-4.5.12.jar!/:4.5.12]
at org.keycloak.adapters.rotation.JWKPublicKeyLocator.sendRequest(JWKPublicKeyLocator.java:97) ~[keycloak-adapter-core-10.0.1.jar!/:10.0.1]
at org.keycloak.adapters.rotation.JWKPublicKeyLocator.getPublicKey(JWKPublicKeyLocator.java:63) ~[keycloak-adapter-core-10.0.1.jar!/:10.0.1]
【讨论】:
以上是关于使用 Keycloak 保护 Spring Boot REST 服务时的 NullPointer的主要内容,如果未能解决你的问题,请参考以下文章
使用 Keycloak 保护 Spring Boot REST 服务时的 NullPointer
需要使用 Keycloak 和 oAuth2 保护普通 Spring REST API 的示例
如何使用 Keycloak 保护 Angular 8 前端和使用网关、eureka 的 Java Spring Cloud 微服务后端