当前位置: 首页 > 知识库问答 >
问题:

如何在Spring Boot应用程序中测试Keycloak身份验证?

龙俭
2023-03-14

在Spring Boot项目中,我们启用了Spring Security性,并使用承载令牌应用了KeyClope身份验证,如以下文章所述:

https://www.keycloak.org/docs/3.2/securing_apps/topics/oidc/java/spring-security-adapter.html

https://www.keycloak.org/docs/3.2/securing_apps/topics/oidc/java/spring-boot-adapter.html

但我找不到任何建议,如何进行自动化测试,以便应用KeyClope配置。

那么,在启用Spring Security性时,如何测试/模拟/验证KeyClope配置呢?一件非常恼人的事:默认情况下,Spring会激活csrf安全过滤器,但如何避免测试呢?

(注意:我们使用承载令牌,所以看起来@WithMockUser在这种情况下不适用)

一个额外的问题:基本上,我们不想在每个控制器集成测试上验证安全性,那么是否可以将安全性与控制器集成测试(使用@SpringBootTest@WebAppConfiguration@AutoConfigureMockMvc等)分开进行验证?

共有3个答案

易和怡
2023-03-14

我在活动项目中工作,我们一直在使用带有Spring启动的密钥斗篷,并遇到了同样的问题。有一个名为KeycloakSecurityContextClientRequestInterceptor的密钥斗篷测试帮助类,我们对其进行了一点定制。它引用了用于测试的领域和用户。我们在使用keycloak的测试中设置了这些属性。这也可用于在一组测试期间切换用户。

对于我们不想使用keycloak的测试,到目前为止,我们遵循的做法是将它们保留在我们项目中的不同级别,因此保留在不同的子模块中。这让我们可以将keycloak maven依赖关系排除在该层之外,这样keycloak就不会在它们上启用。

屈昊天
2023-03-14

部分答案仅适用于“奖励”问题(@Componentunit-test):我刚刚编写了一组库来简化安全Spring应用程序的单元测试。我只运行这样的测试和e2e测试(包括富客户端前端和实际授权服务器)。

它包括一个@WithMockKeycloackAuth注释,以及专用于KeyClope的MockMvc请求后处理器

示例用法:

@RunWith(SpringRunner.class)
@WebMvcTest(GreetingController.class)
@ContextConfiguration(classes = GreetingApp.class)
@ComponentScan(basePackageClasses = { KeycloakSecurityComponents.class, KeycloakSpringBootConfigResolver.class })
public class GreetingControllerTests extends ServletUnitTestingSupport {
    @MockBean
    MessageService messageService;

    @Test
    @WithMockKeycloackAuth("TESTER")
    public void whenUserIsNotGrantedWithAuthorizedPersonelThenSecretRouteIsNotAccessible() throws Exception {
        mockMvc().get("/secured-route").andExpect(status().isForbidden());
    }

    @Test
    @WithMockKeycloackAuth("AUTHORIZED_PERSONNEL")
    public void whenUserIsGrantedWithAuthorizedPersonelThenSecretRouteIsAccessible() throws Exception {
        mockMvc().get("/secured-route").andExpect(content().string(is("secret route")));
    }

