【问题标题】:How can I read all users using keycloak and spring?如何使用 keycloak 和 spring 读取所有用户?
【发布时间】:2018-07-12 23:52:29
【问题描述】:

我正在使用keycloak 3.4spring boot 开发一个网络应用程序。 我使用 Active Directory 作为用户联合来检索所有用户信息。

但要在我的网络应用程序中使用这些信息,我认为我必须将它们保存在“local-webapp”数据库​​中。

那么在用户登录后,我怎样才能将他们保存到我的数据库中呢?

我正在考虑一个场景,比如:“我有一个对象 A,它指向用户 B,所以我必须在它们之间建立一个关系。所以我添加了一个外键。 "

在这种情况下,我需要在我的数据库中拥有该用户。没有?

编辑

为了避免在我的数据库中保存所有用户,我尝试使用管理员 API,因此我在控制器中添加了以下代码。

我还创建了另一个名为Test 的客户端来获取所有用户,这样我可以使用client-idclient-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.

这是我的 keycloak 配置:

我用来以查看用户权限登录的用户:

编辑 6

这是启用日志记录后的日志表单keycloak:

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 作为 OpenID Connect 提供者进行中继?
  • 您使用什么 keycloak api 来获取与 LDAP 同步的用户?

标签: spring spring-boot spring-security authorization keycloak


【解决方案1】:

为了访问整个用户列表,您必须验证登录的用户至少包含来自realm-management 客户端的view-users 角色,请参阅我前段时间写的this answer。一旦用户拥有了这个角色,她检索到的 JWT 就会包含它。

我可以从您的 cmets 推断出,您似乎缺乏关于 Authorization 标头的一些基础。用户登录后,她会从 keycloak 获取签名的 JWT,因此领域中的每个客户端都可以信任它,而无需询问 Keycloak。此 JWT 包含访问令牌,稍后在每个用户请求的 Authorization 标头中都需要该令牌,并以 Bearer 关键字为前缀(请参阅https://auth0.com/blog/cookies-vs-tokens-definitive-guide/ 中的基于令牌的身份验证)。

因此,当用户向您的应用发出请求以查看用户列表时,她的包含view-users 角色的访问令牌已经进入请求标头。 Keycloak Spring Security 适配器已经提供了一个 KeycloakRestTemplate 类,而不必手动解析它,而是自己创建另一个请求来访问 Keycloak 用户端点并附加它(就像您对 KeycloakBuilder 所做的那样)为当前用户执行对另一个服务的请求:

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 类,该类与 keycloak 服务器返回的 JSON 响应相匹配。

请注意,当用户不允许访问该列表时,Keycloak 服务器会返回一个 403 响应代码。你甚至可以在自己面前否认它,使用一些注释,例如:@PreAuthorize("hasRole('VIEW_USERS')")

最后但并非最不重要的一点是,我认为@dchrzascik 的回答非常有针对性。总而言之,我想说实际上还有另一种方法可以避免每次从 keycloak 服务器检索整个用户列表或将用户存储在您的应用程序数据库中:您实际上可以缓存它们,因此如果您可以更新该缓存从您的应用中进行用户管理。


编辑

我已经实现了一个示例项目来展示如何获取完整的用户列表,上传到Github。它是为机密客户端配置的(使用公共客户端时,应从 application.properties 中删除机密)。

另请参阅:

【讨论】:

  • 非常感谢!真的!但它还没有工作:( :( 如果我使用你的项目或者如果我使用我的项目,我会获得Unauthorized :( 我添加了 2 张关于我的 kecycloak 配置的图像 (EDIT 5)..也许是那里的错误..我失去了希望:(
  • 我检查过,但它没有记录任何内容。我认为这是个问题
  • 错误跟踪没有提供有关该问题的更多信息...只是再试一次,您能否使用名称不包含空格的全新领域对其进行测试?这是我在当前测试设置中看到的唯一区别(除了 KC 版本,我已经针对 2.2.1 进行了测试)
  • 很高兴知道它 ;-) 似乎适配器在某种程度上弄乱了包含空格的域名。
  • 啊好吧..我没想到空间是可能的原因..过了一会儿我们修好了:)
【解决方案2】:

我建议您仔细检查您是否真的需要拥有自己的用户存储。您应该仅在 Keycloak 的用户联盟上进行中继,以避免重复数据,从而避免随之而来的问题。其中,Keycloak 负责管理用户,你应该让它完成它的工作。

由于您使用的是 OIDC,因此您可以从两件事中受益:

  1. 在您以 JWT 形式获得的身份令牌中,您有一个“子”字段。此字段唯一标识用户。来自OpenID Connect spec

    必填。主题标识符。最终用户在发行者中的本地唯一且从未重新分配的标识符,旨在供客户端使用,例如 24400320 或 AItOawmwtWwcT0k51BayewNvutrJUqsvl6qs7A4。它的长度不得超过 255 个 ASCII 字符。 sub 值是区分大小写的字符串。

    在 keycloak 中,“sub”只是一个 UUID。您可以使用此字段将您的“对象 A”与“用户 B”相关联。在您的数据库中,这只是一个常规列,而不是外键。

    在 Java 中,您可以使用 security context 访问此 JWT 数据。您还可以查看keycloak's authz-springboot quickstart,其中显示了您如何访问KeycloakSecurityContext - 从那里您可以获得具有getSubject 方法的IDToken。

  2. Keycloak 提供具有users resource 的Admin REST API。这是 OIDC 支持的 API,因此您必须经过适当的身份验证。使用该 API,您可以对用户执行操作 - 包括列出他们。您可以直接使用该 API,也可以通过使用 Java SDK:keycloak admin client

    在这种情况下,您应该使用从用户请求中获得的 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 security 中的org.springframework.security.core.Authentication 或直接获取 Authorization 标头来获取相同的数据。问题是 KeycloakBuilder 需要一个字符串作为“授权”,而不是 AccessToken - 这就是您出现该错误的原因。

    请记住,要使此功能起作用,创建请求的用户必须具有来自“领域管理”客户端的“查看用户”角色。您可以在该用户或他所属的某个组的“角色映射”选项卡中将该角色分配给他。

    此外,您必须经过适当的身份验证才能从安全上下文中受益,否则您将得到一个空值。示例性的 spring security keycloak 配置类是:

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

【讨论】:

  • 其实我和Droide有同样的问题。我应该以某种方式配置keycloak来传递那个标题吗?抱歉,授权字符串是什么?
  • 我给出了直接传递标题的示例,因为我没有完整的项目设置。不过想法是一样的,您应该能够从 KeycloakSecurityContext 获取访问令牌。稍后我将尝试准备一些示例来说明这一点。为了清楚起见,您是使用带有 Keycloak 的 Spring Security 还是直接使用一些 keycloak Java 适配器?
  • 如果我的示例有帮助,请告诉我,如果没有,那么您的项目中存在一些错误配置。如果是这种情况,我将把这个例子分解成细节。
  • @dchrzascik 谢谢你的回答.. 但没有任何改变!我的意思是我已经有了你可以在 EDIT 3.. 中看到的配置
猜你喜欢
  • 2021-06-07
  • 2018-01-29
  • 2021-08-11
  • 1970-01-01
  • 2020-09-05
  • 2020-09-09
  • 2023-03-26
  • 2018-08-02
  • 1970-01-01
相关资源
最近更新 更多