1. Environment preparation

1.1, Nginx

Nginx version: 1.12.0

Nginx requires the http_SSL_module to be installed in order to support Https. The — with-http_SSL_module parameter is required at compile time.

./configure --prefix=/usr/local/nginx --with-http_ssl_module
make && make install
Copy the code

/nginx -v to see if there is a — with-http_SSL_module parameter.

1.2. Openssl generates public and private keys

The openssl command can be used to generate public and private keys on either the client or the server, provided that OpenSSL is installed.

Generate the server private key

openssl genrsa [-out filename] [numbits]
Copy the code

For example, generate a private key named server.key. The length is 1024.

openssl genrsa -out server.key 1024
Copy the code

Generate a server public key certificate

openssl req -new -x509 [-key keyfile] [-out crtfile] [-days numdays]
Copy the code

For example, a certificate named server. CRT is generated with a validity period of 10 years.

openssl req -new -x509 -key server.key -out server.crt -days 3650
Copy the code

Country, province, company, domain name, email and other information will be required in turn.

Common Name is the domain Name of the server, for example, test.com. You can also enter a generic domain name, such as *.test.com. If you do not have a domain name, enter the server IP address directly.

The private key server.key and public key certificate server. CRT have been generated after the preceding two steps.

Generate the client public and private keys

#Generate the client certificate private key
openssl genrsa -out client.key 1024
#Generate a client public key certificate
openssl req -new -x509 -key client.key -out client.crt -days 3650
#Generate client p12 format certificate, need to enter a password, choose a easy to remember, such as 123456
openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.p12
Copy the code

The p12 file is used by clients (including Postman, browsers, and Java clients) to initiate HTTPS requests to provide public and private keys.

You can also use the Java keyTool to generate public and private keys and convert them to the public and private keys generated by OpenSSL. For details, please refer to the appendix at the end of this article.

2. One-way authentication configuration and client invocation

2.1. Nginx configuration

Edit the nginx.conf file in HTTP {… } Add a server configuration block to the configuration block.

Load balancing configuration
upstream backend {
    server 192.168.0.1:10900;
    server 192.168.0.2:10900;
    server 192.168.0.3:10900;
}