    @Test
    @WithMockKeycloakAuth(
            authorities = { "USER", "AUTHORIZED_PERSONNEL" },
            id = @IdTokenClaims(sub = "42"),
            oidc = @OidcStandardClaims(
                    email = "ch4mp@c4-soft.com",
                    emailVerified = true,
                    nickName = "Tonton-Pirate",
                    preferredUsername = "ch4mpy"),
            otherClaims = @ClaimSet(stringClaims = @StringClaim(name = "foo", value = "bar")))
    public void whenAuthenticatedWithKeycloakAuthenticationTokenThenCanGreet() throws Exception {
        mockMvc().get("/greet")
                .andExpect(status().isOk())
                .andExpect(content().string(startsWith("Hello ch4mpy! You are granted with ")))
                .andExpect(content().string(containsString("AUTHORIZED_PERSONNEL")))
                .andExpect(content().string(containsString("USER")));
}

maven central提供了不同的LIB,请根据您的用例选择以下其中一种(仅限@WithMockKeyDoveAuth或更多工具,如MockMvc fluent API):

<dependency>
  <groupId>com.c4-soft.springaddons</groupId>
  <artifactId>spring-security-oauth2-test-addons</artifactId>
  <version>2.4.1</version>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>com.c4-soft.springaddons</groupId>
  <artifactId>spring-security-oauth2-test-webmvc-addons</artifactId>
  <version>2.4.1</version>
  <scope>test</scope>
</dependency>
钱凌
2023-03-14

一种解决方案是使用WireMock对KeyClope授权服务器进行存根。因此,您可以使用库spring cloud contract wiremock(请参阅https://cloud.spring.io/spring-cloud-contract/1.1.x/multi/multi__spring_cloud_contract_wiremock.html),它提供了一个简单的spring boot集成。您可以简单地添加所描述的依赖项。此外,我使用jose4j创建模拟访问令牌的方式与keydepeat创建JWTs的方式相同。您所要做的就是为keydrope OpenId配置和JSON Web密钥存储存根endpoint,因为keydrope适配器只请求验证授权头中的访问令牌的endpoint。

下面列出了一个简单的独立工作示例,需要在一个地方进行自定义(请参见重要注释),并给出了一些解释:

钥匙测试。爪哇:

@ExtendWith(SpringExtension.class)
@WebMvcTest(KeycloakTest.TestController.class)
@EnableConfigurationProperties(KeycloakSpringBootProperties.class)
@ContextConfiguration(classes= {KeycloakTest.TestController.class, SecurityConfig.class, CustomKeycloakSpringBootConfigResolver.class})
@AutoConfigureMockMvc
@AutoConfigureWireMock(port = 0) //random port, that is wired into properties with key wiremock.server.port
@TestPropertySource(locations = "classpath:wiremock.properties")
public class KeycloakTest {

    private static RsaJsonWebKey rsaJsonWebKey;

    private static boolean testSetupIsCompleted = false;

    @Value("${wiremock.server.baseUrl}")
    private String keycloakBaseUrl;

    @Value("${keycloak.realm}")
    private String keycloakRealm;

    @Autowired
    private MockMvc mockMvc;

    @BeforeEach
    public void setUp() throws IOException, JoseException {
        if(!testSetupIsCompleted) {
            // Generate an RSA key pair, which will be used for signing and verification of the JWT, wrapped in a JWK
            rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048);
            rsaJsonWebKey.setKeyId("k1");
            rsaJsonWebKey.setAlgorithm(AlgorithmIdentifiers.RSA_USING_SHA256);
            rsaJsonWebKey.setUse("sig");

            String openidConfig = "{\n" +
                    "  \"issuer\": \"" + keycloakBaseUrl + "/auth/realms/" + keycloakRealm + "\",\n" +
                    "  \"authorization_endpoint\": \"" + keycloakBaseUrl + "/auth/realms/" + keycloakRealm + "/protocol/openid-connect/auth\",\n" +
                    "  \"token_endpoint\": \"" + keycloakBaseUrl + "/auth/realms/" + keycloakRealm + "/protocol/openid-connect/token\",\n" +
                    "  \"token_introspection_endpoint\": \"" + keycloakBaseUrl + "/auth/realms/" + keycloakRealm + "/protocol/openid-connect/token/introspect\",\n" +
                    "  \"userinfo_endpoint\": \"" + keycloakBaseUrl + "/auth/realms/" + keycloakRealm + "/protocol/openid-connect/userinfo\",\n" +
                    "  \"end_session_endpoint\": \"" + keycloakBaseUrl + "/auth/realms/" + keycloakRealm + "/protocol/openid-connect/logout\",\n" +
                    "  \"jwks_uri\": \"" + keycloakBaseUrl + "/auth/realms/" + keycloakRealm + "/protocol/openid-connect/certs\",\n" +
                    "  \"check_session_iframe\": \"" + keycloakBaseUrl + "/auth/realms/" + keycloakRealm + "/protocol/openid-connect/login-status-iframe.html\",\n" +
                    "  \"registration_endpoint\": \"" + keycloakBaseUrl + "/auth/realms/" + keycloakRealm + "/clients-registrations/openid-connect\",\n" +
                    "  \"introspection_endpoint\": \"" + keycloakBaseUrl + "/auth/realms/" + keycloakRealm + "/protocol/openid-connect/token/introspect\"\n" +
                    "}";
            stubFor(WireMock.get(urlEqualTo(String.format("/auth/realms/%s/.well-known/openid-configuration", keycloakRealm)))
                    .willReturn(aResponse()
                            .withHeader("Content-Type", "application/json")
                            .withBody(openidConfig)
                    )
            );
            stubFor(WireMock.get(urlEqualTo(String.format("/auth/realms/%s/protocol/openid-connect/certs", keycloakRealm)))
                    .willReturn(aResponse()
                            .withHeader("Content-Type", "application/json")
                            .withBody(new JsonWebKeySet(rsaJsonWebKey).toJson())
                    )
            );
            testSetupIsCompleted = true;
        }
    }

    @Test
    public void When_access_token_is_in_header_Then_process_request_with_Ok() throws Exception {
        ResultActions resultActions = this.mockMvc
                .perform(get("/test")
                        .header("Authorization",String.format("Bearer %s", generateJWT(true)))
                );
        resultActions
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().string("hello"));
    }

