Spring Boot SSL 客户端

16

我是Spring Boot的新手。到目前为止,我很喜欢它。我开发了一个演示SSL rest web服务器,可以正确处理相互X.509证书身份验证。使用带有自签名客户端和服务器证书的IE浏览器,我已经测试了演示rest web服务器是否正常工作——服务器和浏览器都能够成功地交换并验证对方的证书。

我遇到了一个问题,就是找不到一个SSL客户端示例,它展示了如何包含客户端证书并发出https请求。有没有人有一个简单的rest客户端示例,展示了如何消费我的ssl服务器?

最好的问候, Steve Mansfield


你考虑的客户端是什么?Java(带Spring)?还是其他的东西? - derkoe
Spring是最好的选择,但Java也可以。 - skmansfield
嗨,史蒂夫,我偶然看到了这个问题,想知道为什么需要包含SSL客户端代码? - Robin
这可能不是你想要的答案,但我开始使用OkHttp客户端了,它的工作方式就像标准的Java。 - user1363516
6个回答

19

假设您正在使用Spring,这里是一个示例,展示了如何使用Spring的RestTemplate和配置有客户端证书的Apache的HttpClient并信任来自服务器的自签名证书:

KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(new FileInputStream(new File("keystore.jks")),
        "secret".toCharArray());
SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
        new SSLContextBuilder()
                .loadTrustMaterial(null, new TrustSelfSignedStrategy())
                .loadKeyMaterial(keyStore, "password".toCharArray()).build());
HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory).build();
ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(
        httpClient);
RestTemplate restTemplate = new RestTemplate(requestFactory);
ResponseEntity<String> response = restTemplate.getForEntity(
        "https://localhost:8443", String.class);

1
代码相关的另一个问题。您在密钥库中有“secret”,然后在loadkeymaterial中有“password”。什么是secret? - skmansfield
1
我认为SSLConnectionSocketFactory没有需要修复的问题。如果有帮助的话,这是它的javadocsecret是整个密钥库的密码。password是密钥库中密钥的密码。 - Andy Wilkinson
谢谢,Andy...我找到了问题并正在解决中。由于某些原因,Spring Boot没有下载必要的JAR文件。现在正在努力寻找它们。明天一早应该会有可行的方案。 - skmansfield
发现编译问题。我正在使用一个低版本的sslconnectionsocketfactory。现在一切都已经编译完成。当我运行程序时,我会得到一个错误--Error = org.springframework.web.client.ResourceAccessException:GET请求出错,访问"https://localhost:8443/restserver"时发生I/O错误:证书中的主机名与实际主机名不匹配:<localhost> != <aws server>;嵌套异常是javax.net.ssl.SSLException:证书中的主机名与实际主机名不匹配:<localhost> != <aws server>。我有一个POJO客户端可以正确地对服务器进行身份验证,所以我相当确定服务器正在正常工作。 - skmansfield
这是一个双向SSL连接,对吧?我猜在单向SSL连接中不需要keyStore。我的意思是只应该使用trustStore。 - Zilev av
显示剩余2条评论

18

user1707141的示例对我无效,而 skmansfield 的示例似乎依赖于特定的文件,并且这些文件与 Spring Boot / Maven 不符。此外,Andy Wilkinson的答案使用了已经在 Apache httpclient 4.4+ 中弃用的构造函数 SSLConnectionSocketFactory,而且看起来相当复杂。

因此,我创建了一个示例项目,应该在这里展示所有东西以确保百分之百的易懂性:https://github.com/jonashackt/spring-boot-rest-clientcertificate

除了在您的测试类中使用 @Autowired 的 RestTemplate 的正常用法之外,请确保按照以下方式配置您的 RestTemplate:

package de.jonashackt.restexamples;

import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContextBuilder;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.util.ResourceUtils;
import org.springframework.web.client.RestTemplate;

import javax.net.ssl.SSLContext;

@Configuration
public class RestClientCertTestConfiguration {

    private String allPassword = "allpassword";

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) throws Exception {

        SSLContext sslContext = SSLContextBuilder
                .create()
                .loadKeyMaterial(ResourceUtils.getFile("classpath:keystore.jks"), allPassword.toCharArray(), allPassword.toCharArray())
                .loadTrustMaterial(ResourceUtils.getFile("classpath:truststore.jks"), allPassword.toCharArray())
                .build();

        HttpClient client = HttpClients.custom()
                .setSSLContext(sslContext)
                .build();

        return builder
                .requestFactory(() -> new HttpComponentsClientHttpRequestFactory(client))
                .build();
    }
}

1
无法自动装配,找不到“RestTemplateBuilder”类型的 bean。 - 袁文涛
如果你没有包含 spring-boot-starter-web 或者根本没有使用 Spring Boot,那么你可能需要检查一下。 - Peter Wippermann

7

我无法让Andy提交的客户端正常工作。一直出现“localhost!= clientname”的错误。不管怎样,我已经成功解决了这个问题。

 import java.io.IOException;

 import org.apache.commons.httpclient.HttpClient;
 import org.apache.commons.httpclient.HttpException;
 import org.apache.commons.httpclient.URI;
 import org.apache.commons.httpclient.methods.GetMethod;

 public class SSLClient {

      static
        {
          System.setProperty("javax.net.ssl.trustStore","c:/apachekeys/client1.jks");
          System.setProperty("javax.net.ssl.trustStorePassword", "password");
          System.setProperty("javax.net.ssl.keyStore", "c:/apachekeys/client1.jks");
          System.setProperty("javax.net.ssl.keyStorePassword", "password");
       }

     public static void main(String[] args) throws HttpException, IOException {

         HttpClient client = new HttpClient();
         GetMethod method = new GetMethod();
         method.setURI(new URI("https://localhost:8443/restserver", false));
         client.executeMethod(method);

         System.out.println(method.getResponseBodyAsString());

     }

 }

3
这是系统级的,它甚至适用于JDBC连接,不建议使用。 - Ali Al-Shishani
当您的服务器需要使用数字证书进行客户端身份验证时,这将非常有用。 - Tariq Abbas

0

我知道现在有点晚了,但这是对我有效的代码。

@SpringBootApplication
public class Application {

    private static final Logger log = LoggerFactory.getLogger(Application.class);

    public static void main(String args[]) {
            makeWebServiceCall();
    }
    
    public static void makeWebServiceCall() {
        TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;

        SSLContext sslContext;
        ResponseEntity<String> response = null;
        try {
            sslContext = org.apache.http.ssl.SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy)
                    .build();

            SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext);

            CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(csf).build();

            HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();

            requestFactory.setHttpClient(httpClient);

            RestTemplate restTemplate = new RestTemplate(requestFactory);
            
            StringBuffer plainCreds = new StringBuffer();
            plainCreds.append("username");
            plainCreds.append(":");
            plainCreds.append("password");
            byte[] plainCredsBytes = plainCreds.toString().getBytes();
            byte[] base64CredsBytes = Base64.getEncoder().encode(plainCredsBytes);
            String userBase64Credentials = new String(base64CredsBytes);
            

            HttpHeaders headers = new HttpHeaders();
            headers.add("Authorization", "Basic " + userBase64Credentials);
            headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
            headers.setContentType(MediaType.APPLICATION_JSON);

            HttpEntity entity = new HttpEntity<>(headers);

            String url = "https:restUrl";
            
            response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
            
            if(response.getStatusCodeValue() == 200) {
                log.info("Success! Further processing based on the need");
            } else {
                log.info("****************Status code received: " + response.getStatusCodeValue() + ".************************");
            }

        } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
            log.error("Exception occured. Here are the exception details: ", e);
        } catch(HttpClientErrorException e) {
            if(e.getRawStatusCode() == 403) {
                log.info("****************Status code received: " + e.getRawStatusCode() + ". You do not have access to the requested resource.************************");
                
            } else if(e.getRawStatusCode() == 404) {
                log.info("****************Status code received: " + e.getRawStatusCode() + ". Resource does not exist(or) the service is not up.************************");

            } else if(e.getRawStatusCode() == 400) {
                log.info("****************Status code received: " + e.getRawStatusCode() + ". Bad Request.************************");

            } else {
                log.info("****************Status code received: " + e.getRawStatusCode() + ".************************");
                
            }

           log.info("****************Response body: " + e.getResponseBodyAsString() + "************************");
        }
    }
 }

