如何使用 keycloak 和 spring 读取所有用户?
How can I read all users using keycloak and spring?
我正在使用 keycloak 3.4
和 spring boot
开发网络应用程序。
我正在使用 Active Directory 作为用户联盟来检索所有用户信息。
但是要在我的网络应用程序中使用这些信息,我认为我必须将它们保存在“local-webapp”数据库中。
那么在用户登录后,如何将它们保存在我的数据库中?
我正在考虑一个 场景 ,例如:“我有一个对象 A,它引用用户 B,所以我必须在它们之间建立关系。所以我添加外键。"
在那种情况下,我需要在我的数据库中拥有该用户。没有?
编辑
为了避免将所有用户保存在我的数据库中,我尝试使用管理员 API,因此我在控制器中添加了以下代码。
我还创建了另一个名为 Test
的客户端来获取所有用户,这样我就可以使用 client-id
和 client-secret.
或者有没有办法使用 JWT
使用管理员 API?
客户:
Keycloak keycloak2 = KeycloakBuilder.builder()
.serverUrl("http://localhost:8080/auth/admin/realms/MYREALM/users")
.realm("MYREALMM")
.username("u.user")
.password("password")
.clientId("Test")
.clientSecret("cade3034-6ee1-4b18-8627-2df9a315cf3d")
.resteasyClient(new ResteasyClientBuilder().connectionPoolSize(20).build())
.build();
RealmRepresentation realm2 = keycloak2.realm("MYREALMM").toRepresentation();
错误是:
2018-02-05 12:33:06.638 ERROR 16975 --- [nio-8080-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.Error: Unresolved compilation problem:
The method realm(String) is undefined for the type AccessTokenResponse
] with root cause
java.lang.Error: Unresolved compilation problem:
The method realm(String) is undefined for the type AccessTokenResponse
我哪里做错了?
编辑 2
我也试过这个:
@Autowired
private HttpServletRequest request;
public ResponseEntity listUsers() {
KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) request.getUserPrincipal();
KeycloakPrincipal principal=(KeycloakPrincipal)token.getPrincipal();
KeycloakSecurityContext session = principal.getKeycloakSecurityContext();
Keycloak keycloak = KeycloakBuilder.builder()
.serverUrl("http://localhost:8080/auth")
.realm("MYREALMM")
.authorization(session.getToken().getAuthorization().toString())
.resteasyClient(new ResteasyClientBuilder().connectionPoolSize(20).build())
.build();
RealmResource r = keycloak.realm("MYREALMM");
List<org.keycloak.representations.idm.UserRepresentation> list = keycloak.realm("MYREALMM").users().list();
return ResponseEntity.ok(list);
但授权始终是 null
。
为什么?
编辑 3
下面你可以找到我的 spring 安全配置:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.httpBasic().disable();
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/resources/**").permitAll()
.anyRequest().authenticated()
.and()
.logout()
.logoutUrl("/logout")
.logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
.permitAll()
.logoutSuccessUrl("/")
.invalidateHttpSession(true);
}
@Autowired
public KeycloakClientRequestFactory keycloakClientRequestFactory;
@Bean
public KeycloakRestTemplate keycloakRestTemplate() {
return new KeycloakRestTemplate(keycloakClientRequestFactory);
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
SimpleAuthorityMapper simpleAuthorityMapper = new SimpleAuthorityMapper();
simpleAuthorityMapper.setPrefix("ROLE_");
simpleAuthorityMapper.setConvertToUpperCase(true);
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(simpleAuthorityMapper);
auth.authenticationProvider(keycloakAuthenticationProvider);
}
@Bean
public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**", "/webjars/**");
}
@Bean
@Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public AccessToken accessToken() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
return ((KeycloakSecurityContext) ((KeycloakAuthenticationToken) request.getUserPrincipal()).getCredentials()).getToken();
}
}
编辑 4
这些是 applicatoin.properties
里面的属性
#######################################
# KEYCLOAK #
#######################################
keycloak.auth-server-url=http://localhost:8181/auth
keycloak.realm=My Realm
keycloak.ssl-required=external
keycloak.resource=AuthServer
keycloak.credentials.jwt.client-key-password=keystorePwd
keycloak.credentials.jwt.client-keystore-file=keystore.jks
keycloak.credentials.jwt.client-keystore-password=keystorePwd
keycloak.credentials.jwt.alias=AuthServer
keycloak.credentials.jwt.token-expiration=10
keycloak.credentials.jwt.client-keystore-type=JKS
keycloak.use-resource-role-mappings=true
keycloak.confidential-port=0
keycloak.principal-attribute=preferred_username
编辑 5.
这是我的密钥斗篷配置:
我使用查看用户权限登录的用户:
编辑 6
这是启用日志记录后的日志表单密钥斗篷:
2018-02-12 08:31:00.274 3DEBUG 5802 --- [nio-8080-exec-1] o.k.adapters.PreAuthActionsHandler : adminRequest http://localhost:8080/utente/prova4
2018-02-12 08:31:00.274 3DEBUG 5802 --- [nio-8080-exec-1] .k.a.t.AbstractAuthenticatedActionsValve : AuthenticatedActionsValve.invoke /utente/prova4
2018-02-12 08:31:00.274 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler : AuthenticatedActionsValve.invoke http://localhost:8080/utente/prova4
2018-02-12 08:31:00.274 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler : Policy enforcement is disabled.
2018-02-12 08:31:00.275 3DEBUG 5802 --- [nio-8080-exec-1] o.k.adapters.PreAuthActionsHandler : adminRequest http://localhost:8080/utente/prova4
2018-02-12 08:31:00.275 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler : AuthenticatedActionsValve.invoke http://localhost:8080/utente/prova4
2018-02-12 08:31:00.275 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler : Policy enforcement is disabled.
2018-02-12 08:31:00.276 3DEBUG 5802 --- [nio-8080-exec-1] o.k.adapters.PreAuthActionsHandler : adminRequest http://localhost:8080/utente/prova4
2018-02-12 08:31:00.276 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler : AuthenticatedActionsValve.invoke http://localhost:8080/utente/prova4
2018-02-12 08:31:00.276 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler : Policy enforcement is disabled.
2018-02-12 08:31:10.580 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.s.client.KeycloakRestTemplate : Created GET request for "http://localhost:8181/auth/admin/realms/My%20Realm%20name/users"
2018-02-12 08:31:10.580 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.s.client.KeycloakRestTemplate : Setting request Accept header to [application/json, application/*+json]
2018-02-12 08:31:10.592 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.s.client.KeycloakRestTemplate : GET request for "http://localhost:8181/auth/admin/realms/My%20Realm%20name/users" resulted in 401 (Unauthorized); invoking error handler
2018-02-12 08:31:10.595 ERROR 5802 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.web.client.HttpClientErrorException: 401 Unauthorized] with root cause
org.springframework.web.client.HttpClientErrorException: 401 Unauthorized
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:85) ~[spring-web-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:707) ~[spring-web-4.3.13.RELEASE.jar:4.3.13.RELEASE]
我建议仔细检查您是否真的需要拥有自己的用户存储。您应该完全依靠 Keycloak 的用户联合来避免重复数据,从而避免随之而来的问题。其中,Keycloak 负责管理用户,你应该让它完成它的工作。
由于您使用的是 OIDC,因此您可以从两方面受益:
在您以 JWT 形式获得的身份令牌中,您有一个 "sub" 字段。该字段唯一标识一个用户。来自 OpenID Connect spec:
REQUIRED. Subject Identifier. A locally unique and never reassigned identifier within the Issuer for the End-User, which is intended to be consumed by the Client, e.g., 24400320 or AItOawmwtWwcT0k51BayewNvutrJUqsvl6qs7A4. It MUST NOT exceed 255 ASCII characters in length. The sub value is a case sensitive string.
在keycloak中,"sub"只是一个UUID。您可以使用此字段将您的 "object A" 关联到 "user B"。在您的数据库中,这只是一个常规列,而不是外键。
在 Java 中,您可以使用 security context. You can also take a look at keycloak's authz-springboot quickstart where it is shown how you can access KeycloakSecurityContext 访问此 JWT 数据 - 从那里您可以获得具有 getSubject 方法的 IDToken。
Keycloak 提供具有 users resource. This is OIDC supported API so you have to be properly authenticated. Using that API you can perform operations on users - including listing them. You can consume that API directly or through use of Java SDK: keycloak admin client.
的 Admin REST API
在这种情况下,您应该使用从用户请求中获取的 JWT。使用 JWT,您可以确定发出请求的人可以列出该领域中的所有用户。例如,请考虑以下代码:
@GetMapping("/users")
public List<UserRepresentation> check(HttpServletRequest request){
KeycloakSecurityContext context = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
Keycloak keycloak = KeycloakBuilder.builder()
.serverUrl("http://localhost:8080/auth")
.realm("example")
.authorization(context.getTokenString())
.resteasyClient(new ResteasyClientBuilder().connectionPoolSize(20).build())
.build();
List<UserRepresentation> list = keycloak.realm("example").users().list();
return list;
}
在那种情况下,我们使用 HttpServletRequest 和它包含的令牌。我们可以通过使用来自 spring 安全性的 org.springframework.security.core.Authentication
或直接获得授权 header 来获取相同的数据。问题是 KeycloakBuilder 期望一个字符串作为 'authorization',而不是 AccessToken - 这就是你有那个错误的原因。
请记住,为了使其正常工作,创建请求的用户必须具有来自 'realm-management' 客户端的 'view-users' 角色。您可以在该用户或他所属的某个组的 'Role Mapping' 选项卡中将该角色分配给他。
此外,您必须经过适当的身份验证才能从安全上下文中受益,否则您将得到一个空值。示例性 spring 安全密钥斗篷配置 class 是:
@Configuration
@EnableWebSecurity
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
@Bean
public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests()
.antMatchers("/api/users/*")
.hasRole("admin")
.anyRequest()
.permitAll();
}
}
为了访问整个用户列表,您必须验证登录的用户至少包含来自 realm-management
客户端的 view-users
角色,参见 this answer 我写了一些过去。一旦用户拥有此角色,她检索到的 JWT 将包含它。
从你的评论中我可以推断,你似乎缺乏一些关于 Authorization
header 的基础。用户登录后,她会从 keycloak 获得签名的 JWT,因此领域中的每个客户端都可以信任它,而无需询问 Keycloak。此 JWT 包含访问令牌,稍后在每个用户请求的 Authorization
header 中需要,以 Bearer
关键字为前缀(请参阅 Token-Based https://auth0.com/blog/cookies-vs-tokens-definitive-guide/).
中的身份验证
因此,当用户向您的应用发出请求以查看用户列表时,她包含 view-users
角色的访问令牌已经进入请求 header。不必手动解析它,而是自己创建另一个请求来访问 Keycloak 用户端点并附加它(正如您似乎在使用 KeycloakBuilder
所做的那样),Keycloak Spring 安全适配器已经提供了一个 KeycloakRestTemplate
class,能够为当前用户执行对另一个服务的请求:
SecurityConfig.java
@Configuration
@EnableWebSecurity
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
...
@Autowired
public KeycloakClientRequestFactory keycloakClientRequestFactory;
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public KeycloakRestTemplate keycloakRestTemplate() {
return new KeycloakRestTemplate(keycloakClientRequestFactory);
}
...
}
请注意,模板的范围是 PROTOTYPE
,因此 Spring 将为发出的每个请求使用不同的实例。
然后,自动装配此模板并使用它来发出请求:
@Service
public class UserRetrievalService{
@Autowired
private KeycloakRestTemplate keycloakRestTemplate;
public List<User> getUsers() {
ResponseEntity<User[]> response = keycloakRestTemplate.getForEntity(keycloakUserListEndpoint, User[].class);
return Arrays.asList(response.getBody());
}
}
您需要实现自己的 User
class,它与 keycloak 服务器返回的 JSON 响应相匹配。
请注意,当不允许用户访问列表时,Keycloak 服务器会返回 403 响应代码。你甚至可以在自己面前否认它,使用一些注释,如:@PreAuthorize("hasRole('VIEW_USERS')")
.
最后但同样重要的是,我认为@dchrzascik 的回答很中肯。总而言之,我想说实际上还有另一种方法可以避免每次都从 keycloak 服务器检索整个用户列表或将用户存储在应用程序数据库中:你实际上可以缓存它们,这样你就可以更新缓存如果你从您的应用程序进行用户管理。
编辑
我已经实现了一个示例项目来展示如何获取上传到 Github 的整个用户列表。它是为机密客户端配置的(使用 public 客户端时,应从 application.properties 中删除机密)。
另请参阅:
我正在使用 keycloak 3.4
和 spring boot
开发网络应用程序。
我正在使用 Active Directory 作为用户联盟来检索所有用户信息。
但是要在我的网络应用程序中使用这些信息,我认为我必须将它们保存在“local-webapp”数据库中。
那么在用户登录后,如何将它们保存在我的数据库中?
我正在考虑一个 场景 ,例如:“我有一个对象 A,它引用用户 B,所以我必须在它们之间建立关系。所以我添加外键。"
在那种情况下,我需要在我的数据库中拥有该用户。没有?
编辑
为了避免将所有用户保存在我的数据库中,我尝试使用管理员 API,因此我在控制器中添加了以下代码。
我还创建了另一个名为 Test
的客户端来获取所有用户,这样我就可以使用 client-id
和 client-secret.
或者有没有办法使用 JWT
使用管理员 API?
客户:
Keycloak keycloak2 = KeycloakBuilder.builder()
.serverUrl("http://localhost:8080/auth/admin/realms/MYREALM/users")
.realm("MYREALMM")
.username("u.user")
.password("password")
.clientId("Test")
.clientSecret("cade3034-6ee1-4b18-8627-2df9a315cf3d")
.resteasyClient(new ResteasyClientBuilder().connectionPoolSize(20).build())
.build();
RealmRepresentation realm2 = keycloak2.realm("MYREALMM").toRepresentation();
错误是:
2018-02-05 12:33:06.638 ERROR 16975 --- [nio-8080-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.Error: Unresolved compilation problem:
The method realm(String) is undefined for the type AccessTokenResponse
] with root cause
java.lang.Error: Unresolved compilation problem:
The method realm(String) is undefined for the type AccessTokenResponse
我哪里做错了?
编辑 2
我也试过这个:
@Autowired
private HttpServletRequest request;
public ResponseEntity listUsers() {
KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) request.getUserPrincipal();
KeycloakPrincipal principal=(KeycloakPrincipal)token.getPrincipal();
KeycloakSecurityContext session = principal.getKeycloakSecurityContext();
Keycloak keycloak = KeycloakBuilder.builder()
.serverUrl("http://localhost:8080/auth")
.realm("MYREALMM")
.authorization(session.getToken().getAuthorization().toString())
.resteasyClient(new ResteasyClientBuilder().connectionPoolSize(20).build())
.build();
RealmResource r = keycloak.realm("MYREALMM");
List<org.keycloak.representations.idm.UserRepresentation> list = keycloak.realm("MYREALMM").users().list();
return ResponseEntity.ok(list);
但授权始终是 null
。
为什么?
编辑 3 下面你可以找到我的 spring 安全配置:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.httpBasic().disable();
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/resources/**").permitAll()
.anyRequest().authenticated()
.and()
.logout()
.logoutUrl("/logout")
.logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
.permitAll()
.logoutSuccessUrl("/")
.invalidateHttpSession(true);
}
@Autowired
public KeycloakClientRequestFactory keycloakClientRequestFactory;
@Bean
public KeycloakRestTemplate keycloakRestTemplate() {
return new KeycloakRestTemplate(keycloakClientRequestFactory);
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
SimpleAuthorityMapper simpleAuthorityMapper = new SimpleAuthorityMapper();
simpleAuthorityMapper.setPrefix("ROLE_");
simpleAuthorityMapper.setConvertToUpperCase(true);
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(simpleAuthorityMapper);
auth.authenticationProvider(keycloakAuthenticationProvider);
}
@Bean
public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**", "/webjars/**");
}
@Bean
@Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public AccessToken accessToken() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
return ((KeycloakSecurityContext) ((KeycloakAuthenticationToken) request.getUserPrincipal()).getCredentials()).getToken();
}
}
编辑 4
这些是 applicatoin.properties
#######################################
# KEYCLOAK #
#######################################
keycloak.auth-server-url=http://localhost:8181/auth
keycloak.realm=My Realm
keycloak.ssl-required=external
keycloak.resource=AuthServer
keycloak.credentials.jwt.client-key-password=keystorePwd
keycloak.credentials.jwt.client-keystore-file=keystore.jks
keycloak.credentials.jwt.client-keystore-password=keystorePwd
keycloak.credentials.jwt.alias=AuthServer
keycloak.credentials.jwt.token-expiration=10
keycloak.credentials.jwt.client-keystore-type=JKS
keycloak.use-resource-role-mappings=true
keycloak.confidential-port=0
keycloak.principal-attribute=preferred_username
编辑 5.
这是我的密钥斗篷配置:
我使用查看用户权限登录的用户:
编辑 6
这是启用日志记录后的日志表单密钥斗篷:
2018-02-12 08:31:00.274 3DEBUG 5802 --- [nio-8080-exec-1] o.k.adapters.PreAuthActionsHandler : adminRequest http://localhost:8080/utente/prova4
2018-02-12 08:31:00.274 3DEBUG 5802 --- [nio-8080-exec-1] .k.a.t.AbstractAuthenticatedActionsValve : AuthenticatedActionsValve.invoke /utente/prova4
2018-02-12 08:31:00.274 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler : AuthenticatedActionsValve.invoke http://localhost:8080/utente/prova4
2018-02-12 08:31:00.274 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler : Policy enforcement is disabled.
2018-02-12 08:31:00.275 3DEBUG 5802 --- [nio-8080-exec-1] o.k.adapters.PreAuthActionsHandler : adminRequest http://localhost:8080/utente/prova4
2018-02-12 08:31:00.275 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler : AuthenticatedActionsValve.invoke http://localhost:8080/utente/prova4
2018-02-12 08:31:00.275 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler : Policy enforcement is disabled.
2018-02-12 08:31:00.276 3DEBUG 5802 --- [nio-8080-exec-1] o.k.adapters.PreAuthActionsHandler : adminRequest http://localhost:8080/utente/prova4
2018-02-12 08:31:00.276 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler : AuthenticatedActionsValve.invoke http://localhost:8080/utente/prova4
2018-02-12 08:31:00.276 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler : Policy enforcement is disabled.
2018-02-12 08:31:10.580 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.s.client.KeycloakRestTemplate : Created GET request for "http://localhost:8181/auth/admin/realms/My%20Realm%20name/users"
2018-02-12 08:31:10.580 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.s.client.KeycloakRestTemplate : Setting request Accept header to [application/json, application/*+json]
2018-02-12 08:31:10.592 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.s.client.KeycloakRestTemplate : GET request for "http://localhost:8181/auth/admin/realms/My%20Realm%20name/users" resulted in 401 (Unauthorized); invoking error handler
2018-02-12 08:31:10.595 ERROR 5802 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.web.client.HttpClientErrorException: 401 Unauthorized] with root cause
org.springframework.web.client.HttpClientErrorException: 401 Unauthorized
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:85) ~[spring-web-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:707) ~[spring-web-4.3.13.RELEASE.jar:4.3.13.RELEASE]
我建议仔细检查您是否真的需要拥有自己的用户存储。您应该完全依靠 Keycloak 的用户联合来避免重复数据,从而避免随之而来的问题。其中,Keycloak 负责管理用户,你应该让它完成它的工作。
由于您使用的是 OIDC,因此您可以从两方面受益:
在您以 JWT 形式获得的身份令牌中,您有一个 "sub" 字段。该字段唯一标识一个用户。来自 OpenID Connect spec:
REQUIRED. Subject Identifier. A locally unique and never reassigned identifier within the Issuer for the End-User, which is intended to be consumed by the Client, e.g., 24400320 or AItOawmwtWwcT0k51BayewNvutrJUqsvl6qs7A4. It MUST NOT exceed 255 ASCII characters in length. The sub value is a case sensitive string.
在keycloak中,"sub"只是一个UUID。您可以使用此字段将您的 "object A" 关联到 "user B"。在您的数据库中,这只是一个常规列,而不是外键。
在 Java 中,您可以使用 security context. You can also take a look at keycloak's authz-springboot quickstart where it is shown how you can access KeycloakSecurityContext 访问此 JWT 数据 - 从那里您可以获得具有 getSubject 方法的 IDToken。
Keycloak 提供具有 users resource. This is OIDC supported API so you have to be properly authenticated. Using that API you can perform operations on users - including listing them. You can consume that API directly or through use of Java SDK: keycloak admin client.
的 Admin REST API在这种情况下,您应该使用从用户请求中获取的 JWT。使用 JWT,您可以确定发出请求的人可以列出该领域中的所有用户。例如,请考虑以下代码:
@GetMapping("/users") public List<UserRepresentation> check(HttpServletRequest request){ KeycloakSecurityContext context = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName()); Keycloak keycloak = KeycloakBuilder.builder() .serverUrl("http://localhost:8080/auth") .realm("example") .authorization(context.getTokenString()) .resteasyClient(new ResteasyClientBuilder().connectionPoolSize(20).build()) .build(); List<UserRepresentation> list = keycloak.realm("example").users().list(); return list; }
在那种情况下,我们使用 HttpServletRequest 和它包含的令牌。我们可以通过使用来自 spring 安全性的
org.springframework.security.core.Authentication
或直接获得授权 header 来获取相同的数据。问题是 KeycloakBuilder 期望一个字符串作为 'authorization',而不是 AccessToken - 这就是你有那个错误的原因。请记住,为了使其正常工作,创建请求的用户必须具有来自 'realm-management' 客户端的 'view-users' 角色。您可以在该用户或他所属的某个组的 'Role Mapping' 选项卡中将该角色分配给他。
此外,您必须经过适当的身份验证才能从安全上下文中受益,否则您将得到一个空值。示例性 spring 安全密钥斗篷配置 class 是:
@Configuration @EnableWebSecurity @ComponentScan(basePackageClasses = KeycloakSecurityComponents.class) class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter { @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider(); keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper()); auth.authenticationProvider(keycloakAuthenticationProvider); } @Bean public KeycloakSpringBootConfigResolver KeycloakConfigResolver() { return new KeycloakSpringBootConfigResolver(); } @Bean @Override protected SessionAuthenticationStrategy sessionAuthenticationStrategy() { return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl()); } @Override protected void configure(HttpSecurity http) throws Exception { super.configure(http); http.authorizeRequests() .antMatchers("/api/users/*") .hasRole("admin") .anyRequest() .permitAll(); } }
为了访问整个用户列表,您必须验证登录的用户至少包含来自 realm-management
客户端的 view-users
角色,参见 this answer 我写了一些过去。一旦用户拥有此角色,她检索到的 JWT 将包含它。
从你的评论中我可以推断,你似乎缺乏一些关于 Authorization
header 的基础。用户登录后,她会从 keycloak 获得签名的 JWT,因此领域中的每个客户端都可以信任它,而无需询问 Keycloak。此 JWT 包含访问令牌,稍后在每个用户请求的 Authorization
header 中需要,以 Bearer
关键字为前缀(请参阅 Token-Based https://auth0.com/blog/cookies-vs-tokens-definitive-guide/).
因此,当用户向您的应用发出请求以查看用户列表时,她包含 view-users
角色的访问令牌已经进入请求 header。不必手动解析它,而是自己创建另一个请求来访问 Keycloak 用户端点并附加它(正如您似乎在使用 KeycloakBuilder
所做的那样),Keycloak Spring 安全适配器已经提供了一个 KeycloakRestTemplate
class,能够为当前用户执行对另一个服务的请求:
SecurityConfig.java
@Configuration
@EnableWebSecurity
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
...
@Autowired
public KeycloakClientRequestFactory keycloakClientRequestFactory;
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public KeycloakRestTemplate keycloakRestTemplate() {
return new KeycloakRestTemplate(keycloakClientRequestFactory);
}
...
}
请注意,模板的范围是 PROTOTYPE
,因此 Spring 将为发出的每个请求使用不同的实例。
然后,自动装配此模板并使用它来发出请求:
@Service
public class UserRetrievalService{
@Autowired
private KeycloakRestTemplate keycloakRestTemplate;
public List<User> getUsers() {
ResponseEntity<User[]> response = keycloakRestTemplate.getForEntity(keycloakUserListEndpoint, User[].class);
return Arrays.asList(response.getBody());
}
}
您需要实现自己的 User
class,它与 keycloak 服务器返回的 JSON 响应相匹配。
请注意,当不允许用户访问列表时,Keycloak 服务器会返回 403 响应代码。你甚至可以在自己面前否认它,使用一些注释,如:@PreAuthorize("hasRole('VIEW_USERS')")
.
最后但同样重要的是,我认为@dchrzascik 的回答很中肯。总而言之,我想说实际上还有另一种方法可以避免每次都从 keycloak 服务器检索整个用户列表或将用户存储在应用程序数据库中:你实际上可以缓存它们,这样你就可以更新缓存如果你从您的应用程序进行用户管理。
编辑
我已经实现了一个示例项目来展示如何获取上传到 Github 的整个用户列表。它是为机密客户端配置的(使用 public 客户端时,应从 application.properties 中删除机密)。
另请参阅: