Nginx配置Https单向认证、双向认证以及多证书配置
|字数总计:4.3k|阅读时长:18分钟|阅读量:
个人博客
1、环境准备
1.1、Nginx
Nginx
版本:1.12.0
Nginx
为了支持Https
需要安装http_ssl_module
模块。在编译时需要带上--with-http_ssl_module
参数。
1 2
| ./configure --prefix=/usr/local/nginx --with-http_ssl_module make && make install
|
然后通过./nginx -V
查看有没有--with-http_ssl_module
参数。
1.2、openssl生成公私钥
无论是客户端还是服务端,都可以使用openssl
命令来生成公私钥,前提是需要安装好openssl
。
生成服务端私钥
1
| openssl genrsa [-out filename] [numbits]
|
比如:生成一个名为server.key
的私钥,长度1024。
1
| openssl genrsa -out server.key 1024
|
生成服务端公钥证书
1
| openssl req -new -x509 [-key keyfile] [-out crtfile] [-days numdays]
|
比如:生成一个名为server.crt
的证书,有效期10年。
1
| openssl req -new -x509 -key server.key -out server.crt -days 3650
|
依次会要求输入国家、省市、公司单位、域名、邮箱等信息。
最关键的是域名信息Common Name
,这里需要填写服务器的域名地址,比如test.com
;也可以填写泛域名,比如*.test.com
;如果没有域名,可以直接填写服务端ip地址。
通过以上2步,已经生成了私钥server.key
,公钥证书server.crt
。
生成客户端公私钥
1 2 3 4 5 6
| # 生成客户端证书私钥 openssl genrsa -out client.key 1024 # 生成客户端公钥证书 openssl req -new -x509 -key client.key -out client.crt -days 3650 # 生客户端p12格式证书,需要输入一个密码,选一个好记的,比如123456 openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.p12
|
最后会将公私钥两个文件合成得到一个p12
文件,p12
文件主要用于客户端(包括Postman
、浏览器、Java
客户端等)发起https
请求提供公私钥。
还可以利用Java
自带的keytool
工具来生成公私钥,并且可以和openssl
生成的公私钥进行互相转换。具体可以参考文末的附录。
2、单向认证配置
2.1、Nginx配置
编辑nginx.conf
文件在http{...}
配置块中新增一个server
配置块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 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; ssl_certificate_key ../ssl/server.key;
ssl_session_cache shared:SSL:1m; ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on;
access_log logs/access1.log; error_log logs/error1.log;
location / { proxy_pass http://backend; } }
|
单向认证只需要配置服务器的公私钥即可,这里的相对路径是相对Nginx
的配置文件nginx.conf
的路径而言的。而输出日志的相对路径是相对于conf
目录的路径而言。
3、双向认证配置和客户端调用
3.1、Nginx配置
也是在http{...}
配置块中新增一个server
配置块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| 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; ssl_certificate_key ../ssl/server.key; ssl_client_certificate ../clientcrt/clientA.crt; ssl_verify_client on;
ssl_session_cache shared:SSL:1m; ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on;
access_log logs/a_access.log; error_log logs/a_error.log;
location / { proxy_pass http://backend; } }
|
因为是双向认证,不仅客户端要认证服务端,服务端也需要认证客户端,所以相较于单向认证,会多出以下2个配置参数:
ssl_verify_client on
表示开启双向认证,服务端也要认证客户端,该参数默认是off
关闭。ssl_client_certificate
配置客户端公钥证书存放的路径位置。
3.2、Postman调用
- 在设置
General
中先把SSL certificate verification
关掉。
然后在Certificates
中配置客户端公私钥证书。注意这里的地址和端口要与实际的一致,否则请求时会认证失败。
或者可以只配置p12
文件,同时也要配置p12
文件的密码。p12
文件可以认为是一对公私钥的合体文件,通常会有密码保护;可以通过openssl
命令生成(将公私钥两个文件合成得到一个p12
文件)。
最后发起请求
3.3、浏览器调用
浏览器一般用单向认证会比较多,双向认证的详细配置步骤这里就不多啰嗦了。主要就是把自己客户端的p12
文件导入到自己电脑的证书列表中再访问服务端,如果提示服务端的证书有风险,点击继续就行。
3.4、Java客户端调用
这里我们使用httpclient
来发起https
的请求进行双向认证。
1 2 3 4 5
| <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.8</version> </dependency>
|
不过也分为2种方式:
- 一种是要把服务端公钥证书导入到客户端
JDK
的cacerts
文件中; - 另一种则是把服务端的公钥证书自行生成一个
truststore
信任库,由客户端程序读取这个信任库然后发起https
请求进行双向认证。
3.4.1、导入cacerts进行访问
1 2 3 4 5 6 7 8 9
| # 切换到jdk的security目录 cd $JAVA_HOME/jre/lib/security # 将服务端证书导入cacerts文件中,指定别名myserver,-file参数指定的就是服务端公钥证书的路径 keytool -import -alias myserver -keystore cacerts -storepass changeit -file C:/Users/my/Desktop/cert/server_ssl/server.crt
# 查看所有证书 keytool -list -keystore cacerts -storepass changeit # 删除指定别名的证书 keytool -delete -alias myserver -keystore cacerts -storepass changeit
|
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| 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 {
private String pfxPath = "C:/Users/my/Desktop/cert/client.p12";
private String pfxPasswd = "123456";
private String url = "https://139.9.127.172:21000/senddata";
@Test public void SSLTestCase() throws Exception {
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"}, null, SSLConnectionSocketFactory.getDefaultHostnameVerifier());
CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build(); RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(5000) .setSocketTimeout(30000).build(); CloseableHttpResponse response = null; try { HttpPost httpPost = new HttpPost(url); httpPost.setConfig(requestConfig);
String requestBody = "requestBody";
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(); } } } }
|
3.4.2、自行生成truststore信任库文件进行访问
如果服务器的JDK/JRE
不能随便改动,我们还可以自行生成truststore
信任库,由程序来读取这个信任库中的证书。
1 2
| # -keystore参数指定生成后的truststore文件,-file参数指定服务公钥证书路径 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
|
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| 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 {
private String pfxPath = "C:/Users/my/Desktop/cert/client.p12";
private String pfxPasswd = "123456";
private String trustStroreFile = "C:/Users/my/Desktop/cert/server_ssl/server.truststore";
private String trustStorePwd = "654321";
private String url = "https://139.9.127.172:21000/senddata";
@Test public void SSLTestCase2() throws Exception {
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509"); KeyStore keyStore = getKeyStore(pfxPath, pfxPasswd, "PKCS12"); keyManagerFactory.init(keyStore, pfxPasswd.toCharArray());
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"}, null, SSLConnectionSocketFactory.getDefaultHostnameVerifier());
CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build(); RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(5000) .setSocketTimeout(30000).build(); CloseableHttpResponse response = null; try { HttpPost httpPost = new HttpPost(url); httpPost.setConfig(requestConfig);
String requestBody = "requestBody";
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(); } return keyStore; } }
|
总结双向认证的几种客户端调用方式,可以发现只有Java
客户端调用时会需要用到服务端证书;而用Postman
、浏览器这些客户端工具时我们并没有配置服务端证书,是因为在一开始建立连接时,服务端本来就会把自己的证书发给客户端去进行认证。
3.5、客户端获取服务端公钥证书
有时候,产线环境的服务端公钥证书并不能很方便地拿出来给客户端去使用,这时候需要客户端通过执行openssl
的一个命令来获取服务端的公钥证书,当然前提是Nginx
服务需要启动。
1
| openssl s_client -connect 139.9.127.172:21000 </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' >./server.crt
|
- -connect:
Nginx
服务器的ip和端口。 - 服务端公钥证书最后输出到客户端本地目录的
server.crt
文件。
4、双向认证接入多个客户端
很多时候作为服务端要对接多个客户端,每个客户端都有自己的证书,Nginx
服务端需要为每一个接入的客户端渠道配置一个server
块来进行双向认证。既然是多个server
配置块,那就会涉及到对接入的客户端匹配哪个server
块来进行双向认证的问题。
首先Nginx
会根据不同的监听端口来匹配,但是这样会为每个接入的客户端渠道新开放一个端口。如何统一用一个监听端口接入所有客户端的https
请求并验证各个渠道的证书合法性,主要有以下2种方式。
4.1、SNI 多域名匹配不同证书
这里就需要使用到SNI
功能。如果编译Nginx
开启了http_ssl_module
模块,一般默认也是启用SNI
功能的,可以通过./nginx -V
命令查看。
Nginx配置多个vhost
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| 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.test.com;
ssl_certificate ../ssl/server.crt; ssl_certificate_key ../ssl/server.key; ssl_client_certificate ../clientcrt/clientA.crt; ssl_verify_client on;
ssl_session_cache shared:SSL:1m; ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on;
access_log logs/a_access.log; error_log logs/a_error.log;
location / { proxy_pass http://backend; } }
server { listen 21000 ssl; server_name b.test.com;
ssl_certificate ../ssl/server.crt; ssl_certificate_key ../ssl/server.key; ssl_client_certificate ../clientcrt/clientB.crt; ssl_verify_client on;
ssl_session_cache shared:SSL:1m; ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on;
access_log logs/b_access.log; error_log logs/b_error.log;
location / { proxy_pass http://backend; } }
|
配置的listen
端口是一样的,但每个vhost
的server_name
不同,这里其实是通过配置不同的server_name
来匹配各个不同的客户端,需要客户端请求的url
中的域名(Http请求头中的Host
字段值)与配置的server_name
一致。比如:
客户端的域名解析可以通过域名解析服务器或者可以在本地hosts
文件中配置。
需要注意的是:如果使用SNI功能,服务器签发公钥证书时,填写的域名信息Common Name
需要是泛域名,比如*.test.com
。这样客户端在验证服务器域名时才会通过。
另外,Nginx
在同一个端口下匹配多个vhost
时,如果找不到匹配的server_name
,则会使用默认的vhost
(默认第一个)来进行认证。为了防止隐式匹配带来的一些问题困扰,可以通过default_server
显式地指定一个默认的vhost
,一律返回401。
1 2 3 4 5 6 7 8 9 10 11 12 13
| server { listen 21000 ssl default_server;
ssl_certificate ../ssl/server.crt; ssl_certificate_key ../ssl/server.key;
access_log logs/default_access.log; error_log logs/default_error.log;
location / { return 401; } }
|
4.2、CA根证书统一签发客户端证书
先统一生成CA
根证书,然后由根证书派生出服务端和各个客户端的证书。
生成root根证书
1 2 3 4 5 6
| # 创建根证书私钥 openssl genrsa -out root.key 1024 # 创建根证书请求文件 openssl req -new -key root.key -out root.csr # 创建根证书 openssl x509 -req -in root.csr -out root.crt -signkey root.key -CAcreateserial -days 3650
|
生成服务端证书
1 2 3 4 5 6
| # 生成服务器端证书私钥 openssl genrsa -out server.key 1024 # 生成服务器证书请求文件 openssl req -new -key server.key -out server.csr # 生成服务器端公钥证书 openssl x509 -req -in server.csr -out server.crt -signkey server.key -CA root.crt -CAkey root.key -CAcreateserial -days 3650
|
生成客户端证书
1 2 3 4 5 6 7 8
| # 生成客户端证书私钥 openssl genrsa -out client.key 1024 # 生成客户端证书请求文件 openssl req -new -key client.key -out client.csr # 生成客户端证书 openssl x509 -req -in client.csr -out client.crt -signkey client.key -CA root.crt -CAkey root.key -CAcreateserial -days 3650 # 生客户端p12格式证书,需要输入一个密码,选一个好记的,比如123456 openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.p12
|
一定要注意的是,根证书的域名信息Common Name
这个字段和客户端证书、服务器端证书不能一样。
然后在Nginx
中ssl_client_certificate
字段配置根证书的路径,这样就可以验证所有它颁发的客户端证书。不需要再为每个客户端渠道创建一个server
配置块去认证。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| server { listen 21000 ssl; server_name localhost;
ssl_certificate ../ssl/server.crt; ssl_certificate_key ../ssl/server.key; ssl_client_certificate ../ssl/root.crt; ssl_verify_client on;
ssl_session_cache shared:SSL:1m; ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on;
access_log logs/access.log; error_log logs/error.log;
location / { proxy_pass http://192.168.0.1:10900; } }
|
参考链接
附录
keytool生成证书
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| # 生成服务端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 指定密钥库的密码(获取keystore信息所需的密码) # -keypass 指定别名条目的密码(私钥的密码) # keystore信息的查看 keytool -list -v -keystore server.jks -storepass 123456
# 从jks中导出公钥证书 keytool -export -alias servertest -keystore server.jks -storepass 123456 -file server.crt # 查看导出的证书信息 keytool -printcert -file server.crt
# 生成客户端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 # 将服务端公钥证书导入的客户端jks信任库中 keytool -import -trustcacerts -alias servertest -file server.crt -storepass 123456 -keystore client.jks # keystore信息的查看 keytool -list -v -keystore client.jks -storepass 123456
|
openssl 2 keytool
1 2 3 4 5 6 7 8
| # 生成私钥 openssl genrsa -out client.key 1024 # 生成公钥证书 openssl req -new -x509 -key client.key -out client.crt -days 3650 # 合成p12文件 openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.p12 -passout pass:123456 -name clienttest # p12文件转jks keytool -importkeystore -srcstoretype PKCS12 -srckeystore client.p12 -srcstorepass 123456 -srcalias clienttest -deststoretype JKS -destalias clienttest -deststorepass 123456 -destkeypass 123456 -destkeystore client.jks
|
keytool 2 openssl
1 2 3 4 5 6 7 8 9 10
| # 生成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 # 从jks中导出公钥证书 keytool -export -alias clienttest -keystore client.jks -storepass 123456 -file client.crt # jks转p12文件 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文件转pem格式 openssl pkcs12 -in client.p12 -out client.pem.p12 -passin pass:123456 -passout pass:123456 # 从pem格式文件单独输出私钥 openssl rsa -in client.pem.p12 -passin pass:123456 -out client.key -passout pass:123456
|