这是Maven文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.springframework</groupId>
<artifactId>gs-consuming-rest</artifactId>
<version>0.1.0</version>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.2.RELEASE</version>
</parent>

<properties>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
    
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.3</version>
</dependency>

        
        <!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpcore -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpcore</artifactId>
    <version>4.4.6</version>
</dependency>
    
    
</dependencies>


<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

0

另一种方法是注入 keyStoreLocation 和 keyStorePassword 的值

@Configuration
public class SampleSSLClient extends RestTemplate{

    /** The key store password. */
    private  String keyStorePassword;
  
    /** The key store location. */
    private  String keyStoreLocation;
  
    /** The rest template. */
    @Autowired
    private RestTemplate restTemplate;
    
     /** The http components client http request factory. */
    private HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory;

    /**
     * Instantiates a new custom rest template.
     */
    public CustomRestTemplate() {
        super();
    }
    
    public CustomRestTemplate(RestTemplate restTemplate){
        this.restTemplate = getRestTemplate();
    }
        
    /**
     * Rest template.
     *
     * @return the rest template
     */
    public RestTemplate getRestTemplate()  {
        if (null == httpComponentsClientHttpRequestFactory) {
            httpComponentsClientHttpRequestFactory = loadCert();
            restTemplate.setRequestFactory(httpComponentsClientHttpRequestFactory);
        }
        return restTemplate;
    }

    /**
     * Load cert.
     *
     * @return the http components client http request factory
     */
    private HttpComponentsClientHttpRequestFactory loadCert()  {
        try {
            char[] keypass = keyStorePassword.toCharArray();
            SSLContext sslContext = SSLContextBuilder.create()
                    .loadKeyMaterial(getkeyStore(keyStoreLocation, keypass), keypass)
                    .loadTrustMaterial(null, new TrustSelfSignedStrategy()).build();
            HttpClient client = HttpClients.custom().setSSLContext(sslContext).build();
            httpComponentsClientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(client);
            httpComponentsClientHttpRequestFactory.setConnectTimeout(5000);
            httpComponentsClientHttpRequestFactory.setReadTimeout(30000);
        } catch (Exception ex) {
            LOGGER.error(MessageFormat.format("Some Error", ex.getMessage()), ex);
            
        }
        return httpComponentsClientHttpRequestFactory;
    }

     /**
     * Key store.
     *
     * @param storePath the store path
     * @param password the password
     * @return the key store
     */
    private KeyStore getkeyStore(String storePath, char[] password) {
        KeyStore keyStore;
        try {
            keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            File key = ResourceUtils.getFile(storePath);
            try (InputStream in = new FileInputStream(key)) {
                keyStore.load(in, password);
            }
        }catch (Exception ex) {
            LOGGER.error(MessageFormat.format("Some Error", ex.getMessage()), ex);
        
        }
        return keyStore;
    }

    /**
     * Sets the key store password.
     *
     * @param keyStorePassword the new key store password
     */
    public void setKeyStorePassword(String keyStorePassword) {
        this.keyStorePassword = keyStorePassword;
    }

    /**
     * Sets the key store location.
     *
     * @param keyStoreLocation the new key store location
     */
    public void setKeyStoreLocation(String keyStoreLocation) {
        this.keyStoreLocation = keyStoreLocation;
    }

    /**
     * Sets the rest template.
     *
     * @param restTemplate the new rest template
     */
    public void setRestTemplate(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }
}

吞咽异常并不是最好的做法。 - jannis
1
这被称为“处理预期”@jannis - Sireesh Yarlagadda

-3
这对我有用:
TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;
javax.net.ssl.SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom()
        .loadTrustMaterial(null, acceptingTrustStrategy).build();
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext);
CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(csf).build();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setHttpClient(httpClient);
RestTemplate restTemplate = new RestTemplate(requestFactory);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接