server {
    listen       21000 ssl;
    server_name  localhost;

    ssl_certificate      ../ssl/server.crt;  Server public key certificate
    ssl_certificate_key  ../ssl/server.key;  # server private key

    ssl_session_cache    shared:SSL:1m;
    ssl_session_timeout  5m;

    ssl_ciphersHIGH:! aNULL:! MD5;ssl_prefer_server_ciphers  on;

    access_log logs/access1.log; # Specify access log output separately
    error_log logs/error1.log; # # Specifies the error log output separately

    location / {
        proxy_passhttp://backend; }}Copy the code

For one-way authentication, you only need to configure the public and private keys of the server. The relative path is relative to the path of the Nginx configuration file nginx.conf. The relative path of the output log is relative to the path of the conf directory.

2.2 Postman call

You just need to replace the original HTTP request with HTTPS, and nothing else needs to be changed.

2.3. Browser Invocation

You can just replace HTTP with HTTPS.

If the browser says riskNET::ERR_CERT_AUTHORITY_INVALID, this is mainly because the server certificate is generated and issued by ourselves through the command, not issued by the formal CA organization, so the browser does not trust, you can clickAdvanced -> ContinueCan. Note that it’s best to use Google Chrome here, some domestic browsers may promptNET::ERR_CERT_REVOKEDThe certificate has been revoked and cannot be tested.

2.4. Java client invocation

Httpclient is used as an HTTP library. Hutool is recommended here.

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.2.4</version>
</dependency>
Copy the code
HttpResponse httpResponse = HttpRequest.post("https://ip:port/senddata")
                .contentType("text/plain")
                .body("requestBody")
                .execute();
System.out.println(httpResponse.getStatus());
System.out.println(httpResponse.body());
Copy the code

Just replace the original HTTP with HTTPS, very convenient!

3. Two-way authentication configuration and client invocation

3.1. Nginx configuration

Also in the HTTP {… } Add a server configuration block to the configuration block.

Load balancing configuration
upstream backend {
    server 192.168.0.1:10900;
    server 192.168.0.2:10900;
    server 192.168.0.3:10900;
}

server {
    listen       21000 ssl;
    server_name  a.com;

    ssl_certificate      ../ssl/server.crt;  Server public key certificate
    ssl_certificate_key  ../ssl/server.key;  # server private key
    ssl_client_certificate ../clientcrt/clientA.crt;  # client Public key certificate
    ssl_verify_client on;  # Enable client certificate verification

    ssl_session_cache    shared:SSL:1m;
    ssl_session_timeout  5m;

    ssl_ciphersHIGH:! aNULL:! MD5;ssl_prefer_server_ciphers  on;

    access_log logs/a_access.log;
    error_log logs/a_error.log;

    location / {
        proxy_passhttp://backend; }}Copy the code

In bidirectional authentication, both the client and the server need to authenticate the client. Therefore, compared with one-way authentication, the following two configuration parameters are available:

  • ssl_verify_client onIndicates that bidirectional authentication is enabled. The server also needs to authenticate the client. The default value isoffShut down.
  • ssl_client_certificateThe path for storing the client public key certificate is specified.

3.2 Postman call

  1. In setting upGeneralIn the firstSSL certificate verificationTurn it off.

  1. Then, inCertificatesTo configure the client public and private key certificate. The IP address and port must be the same as the actual one; otherwise, authentication fails.

  1. Or you can just configure itp12File, also to configurep12Password of the file.p12A file can be thought of as a combination of a pair of public and private keys, usually password protected. Can be achieved byopensslCommand generation (the public and private key two files are synthesized into ap12File).

  1. Making the last request

3.3. Browser Invocation

Browsers generally use one-way authentication more, the detailed configuration steps of two-way authentication here is not much verbose. Import the P12 file of the client to the certificate list of the PC and then access the server. If the certificate of the server is at risk, click Continue.

3.4. Java client invocation

Here we use HttpClient to initiate HTTPS requests for two-way authentication.

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.8</version>
</dependency>
Copy the code

But there are also two ways:

  1. One is to import the server public key certificate to the clientJDKthecacertsFile;
  2. The other is to generate a public key certificate for the servertruststoreTruststore, which the client program reads and initiateshttpsRequest two-way authentication.
3.4.1 Import cacerts for access
#Switch to the JDK's Security directory
cd $JAVA_HOME/jre/lib/security
#Import the server certificate into the cacerts file and specify the alias myServer. The -file parameter specifies the path of the server public key certificate
keytool -import -alias myserver -keystore cacerts -storepass changeit -file C:/Users/my/Desktop/cert/server_ssl/server.crt

#Viewing all Certificates
keytool -list -keystore cacerts -storepass changeit
#Deletes the certificate for the specified alias
keytool -delete -alias myserver -keystore cacerts -storepass changeit
Copy the code

The sample code

import org.apache.http.HttpEntity;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import org.junit.Test;

import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.KeyStore;

public class SSLTestCase {
    /** * Client P12 certificate path */
    private String pfxPath = "C:/Users/my/Desktop/cert/client.p12";
    /** * Client P12 certificate password */
    private String pfxPasswd = "123456";

    private String url = "https://139.9.127.172:21000/senddata";

    @Test
    public void SSLTestCase(a) throws Exception {
        ** * 1. Load the P12 certificate */
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        InputStream instream = new FileInputStream(new File(pfxPath));
        try {
            keyStore.load(instream, pfxPasswd.toCharArray());
        } finally {
            instream.close();
        }
        SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, pfxPasswd.toCharArray()).build();
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext,
                new String[]{"TLSv1"}, // supportedProtocols, which can be set as required
                null,
                SSLConnectionSocketFactory.getDefaultHostnameVerifier());
        /**
         * 2.使用httpclient4.5.8发送post请求
         */
        CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
        // Set the timeout period
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(5000)
                .setSocketTimeout(30000).build();
        CloseableHttpResponse response = null;
        try {
            HttpPost httpPost = new HttpPost(url);
            httpPost.setConfig(requestConfig);
// httpPost.addHeader("Connection", "keep-alive"); // Set some headers, etc
            String requestBody = "requestBody";
// StringEntity stringEntity = new StringEntity(requestBody, "UTF-8");
            StringEntity stringEntity = new StringEntity(requestBody, ContentType.create("text/plain"."UTF-8"));
            httpPost.setEntity(stringEntity);
            response = httpclient.execute(httpPost);
            HttpEntity entity = response.getEntity();
            String respBody = EntityUtils.toString(response.getEntity(), "UTF-8");
            EntityUtils.consume(entity);
            System.out.println(respBody);
        } finally {
            if(httpclient ! =null) {
                httpclient.close();
            }
            if(response ! =null) { response.close(); }}}}Copy the code
3.4.2. Generate the TrustStore trust library file for access

If the JDK/JRE of the server cannot be changed arbitrarily, we can also generate a trustStore truststore, and the program will read the certificate in this truststore.

#-keystore specifies the generated trustStore file, and -file specifies the path of the service public key certificate
keytool -keystore C:/Users/my/Desktop/cert/server_ssl/server.truststore -keypass 654321 -storepass 654321 -alias myservertruststore -import -trustcacerts -file C:/Users/my/Desktop/cert/server_ssl/server.crt
Copy the code

The sample code

import org.apache.http.HttpEntity;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.Test;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.SecureRandom;

public class SSLTestCase2 {
    /** * Client P12 certificate path */
    private String pfxPath = "C:/Users/my/Desktop/cert/client.p12";
    /** * Client P12 certificate password */
    private String pfxPasswd = "123456";
    /**
     * 信任库路径
     */
    private String trustStroreFile = "C:/Users/my/Desktop/cert/server_ssl/server.truststore";
    /** * truststore password */
    private String trustStorePwd = "654321";

    private String url = "https://139.9.127.172:21000/senddata";

    @Test
    public void SSLTestCase2(a) throws Exception {
        /**
         * 1.初始化密钥库
         */
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
        KeyStore keyStore = getKeyStore(pfxPath, pfxPasswd, "PKCS12");
        keyManagerFactory.init(keyStore, pfxPasswd.toCharArray());

        /** * 2. Initialize truststore */
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
        KeyStore trustkeyStore = getKeyStore(trustStroreFile, trustStorePwd, "JKS");
        trustManagerFactory.init(trustkeyStore);

        SSLContext sslContext = SSLContext.getInstance("SSL");
        sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext,
                new String[]{"TLSv1"}, // supportedProtocols, which can be set as required
                null,
                SSLConnectionSocketFactory.getDefaultHostnameVerifier());

        /**
         * 3.使用httpclient4.5.8发送post请求
         */
        CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
        // Set the timeout period
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(5000)
                .setSocketTimeout(30000).build();
        CloseableHttpResponse response = null;
        try {
            HttpPost httpPost = new HttpPost(url);
            httpPost.setConfig(requestConfig);
// httpPost.addHeader("Connection", "keep-alive"); // Set some headers, etc
            String requestBody = "requestBody";
// StringEntity stringEntity = new StringEntity(requestBody, "UTF-8");
            StringEntity stringEntity = new StringEntity(requestBody, ContentType.create("text/plain"."UTF-8"));
            httpPost.setEntity(stringEntity);
            response = httpclient.execute(httpPost);
            HttpEntity entity = response.getEntity();
            String respBody = EntityUtils.toString(response.getEntity(), "UTF-8");
            EntityUtils.consume(entity);
            System.out.println(respBody);
        } finally {
            if(httpclient ! =null) {
                httpclient.close();
            }
            if(response ! =null) { response.close(); }}}private KeyStore getKeyStore(String pfxPath, String pfxPasswd, String type) throws Exception {
        KeyStore keyStore = KeyStore.getInstance(type);
        InputStream instream = new FileInputStream(new File(pfxPath));
        try {
            keyStore.load(instream, pfxPasswd.toCharArray());
        } finally {
            instream.close();
        }
        returnkeyStore; }}Copy the code

After summarizing several client invocation methods of bidirectional authentication, we can find that only Java client invocation requires server certificate. With Postman, browser and other client tools, we do not configure server certificates, because when the connection is initially established, the server will send its certificate to the client for authentication.

3.5. The client obtains the server public key certificate

Sometimes, the server public key certificate in the production environment cannot be easily used by the client. In this case, the client needs to execute an OpenSSL command to obtain the server public key certificate, provided that the Nginx service needs to be started.

Openssl s_client - connect 139.9.127.172:21000 < / dev/null | sed - ne '/ - BEGIN CERTIFICATE - /, / - END CERTIFICATE - / p' >./server.crtCopy the code
  • – connect:NginxIP address and port number of the server.
  • The public key certificate of the server is exported to the local directory of the clientserver.crtFile.

4. Access multiple clients through bidirectional authentication

Most of the time, as a server to connect to multiple clients, each client has its own certificate, Nginx server needs to configure a server block for each access client channel for two-way authentication. Since it is multiple server configuration blocks, it will involve the question of which server block to match the access client for two-way authentication.

At first Nginx will match different listening ports, but this will open up a new port for each access client channel. You can use one listening port to access HTTPS requests from all clients and verify the validity of certificates of each channel in the following two ways.

4.1 SNI multiple domain names match different certificates

That’s what you need to use hereSNIFunction. If the compilerNginxOpen thehttp_ssl_moduleModule, usually also enabled by defaultSNIFunctional, can pass./nginx -VCommand to view.

Nginx is configured with multiple vhosts

Load balancing configuration
upstream backend {
    server 192.168.0.1:10900;
    server 192.168.0.2:10900;
    server 192.168.0.3:10900;
}

# channel A
server {
    listen       21000 ssl;
    server_name  a.test.com;

    ssl_certificate      ../ssl/server.crt;  Server public key certificate
    ssl_certificate_key  ../ssl/server.key;  # server private key
    ssl_client_certificate ../clientcrt/clientA.crt;  # client Public key certificate
    ssl_verify_client on;  # Enable client certificate verification

    ssl_session_cache    shared:SSL:1m;
    ssl_session_timeout  5m;

    ssl_ciphersHIGH:! aNULL:! MD5;ssl_prefer_server_ciphers  on;

    access_log logs/a_access.log;
    error_log logs/a_error.log;

    location / {
        proxy_passhttp://backend; }}# channel B
server {
    listen       21000 ssl;
    server_name  b.test.com;

    ssl_certificate      ../ssl/server.crt;  Server public key certificate
    ssl_certificate_key  ../ssl/server.key;  # server private key
    ssl_client_certificate ../clientcrt/clientB.crt;  # client Public key certificate
    ssl_verify_client on;  # Enable client certificate verification

    ssl_session_cache    shared:SSL:1m;
    ssl_session_timeout  5m;

    ssl_ciphersHIGH:! aNULL:! MD5;ssl_prefer_server_ciphers  on;

    access_log logs/b_access.log;
    error_log logs/b_error.log;

    location / {
        proxy_passhttp://backend; }}Copy the code

The listen port configuration is the same, but the server_name of each Vhost is different. The domain name (Host field in the Http request header) in the URL to be requested by the client must be the same as the configured server_name. Such as:

  • a.test.com/senddata Client channel A requests the URL
  • b.test.com/senddata Client channel B requests the URL

Domain name resolution for clients can be configured using the DOMAIN name resolution server or in the local hosts file.

Note the following: If the SNI function is used, when the server issues the public key certificate, the domain Name Common Name must be a generic domain Name, for example, *.test.com. In this way, the client can pass the authentication of the server domain name.

In addition, if Nginx matches multiple vhosts on the same port and no matching server_name is found, the default vhost (default first) will be used for authentication. To avoid some of the problems associated with implicit matching, default_server explicitly specifies a default Vhost that returns 401.

server {
    listen 21000 ssl default_server;

    ssl_certificate      ../ssl/server.crt;  Server public key certificate
    ssl_certificate_key  ../ssl/server.key;  # server private key

    access_log logs/default_access.log;
    error_log logs/default_error.log;

    location / {
        return 401; }}Copy the code

4.2 CA root Certificates Issue client certificates in a unified manner

First unified generationCAThe root certificate is then derived from the server and client certificates.Generate a root certificate

#Create a root certificate private key
openssl genrsa -out root.key 1024
#Create the root certificate request file
openssl req -new -key root.key -out root.csr
#Creating a Root Certificate
openssl x509 -req -in root.csr -out root.crt -signkey root.key -CAcreateserial -days 3650
Copy the code

Generate a server certificate

#Generate the server certificate private key
openssl genrsa -out server.key 1024
#Generate the server certificate request file
openssl req -new -key server.key -out server.csr
#Generate a server public key certificate
openssl x509 -req -in server.csr -out server.crt -signkey server.key -CA root.crt -CAkey root.key -CAcreateserial -days 3650
Copy the code

Generate a client certificate

#Generate the client certificate private key
openssl genrsa -out client.key 1024
#Generate the client certificate request file
openssl req -new -key client.key -out client.csr
#Generate a client certificate
openssl x509 -req -in client.csr -out client.crt -signkey client.key -CA root.crt -CAkey root.key -CAcreateserial -days 3650
#Generate client p12 format certificate, need to enter a password, choose a easy to remember, such as 123456
openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.p12
Copy the code

Note that the Common Name field of the root certificate is different from that of the client certificate and server certificate.

The path to the root certificate is then configured in the SSL_client_certificate field in Nginx so that all client certificates it issues can be verified. There is no need to create a server configuration block for each client channel to authenticate.

Unified root certificate authentication
server {
    listen       21000 ssl;
    server_name  localhost;

    ssl_certificate      ../ssl/server.crt;  Server public key certificate
    ssl_certificate_key  ../ssl/server.key;  # server private key
    ssl_client_certificate ../ssl/root.crt;  # root certificate, which validates all client certificates it issues
    ssl_verify_client on;  # Enable client certificate verification

    ssl_session_cache    shared:SSL:1m;
    ssl_session_timeout  5m;

    ssl_ciphersHIGH:! aNULL:! MD5;ssl_prefer_server_ciphers  on;

    access_log logs/access.log;
    error_log logs/error.log;

    location / {
        proxy_passhttp://192.168.0.1:10900; }}Copy the code

Refer to the link

  • How do I use Java to access bidirectionally authenticated Https resources
  • HTTPS bidirectional authentication guide
  • KeyTool and OpenSSL convert each other

Personal website

  • Github Pages
  • Gitee Pages

The appendix

Keytool Related commands

Keytool Generates a certificate

#Generate the server JKS
keytool -genkey -alias servertest -keysize 2048 -validity 3650 -keyalg RSA -dname "CN=client.test.com, OU=R & D department, O=\"BJ SOS Software Tech Co., Ltd\", L=Beijing, S=Beijing, C=CN" -keypass 123456 -storepass 123456 -keystore server.jks
#-storepass Specifies the password of the keystore (the password required to obtain keystore information) 
#-keypass Specifies the password for the alias entry (the password for the private key) 
#View keystore information
keytool -list -v -keystore server.jks -storepass 123456

#Export the public key certificate from JKS
keytool -export -alias servertest -keystore server.jks -storepass 123456 -file server.crt
#View the exported certificate information
keytool -printcert -file server.crt

#Generate client JKS
keytool -genkey -alias clienttest -keysize 2048 -validity 3650 -keyalg RSA -dname "CN=client.test.com, OU=R & D department, O=\"BJ SOS Software Tech Co., Ltd\", L=Beijing, S=Beijing, C=CN" -keypass 123456 -storepass 123456 -keystore client.jks
#Import the server public key certificate into the client JKS truststore
keytool -import -trustcacerts -alias servertest -file server.crt -storepass 123456 -keystore client.jks
#View keystore information
keytool -list -v -keystore client.jks -storepass 123456
Copy the code

openssl 2 keytool

#Generate the private key
openssl genrsa -out client.key 1024
#Generating a Public Key Certificate
openssl req -new -x509 -key client.key -out client.crt -days 3650
#Compositing p12 files
openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.p12 -passout pass:123456 -name clienttest
#P12 file transfer to JKS
keytool -importkeystore -srcstoretype PKCS12 -srckeystore client.p12 -srcstorepass 123456 -srcalias clienttest -deststoretype JKS -destalias clienttest -deststorepass 123456 -destkeypass 123456 -destkeystore client.jks
Copy the code

keytool 2 openssl

#Generate JKS
keytool -genkey -alias clienttest -keysize 2048 -validity 3650 -keyalg RSA -dname "CN=client.test.com, OU=R & D department, O=\"BJ SOS Software Tech Co., Ltd\", L=Beijing, S=Beijing, C=CN" -keypass 123456 -storepass 123456 -keystore client.jks
#Export the public key certificate from JKS
keytool -export -alias clienttest -keystore client.jks -storepass 123456 -file client.crt
#JKS to p12 file
keytool -importkeystore -srcstoretype JKS -srckeystore client.jks -srcstorepass 123456 -srcalias clienttest -srckeypass 123456 -deststoretype PKCS12 -destkeystore client.p12 -deststorepass 123456 -destalias clienttest -destkeypass 123456 -noprompt
#P12 file to PEM format
openssl pkcs12 -in client.p12 -out client.pem.p12 -passin pass:123456 -passout pass:123456
#Export the private key from a file in PEM format
openssl rsa -in client.pem.p12 -passin pass:123456 -out client.key -passout pass:123456
Copy the code