    @Test
    public void When_access_token_is_missing_Then_redirect_to_login() throws Exception {
        ResultActions resultActions = this.mockMvc
                .perform(get("/test"));
        resultActions
                .andDo(print())
                .andExpect(status().isFound())
                .andExpect(redirectedUrl("/sso/login"));
    }

    private String generateJWT(boolean withTenantClaim) throws JoseException {

        // Create the Claims, which will be the content of the JWT
        JwtClaims claims = new JwtClaims();
        claims.setJwtId(UUID.randomUUID().toString()); // a unique identifier for the token
        claims.setExpirationTimeMinutesInTheFuture(10); // time when the token will expire (10 minutes from now)
        claims.setNotBeforeMinutesInThePast(0); // time before which the token is not yet valid (2 minutes ago)
        claims.setIssuedAtToNow(); // when the token was issued/created (now)
        claims.setAudience("account"); // to whom this token is intended to be sent
        claims.setIssuer(String.format("%s/auth/realms/%s",keycloakBaseUrl,keycloakRealm)); // who creates the token and signs it
        claims.setSubject(UUID.randomUUID().toString()); // the subject/principal is whom the token is about
        claims.setClaim("typ","Bearer"); // set type of token
        claims.setClaim("azp","example-client-id"); // Authorized party  (the party to which this token was issued)
        claims.setClaim("auth_time", NumericDate.fromMilliseconds(Instant.now().minus(11, ChronoUnit.SECONDS).toEpochMilli()).getValue()); // time when authentication occured
        claims.setClaim("session_state", UUID.randomUUID().toString()); // keycloak specific ???
        claims.setClaim("acr", "0"); //Authentication context class
        claims.setClaim("realm_access", Map.of("roles",List.of("offline_access","uma_authorization","user"))); //keycloak roles
        claims.setClaim("resource_access", Map.of("account",
                    Map.of("roles", List.of("manage-account","manage-account-links","view-profile"))
                )
        ); //keycloak roles
        claims.setClaim("scope","profile email");
        claims.setClaim("name", "John Doe"); // additional claims/attributes about the subject can be added
        claims.setClaim("email_verified",true);
        claims.setClaim("preferred_username", "doe.john");
        claims.setClaim("given_name", "John");
        claims.setClaim("family_name", "Doe");

        // A JWT is a JWS and/or a JWE with JSON claims as the payload.
        // In this example it is a JWS so we create a JsonWebSignature object.
        JsonWebSignature jws = new JsonWebSignature();

        // The payload of the JWS is JSON content of the JWT Claims
        jws.setPayload(claims.toJson());

        // The JWT is signed using the private key
        jws.setKey(rsaJsonWebKey.getPrivateKey());

        // Set the Key ID (kid) header because it's just the polite thing to do.
        // We only have one key in this example but a using a Key ID helps
        // facilitate a smooth key rollover process
        jws.setKeyIdHeaderValue(rsaJsonWebKey.getKeyId());

        // Set the signature algorithm on the JWT/JWS that will integrity protect the claims
        jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);

        // set the type header
        jws.setHeader("typ","JWT");

        // Sign the JWS and produce the compact serialization or the complete JWT/JWS
        // representation, which is a string consisting of three dot ('.') separated
        // base64url-encoded parts in the form Header.Payload.Signature
        return jws.getCompactSerialization();
    }

    @RestController
    public static class TestController {
        @GetMapping("/test")
        public String test() {
            return "hello";
        }
    }

}

wiremock。属性:

wiremock.server.baseUrl=http://localhost:${wiremock.server.port}
keycloak.auth-server-url=${wiremock.server.baseUrl}/auth

注释@AutoConfigreWireMock(port=0)将在随机端口启动WireMock服务器,该端口自动设置为属性wiremock.server.port,因此可用于相应地覆盖Spring Boot Keycloak Adapter的keycloak.auth-server-url属性(请参阅wiremock.properties)

