如何配置 TestRestTemplate 以使用密钥库?

How to configure TestRestTemplate to use a Keystore?

我的项目有一系列使用 TestRestTemplateMockMvc 的集成测试。这些都顺利通过了。

我现在已将 Spring Boot Starter SecuritySpring Security OAuth2 Autoconfigure 依赖项添加到我的项目中。我添加了一个扩展 WebSecurityConfigurerAdapter 的自定义 class 以允许(暂时)对我的应用程序进行开放访问。这是 class

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
            .authorizeRequests()
            .anyRequest()
            .permitAll();
    }

    @Override
    public void configure(WebSecurity webSecurity) {
        webSecurity
            .ignoring()
            .antMatchers(HttpMethod.OPTIONS, "/**");
    }
}

该应用程序还需要充当 OAuth2 Resource Server,因此我还用 @EnableResourceServer 注释了我的主要 class。在 运行 应用程序时,我将可信密钥存储的路径作为 运行 参数提供。 -Djavax.net.ssl.trustStore=<where the cert is stored locally> -Djavax.net.ssl.trustStorePassword=<the password>

应用程序工作正常,但现在所有集成测试都失败了。这是使用 TestRestTemplate

的所有测试的常见错误示例
Could not fetch user details: class org.springframework.web.client.ResourceAccessException, I/O error on GET request for <the path to my userinfo URL>: 
PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: 
unable to find valid certification path to requested target; nested exception is javax.net.ssl.SSLHandshakeException: 
PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

似乎需要指示我用于测试的 TestRestTemplate 使用与应用程序相同的密钥库。是否有可能做到这一点?它对 MockMvc 有何影响?

我想你可能还需要通过 -Djavax.net.ssl.trustStore= -Djavax.net.ssl.trustStorePassword= 运行 测试时的参数。 对于 运行 配置中的单个测试传递参数,在 Maven 中也可以传递这些参数。

以下两个链接可能有帮助

Specifying trust store information in spring boot application.properties

http://codeboarding.com/tag/testresttemplate/

谢谢,您发的第一个 link 非常有用。这是我接受任何证书的 RestTemplate 的工作代码,如果其他人发现它有用的话。它仍然取决于所提供的有效令牌,但那是另一回事了。

private RestTemplate buildRestTemplate() throws Exception {
    SSLContext sslContext = new SSLContextBuilder()
        .loadTrustMaterial(
            new TrustSelfSignedStrategy()
        ).build();
    SSLConnectionSocketFactory socketFactory =
        new SSLConnectionSocketFactory(sslContext);
    HttpClient httpClient = HttpClients.custom()
        .setSSLSocketFactory(socketFactory).build();
    HttpComponentsClientHttpRequestFactory factory =
        new HttpComponentsClientHttpRequestFactory(httpClient);
    return new RestTemplate(factory);
}

Spring 启动 2

的解决方案

以下答案针对针对 Spring Boot 2 进行开发并使用自签名证书进行开发的人(推荐用于生产的适当证书 - 请参阅 https://letsencrypt.org/)。

您可以使用 keytool 命令创建包含自签名证书的密钥库文件:-

keytool -genkey -storetype PKCS12 \
    -alias selfsigned_localhost_sslserver \
    -keyalg RSA -keysize 2048 -validity 3650 \
    -dname "CN=localhost, OU=Engineering, O=Acme Corp, L=New York, S=New York, C=US" \
    -noprompt -keypass changeit -storepass changeit \
    -keystore keystore-self-signed.p12

keystore-self-signed.p12 文件将包含一个自签名证书,可以将此文件移至 src/main/resources 文件夹(或 src/test/resources,如果您愿意)。

将以下内容添加到您的 application.yaml Spring 配置中以使用 SSL 并指向密钥库:-

server:
  port: 443
  ssl:
    enabled: true
    key-store: classpath:keystore-self-signed.p12
    key-store-type: PKCS12
    protocol: TLS
    enabled-protocols: TLSv1.2   # Best practice - see https://github.com/ssllabs/research/wiki/SSL-and-TLS-Deployment-Best-Practices
    key-password: changeit
    key-store-password: changeit

让我们创建一个超级简单的 Spring 要测试的引导控制器端点:-

@RestController
public class PingController {

    @GetMapping("/ping")
    public ResponseEntity<String> ping() {
        return new ResponseEntity<>("pong", HttpStatus.OK);
    }

}

我们现在可以使用 curl 命令(或 Postman)即

访问此端点
$ curl https://localhost/ping --insecure --silent
pong

Note: if we don't include --insecure then curl will return curl: (60) SSL certificate problem: self signed certificate.

要使用 TestRestTemplate 测试正确的 Spring 启动集成测试到他的端点,那么我们可以执行以下操作:-

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class PingControllerTest {

    @Value("${server.ssl.key-store}")
    private Resource keyStore;   // inject keystore specified in config

    @Value("${server.ssl.key-store-password}")
    private String keyStorePassword;  // inject password from config

    @LocalServerPort
    protected int port;   // server port picked randomly at runtime

    private TestRestTemplate restTemplate;

    @Before
    public void setup() throws Exception {
        SSLContext sslContext = new SSLContextBuilder()
            .loadTrustMaterial(
                keyStore.getURL(),
                keyStorePassword.toCharArray()
            ).build();
        SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext);
        HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory).build();
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(
            httpClient);
        RestTemplateBuilder rtb = new RestTemplateBuilder()
            .requestFactory(() -> factory)
            .rootUri("https://localhost:" + port);
        this.restTemplate = new TestRestTemplate(rtb, null, null, HttpClientOption.SSL);
    }

    @Test
    public void shouldPing() {
        ResponseEntity<String> result = restTemplate.getForEntity("/ping", String.class);
        assertEquals(HttpStatus.OK, result.getStatusCode());
        assertEquals("pong", result.getBody());
    }


}

如您所见,setup 方法创建了 SSLContext 对象的实例,该实例加载(并“信任”)keystore-self-signed.p12 文件中的自签名证书(注入通过 Spring Resource 对象)。

SSLContext class 被注入到 SSLConnectionSocketFactory 对象中,后者又被注入到 HttpClient 对象中,然后再被注入到 HttpComponentsClientHttpRequestFactory 对象。

这个工厂对象最终被注入到 TestRestTemplate 实例中,用于 shouldPing 集成测试。

注意 - 我最初使用以下代码浪费时间:

...
this.restTemplate = new TestRestTemplate(rgb);

...但这返回...

org.springframework.web.client.ResourceAccessException: I/O error on GET request for "https://localhost:56976/ping": 
    sun.security.validator.ValidatorException: PKIX path building failed: 
    sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target; nested exception is 
    javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: 
    sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

通过 TestRestTemplate 调试后,我意识到必须将 TestRestTemplate 的 4 参数构造函数与 HttpClientOption.SSL 一起使用,即

this.restTemplate = new TestRestTemplate(rtb, null, null, HttpClientOption.SSL);

但是,如果您使用的是正常的 RestTemplate(例如,在 Spring 测试之外),则 以下作品:-

...
RestTemplate restTemplate = new RestTemplate(rgb);

NOTE, to improve - create a @Bean method which returns a TestRestTemplate instance.