如何在 Keycloak 中使用 Spring Security RemoteTokenService
How to use Spring Security RemoteTokenService with Keycloak
我设置了一个 Keycloak 服务器。配置领域和客户端等。
我成功地使用 "org.keycloak:keycloak-spring-boot-starter" 编写了 Spring 引导服务并保护了我的 RestController。很有魅力。
但是当我尝试使用 Spring 安全性(没有 keycloak 特定依赖项)时,我被卡住了。
这是我的 gradle:
dependencies {
compile('org.springframework.boot:spring-boot-starter-security')
compile('org.springframework.security.oauth:spring-security-oauth2')
compile('org.springframework.boot:spring-boot-starter-web')
compileOnly('org.projectlombok:lombok')
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('org.springframework.security:spring-security-test')
}
这是我的安全配置:
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends
ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/person/**").hasRole("DEMO_SPRING_SECURITY")
.anyRequest().authenticated()
.and().formLogin().disable();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("demo-client");
RemoteTokenServices tokenServices = new RemoteTokenServices();
tokenServices.setCheckTokenEndpointUrl(
"http://localhost:8280/auth/realms/demo-realm/protocol/openid-connect/token/introspect");
tokenServices.setClientId("demo-client");
tokenServices.setClientSecret("80e19056-7770-4a4a-a3c4-06d8ac8792ef");
resources.tokenServices(tokenServices);
}
}
现在我尝试访问服务器:
- 获取访问令牌(通过 REST 客户端)
解码后的 JWT 如下所示:
{
"jti": "78c00562-d80a-4f5a-ab08-61ed10cb575c",
"exp": 1509603570,
"nbf": 0,
"iat": 1509603270,
"iss": "http://localhost:8280/auth/realms/demo-realm",
"aud": "demo-client",
"sub": "6ee90ba4-2854-49c1-9776-9aa95b6ae598",
"typ": "Bearer",
"azp": "demo-client",
"auth_time": 0,
"session_state": "68ce12fb-3b3f-429d-9390-0662f0503bbb",
"acr": "1",
"client_session": "ec0113e1-022a-482a-a26b-e5701e5edec1",
"allowed-origins": [],
"realm_access": {
"roles": [
"demo_user_role",
"uma_authorization"
]
},
"resource_access": {
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"name": "Jim Panse",
"preferred_username": "demo-user",
"given_name": "Jim",
"family_name": "Panse",
"email": "user@dmoain.com"
}
但是我得到一个 AccessDeniedException。
2017-11-02 07:18:05.344 DEBUG 17637 --- [nio-8080-exec-1] o.s.s.w.a.i.FilterSecurityInterceptor : Previously Authenticated:
org.springframework.security.oauth2.provider.OAuth2Authentication@1f3ee7e1:
Principal: demo-client; Credentials: [PROTECTED]; Authenticated: true;
Details: remoteAddress=127.0.0.1, tokenType=BearertokenValue=;
Not granted any authorities 2017-11-02 07:18:05.348 DEBUG 17637 ---
[nio-8080-exec-1] o.s.s.access.vote.AffirmativeBased : Voter:
org.springframework.security.web.access.expression.WebExpressionVoter@14032696,
returned: -1 2017-11-02 07:18:05.353 DEBUG 17637 --- [nio-8080-exec-1]
o.s.s.w.a.ExceptionTranslationFilter : Access is denied (user is
not anonymous); delegating to AccessDeniedHandler
org.springframework.security.access.AccessDeniedException: Access is
denied
我调试了 RemoteTokenService 并发现 Keycloak 使用完全相同的访问令牌进行响应。这很好。
但是 DefaultAccessTokenConverter
试图从不存在的字段 authorities
中读取用户角色。 OAuth2WebSecurityExpressionHandler
评估用户没有任何角色。 --> 拒绝访问
所以我的问题是:
使 Spring 安全与 Keycloak 访问令牌一起工作需要什么?
在这里提出这个问题后,我自己找到了解决方案。有时尝试表达问题会有所帮助。
解决方法是覆盖DefaultAccessTokenConverter,教他如何读取"realm_access"字段。它很丑但很管用:
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("demo-client");
RemoteTokenServices tokenServices = new RemoteTokenServices();
tokenServices.setCheckTokenEndpointUrl(
"http://localhost:8280/auth/realms/demo-realm/protocol/openid-connect/token/introspect");
tokenServices.setClientId("demo-client");
tokenServices.setClientSecret("80e19056-7770-4a4a-a3c4-06d8ac8792ef");
tokenServices.setAccessTokenConverter(new KeycloakAccessTokenConverter());
resources.tokenServices(tokenServices);
}
private class KeycloakAccessTokenConverter extends DefaultAccessTokenConverter {
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
OAuth2Authentication oAuth2Authentication = super.extractAuthentication(map);
Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>) oAuth2Authentication.getOAuth2Request().getAuthorities();
if (map.containsKey("realm_access")) {
Map<String, Object> realm_access = (Map<String, Object>) map.get("realm_access");
if(realm_access.containsKey("roles")) {
((Collection<String>) realm_access.get("roles")).forEach(r -> authorities.add(new SimpleGrantedAuthority(r)));
}
}
return new OAuth2Authentication(oAuth2Authentication.getOAuth2Request(),oAuth2Authentication.getUserAuthentication());
}
}
通过 keycloak 管理控制台,您可以为您的客户端 "demo-client" 创建类型为 用户领域角色 的令牌映射器,声明名称为 "authorities"。
然后访问令牌包含此属性中的角色名称,不需要自定义 DefaultAccessTokenConverter。
我设置了一个 Keycloak 服务器。配置领域和客户端等。 我成功地使用 "org.keycloak:keycloak-spring-boot-starter" 编写了 Spring 引导服务并保护了我的 RestController。很有魅力。
但是当我尝试使用 Spring 安全性(没有 keycloak 特定依赖项)时,我被卡住了。
这是我的 gradle:
dependencies {
compile('org.springframework.boot:spring-boot-starter-security')
compile('org.springframework.security.oauth:spring-security-oauth2')
compile('org.springframework.boot:spring-boot-starter-web')
compileOnly('org.projectlombok:lombok')
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('org.springframework.security:spring-security-test')
}
这是我的安全配置:
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends
ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/person/**").hasRole("DEMO_SPRING_SECURITY")
.anyRequest().authenticated()
.and().formLogin().disable();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("demo-client");
RemoteTokenServices tokenServices = new RemoteTokenServices();
tokenServices.setCheckTokenEndpointUrl(
"http://localhost:8280/auth/realms/demo-realm/protocol/openid-connect/token/introspect");
tokenServices.setClientId("demo-client");
tokenServices.setClientSecret("80e19056-7770-4a4a-a3c4-06d8ac8792ef");
resources.tokenServices(tokenServices);
}
}
现在我尝试访问服务器:
- 获取访问令牌(通过 REST 客户端) 解码后的 JWT 如下所示:
{ "jti": "78c00562-d80a-4f5a-ab08-61ed10cb575c", "exp": 1509603570, "nbf": 0, "iat": 1509603270, "iss": "http://localhost:8280/auth/realms/demo-realm", "aud": "demo-client", "sub": "6ee90ba4-2854-49c1-9776-9aa95b6ae598", "typ": "Bearer", "azp": "demo-client", "auth_time": 0, "session_state": "68ce12fb-3b3f-429d-9390-0662f0503bbb", "acr": "1", "client_session": "ec0113e1-022a-482a-a26b-e5701e5edec1", "allowed-origins": [], "realm_access": { "roles": [ "demo_user_role", "uma_authorization" ] }, "resource_access": { "account": { "roles": [ "manage-account", "manage-account-links", "view-profile" ] } }, "name": "Jim Panse", "preferred_username": "demo-user", "given_name": "Jim", "family_name": "Panse", "email": "user@dmoain.com" }
但是我得到一个 AccessDeniedException。
2017-11-02 07:18:05.344 DEBUG 17637 --- [nio-8080-exec-1] o.s.s.w.a.i.FilterSecurityInterceptor : Previously Authenticated:
org.springframework.security.oauth2.provider.OAuth2Authentication@1f3ee7e1: Principal: demo-client; Credentials: [PROTECTED]; Authenticated: true; Details: remoteAddress=127.0.0.1, tokenType=BearertokenValue=; Not granted any authorities 2017-11-02 07:18:05.348 DEBUG 17637 --- [nio-8080-exec-1] o.s.s.access.vote.AffirmativeBased : Voter: org.springframework.security.web.access.expression.WebExpressionVoter@14032696, returned: -1 2017-11-02 07:18:05.353 DEBUG 17637 --- [nio-8080-exec-1] o.s.s.w.a.ExceptionTranslationFilter : Access is denied (user is not anonymous); delegating to AccessDeniedHandler
org.springframework.security.access.AccessDeniedException: Access is denied
我调试了 RemoteTokenService 并发现 Keycloak 使用完全相同的访问令牌进行响应。这很好。
但是 DefaultAccessTokenConverter
试图从不存在的字段 authorities
中读取用户角色。 OAuth2WebSecurityExpressionHandler
评估用户没有任何角色。 --> 拒绝访问
所以我的问题是:
使 Spring 安全与 Keycloak 访问令牌一起工作需要什么?
在这里提出这个问题后,我自己找到了解决方案。有时尝试表达问题会有所帮助。
解决方法是覆盖DefaultAccessTokenConverter,教他如何读取"realm_access"字段。它很丑但很管用:
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("demo-client");
RemoteTokenServices tokenServices = new RemoteTokenServices();
tokenServices.setCheckTokenEndpointUrl(
"http://localhost:8280/auth/realms/demo-realm/protocol/openid-connect/token/introspect");
tokenServices.setClientId("demo-client");
tokenServices.setClientSecret("80e19056-7770-4a4a-a3c4-06d8ac8792ef");
tokenServices.setAccessTokenConverter(new KeycloakAccessTokenConverter());
resources.tokenServices(tokenServices);
}
private class KeycloakAccessTokenConverter extends DefaultAccessTokenConverter {
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
OAuth2Authentication oAuth2Authentication = super.extractAuthentication(map);
Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>) oAuth2Authentication.getOAuth2Request().getAuthorities();
if (map.containsKey("realm_access")) {
Map<String, Object> realm_access = (Map<String, Object>) map.get("realm_access");
if(realm_access.containsKey("roles")) {
((Collection<String>) realm_access.get("roles")).forEach(r -> authorities.add(new SimpleGrantedAuthority(r)));
}
}
return new OAuth2Authentication(oAuth2Authentication.getOAuth2Request(),oAuth2Authentication.getUserAuthentication());
}
}
通过 keycloak 管理控制台,您可以为您的客户端 "demo-client" 创建类型为 用户领域角色 的令牌映射器,声明名称为 "authorities"。 然后访问令牌包含此属性中的角色名称,不需要自定义 DefaultAccessTokenConverter。