为了生成用作访问令牌的JWT,我确实使用jose4j创建了一个RSA密钥对,该密钥对被声明为测试类属性,因为我确实需要在测试设置期间与WireMock服务器一起初始化它。

private static RsaJsonWebKey rsaJsonWebKey;

然后在测试设置期间初始化如下:

rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048);
            rsaJsonWebKey.setKeyId("k1");
            rsaJsonWebKey.setAlgorithm(AlgorithmIdentifiers.RSA_USING_SHA256);
            rsaJsonWebKey.setUse("sig");

keyId的选择并不重要。您可以选择任何您想要的,只要它被设置。所选择的算法和使用确实很重要,并且必须完全按照示例中的方式进行调整。

通过这种方式,可以按照如下方式相应地设置KeyClope存根的JSON Web密钥存储endpoint:

stubFor(WireMock.get(urlEqualTo(String.format("/auth/realms/%s/protocol/openid-connect/certs", keycloakRealm)))
                    .willReturn(aResponse()
                            .withHeader("Content-Type", "application/json")
                            .withBody(new JsonWebKeySet(rsaJsonWebKey).toJson())
                    )
            );

除此之外,另一个endpoint需要像前面提到的那样为KeyClope设置存根。如果没有缓存,KeyClope适配器需要请求openid配置。作为一个简单的工作示例,所有endpoint都需要在配置中定义,该配置是从OpenId配置endpoint返回的:

String openidConfig = "{\n" +
                    "  \"issuer\": \"" + keycloakBaseUrl + "/auth/realms/" + keycloakRealm + "\",\n" +
                    "  \"authorization_endpoint\": \"" + keycloakBaseUrl + "/auth/realms/" + keycloakRealm + "/protocol/openid-connect/auth\",\n" +
                    "  \"token_endpoint\": \"" + keycloakBaseUrl + "/auth/realms/" + keycloakRealm + "/protocol/openid-connect/token\",\n" +
                    "  \"token_introspection_endpoint\": \"" + keycloakBaseUrl + "/auth/realms/" + keycloakRealm + "/protocol/openid-connect/token/introspect\",\n" +
                    "  \"userinfo_endpoint\": \"" + keycloakBaseUrl + "/auth/realms/" + keycloakRealm + "/protocol/openid-connect/userinfo\",\n" +
                    "  \"end_session_endpoint\": \"" + keycloakBaseUrl + "/auth/realms/" + keycloakRealm + "/protocol/openid-connect/logout\",\n" +
                    "  \"jwks_uri\": \"" + keycloakBaseUrl + "/auth/realms/" + keycloakRealm + "/protocol/openid-connect/certs\",\n" +
                    "  \"check_session_iframe\": \"" + keycloakBaseUrl + "/auth/realms/" + keycloakRealm + "/protocol/openid-connect/login-status-iframe.html\",\n" +
                    "  \"registration_endpoint\": \"" + keycloakBaseUrl + "/auth/realms/" + keycloakRealm + "/clients-registrations/openid-connect\",\n" +
                    "  \"introspection_endpoint\": \"" + keycloakBaseUrl + "/auth/realms/" + keycloakRealm + "/protocol/openid-connect/token/introspect\"\n" +
                    "}";
stubFor(WireMock.get(urlEqualTo(String.format("/auth/realms/%s/.well-known/openid-configuration", keycloakRealm)))
                    .willReturn(aResponse()
                            .withHeader("Content-Type", "application/json")
                            .withBody(openidConfig)
                    )
            );

令牌的生成是在GenerateJWT()中实现的,大量使用jose4j。这里需要注意的最重要的一点是,必须使用生成的JWK的私钥,该私钥与wiremck测试设置期间初始化的私钥相同。

jws.setKey(rsaJsonWebKey.getPrivateKey());

除此之外,代码主要改编自https://bitbucket.org/b_c/jose4j/wiki/JWT例子
现在可以根据自己的特定测试设置调整或扩展声明。发布的代码片段中的最小示例代表了KeyClope生成的JWT的典型示例。

生成的JWT可以像往常一样在授权标头中用于向RESTendpoint发送请求:

ResultActions resultActions = this.mockMvc
                .perform(get("/test")
                        .header("Authorization",String.format("Bearer %s", generateJWT(true)))
                );

为了表示一个独立的示例,测试类确实有一个简单的Restcontroller,它被定义为一个内部类,用于测试。

@RestController
public static class TestController {
    @GetMapping("/test")
    public String test() {
        return "hello";
    }
}

出于测试目的,我引入了一个定制的TestController,因此有必要定义一个定制的ContextConfiguration,将其加载到WebMvcTest中,如下所示:

@ContextConfiguration(classes= {KeycloakTest.TestController.class, SecurityConfig.class, CustomKeycloakSpringBootConfigResolver.class})

除了TestController本身之外,还包括一系列关于Spring Security性和KeyClope适配器的配置bean,如SecurityConfig。类和CustomKeyClope SpringBootConfigResolver。上课让它工作。当然,这些需要由您自己的配置来取代。为了完整起见,这些类别也将列在下面:

SecurityConfig.java:

@Configuration
@EnableWebSecurity
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) {
        SimpleAuthorityMapper grantedAuthorityMapper = new SimpleAuthorityMapper();
        grantedAuthorityMapper.setPrefix("ROLE_");

        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(grantedAuthorityMapper);
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

    /*
     * Workaround for reading the properties for the keycloak adapter (see https://stackoverflow.com/questions/57787768/issues-running-example-keycloak-spring-boot-app)
     */
    @Bean
    @Primary
    public KeycloakConfigResolver keycloakConfigResolver(KeycloakSpringBootProperties properties) {
        return new CustomKeycloakSpringBootConfigResolver(properties);
    }

    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    @Bean
    @Override
    @ConditionalOnMissingBean(HttpSessionManager.class)
    protected HttpSessionManager httpSessionManager() {
        return new HttpSessionManager();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http
                .authorizeRequests()
                .antMatchers("/**").hasRole("user")
                .anyRequest().authenticated()
                .and().csrf().disable();
    }
}

CustomKeycloakSpringBootConfigResolver.java:

 /*
  * Workaround for reading the properties for the keycloak adapter (see https://stackoverflow.com/questions/57787768/issues-running-example-keycloak-spring-boot-app)
  */
@Configuration
public class CustomKeycloakSpringBootConfigResolver extends KeycloakSpringBootConfigResolver {
    private final KeycloakDeployment keycloakDeployment;

    public CustomKeycloakSpringBootConfigResolver(KeycloakSpringBootProperties properties) {
        keycloakDeployment = KeycloakDeploymentBuilder.build(properties);
    }

    @Override
    public KeycloakDeployment resolve(HttpFacade.Request facade) {
        return keycloakDeployment;
    }
}
 类似资料:
  • 我有一个iOS应用程序,实现了与CognitoYourUserPoolsSample非常相似的Cognito身份验证。最重要的片段在SignInViewController.swift中: > 后来,我们得到成功或错误的响应: 我也有一个用户界面测试,它填充用户名和密码,点击登录并验证响应: 目前,该测试针对的是实际的AWS基础设施,由于许多原因,该基础设施并不理想。我想要的是模拟来自AWS的各种

  • 我有一个Keycloak Cordova本地应用程序,一旦启动,就会正确显示Keycloak的用户名/密码表单,但在成功验证后,当试图重定向到该应用程序时,会显示err_connection_rejection。重定向当前设置为“http://localhost”。 我使用了这个例子https://github.com/keycloak/keycloak/tree/master/examples/

  • 我有一个Spring应用程序,它使用Kafka实例而没有任何身份验证。 现在故事发生了变化,Kafka离开了应用程序并作为集群运行。我收到了Kafka凭据用户名和密码以及主机名:端口信息。 其他信息,我需要连接到Kafka集群。 是否需要进行任何代码更改?还是我只需要在应用程序配置文件.yaml文件中添加一些信息? 我尝试了Google中建议的不同方法,但似乎对我没有任何效果,我一直收到: 错误:

  • 问题内容: 我想在我的Java应用程序中使用Windows NTLM认证来透明地认证Intranet用户。如果使用浏览器(单点登录),用户将不会注意到任何身份验证。 我发现了一些具有NTLM支持的库,但是不知道要使用哪个库: http://spnego.sourceforge.net/ http://sourceforge.net/projects/ntlmv2auth/ http://jcifs

  • react中的前端 基于Spring Boot“resource-service”的后端服务 keycloak 其他后端服务(使用者) 前端和使用者服务都使用REST API与后端通信。我们使用Keycloak作为我们的用户管理和身份验证服务。 我们希望通过提供web应用程序和服务流来将基于Spring的服务“资源服务”与Keycloak集成: > 基于web application-react的

  • 我有一门课: 如何测试它?我试过这样做:如何使用JUnit测试类的验证注释?但这不起作用,因为我的验证器中的validate方法需要传递给它的方法签名的类。 我不知道是否可以将此错误传递给验证器。还有别的办法吗?