方法在最后一小节,前面都是用到的知识的总结,了解的可以跳过。
1. 对称加密和非对称加密
对称加密:加密和解密都用同一个密码
非对称加密:公钥对所有人公开,发送者加密用公钥,接收者唯一掌握私钥,对公钥加密内容的解密用且只能用私钥
非对称加密缺点:加密速度比对称慢
非对称加密优点:公钥本身就是公开给别人的,所以不用担心被窃取,私钥永远在自己手里,只有自己能解密消息
2. https和http
https在http的基础上,对传输内容进行了加密。
我们都知道http建立连接的三次握手和四次挥手过程。如果使用的是https,在三次握手过后,需要进行SSL握手,然后客户端和服务端通信过程中使用SSL(新版本叫TLS了)的互相加解密。通信完之后,在四次挥手之前,先关闭SSL连接。
从这里能看出,https比http要慢(多了SSL握手、挥手,就多了好几次TCP通信,建立连接之后每次通信还要加解密,http/1.1开始支持长连接,以及http/2支持多路复用,都可以减少建立连接的次数,也就减少了多次http请求下每次http请求的平均耗时)。
https相比http还可以防篡改。接收到消息之后计算出数字签名,对比收到的数字签名,就知道消息体是否被篡改了。(想了解数字签名可以参考阿里云视频监控产品OpenAPI的签名机制)
3. https单向认证和双向认证
图来自这里
从上面对比可以看到,https使用的非对称加密是用在客户端和服务端交互SSL信息的过程中,SSL握手完成后,客户端和服务端通信使用的还是对称加密,对称加密的密钥在本次连接随机生成,其他的连接以及本次连接关闭后都不能使用。
双向认证和单向认证的区别就在于,客户端需要向服务端发送自己的证书,服务端需要校验客户端的证书,以及服务端选择加密方案通知客户端时也是加密的。
4. CA证书和自制证书
CA(Certificate Authority)是负责管理和签发证书的第三方权威机构,常见的有Symantec、GeoTrust、Comodo等等,他们是所有行业和公众都信任的、认可的,并负责审核向他们申请证书的网站的安全性。
CA证书,就是CA颁发的证书,可用于验证网站是否可信(针对HTTPS)、验证某文件是否可信(是否被篡改)等,也可以用一个证书来证明另一个证书是真实可信,最顶级的证书称为根证书。除了根证书(自己证明自己是可靠),其它证书都要依靠上一级的证书,来证明自己。
我们用的操作系统都预置了很多可信任的根证书,SSL握手时服务器会把它的服务器证书发给浏览器。例如CSDN的服务器证书是GeoTrust颁发的,本地的GeoTrust根证书可以证明CSDN的服务器证书是真的,值得信任,于是我们访问CSDN时浏览器就建立了和CSDN服务器的https连接。
我们也可以自己做CA根证书,我们自己的机器或者访问我们服务的客户的机器,都安装上该CA根证书。12306以前就是这样干的(SRCA就是12306的根证书,现在已经换成DigiCert的证书了)。
关于CA证书更多的解释,以及各种证书文件的区别,参考这里。
自制CA证书可以用OpenSSL命令行工具,linux基本都自带,也可以使用GUI工具,参考这里。
5. 利用chrome和nginx实践一下单向认证和双向认证
先假定我们已经做好了证书文件,包括根证书ca.pem、服务端证书server.pem、服务端私钥server.key、客户端证书client.pem、客户端私钥client.key,可以来这里下载我做好的,可以用于你自己测试,提取码:yd11。
nginx加上配置文件:
server {
listen 443 ssl; # ssl代表该端口监听启用https
server_name localhost;
ssl_certificate /data/sslKey/server.pem; # 证书
ssl_certificate_key /data/sslKey/server.key; # 私钥
ssl_client_certificate /data/sslKey/ca.pem; # 根证书,用于验证各个下级证书
ssl_verify_client on; # 校验客户端证书开启
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
其中:
ssl_certificate /data/sslKey/server.pem; # server证书公钥
ssl_certificate_key /data/sslKey/server.key; # server私钥
很常见。
通常我们需要让自己的网站变成https访问时都是这么做。使用nginx对外暴露https请求接口,nginx到后端的内网服务仍然是http,改动小,效率高。
只有这两项配置就是单向认证,即客户端需要校验服务端的证书来确信服务端是安全网站。这时候访问https://localhost:443
会提示不安全的网站(以下所有截图都是chrome浏览器的交互,其他浏览器可能不是这样,自行处理),因为服务端提供的证书是我们自制的,操作系统预存的证书无法识别该证书。
点击继续访问代表我们手动告诉浏览器信任这个网站,就可以继续访问了。
再加上:
ssl_client_certificate /data/sslKey/ca.pem; # 根证书,用于验证各个下级证书
ssl_verify_client on; # 校验客户端证书开启
这两项配置就是双向认证。
服务端需要验证客户端的证书,我们直接访问就会得到报错400 No Required SSL certificate was sent
,需要在chrome中导入自制的client.pem证书,导入方法参考这里。
需要注意的是windows导入证书的格式不是pem和crt,需要转换一下才能导入,OpenSSL命令行工具可以转换,上面的GUI工具也可以在导出时选择指定格式。
再次访问chrome会弹窗提示,
选择刚才导入的证书就能正常访问了。
6. 利用自制CA证书双向认证实现安全访问
以go代码示例,仍然使用上面的证书文件,加载证书文件的代码:
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"log"
"google.golang.org/grpc/credentials"
)
// GetServerCredentials 服务端证书
func GetServerCredentials() credentials.TransportCredentials {
cert, err := tls.LoadX509KeyPair("cert/server.pem", "cert/server.key")
if err != nil {
log.Fatalf("加载服务端证书失败, err: %v\n", err)
}
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile("cert/ca.pem")
if err != nil {
log.Fatalf("读取公钥文件失败: %v\n", err)
}
certPool.AppendCertsFromPEM(ca)
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: certPool,
})
return creds
}
// GetClientCredentials 客户端证书
func GetClientCredentials() credentials.TransportCredentials {
cert, err := tls.LoadX509KeyPair("cert/client.pem", "cert/client.key")
if err != nil {
log.Fatalf("加载客户端证书失败, err: %v\n", err)
}
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile("cert/ca.pem")
if err != nil {
log.Fatalf("读取公钥文件失败: %v\n", err)
}
certPool.AppendCertsFromPEM(ca)
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
ServerName: "localhost",
RootCAs: certPool,
})
return creds
}
服务端加上:
opts := []grpc.ServerOption{
grpc.Creds(utils.GetServerCredentials()),
}
客户端加上:
grpc.Dial(
"localhost:8080",
grpc.WithTransportCredentials(cert.GetClientCredentials()),
如果服务端使用了证书,客户端没有使用证书,在grpc.Dial()
时连接可以建立成功,但是访问时会报错:
rpc error: code = Unavailable desc = connection closed
并且可以一直访问一直报错。
这里涉及到gRPC连接机制的问题。调用Dial或者DialContext函数创建连接时,默认只是返回ClientConn结构体指针,同时会启动一个Goroutine异步的去建立连接,连接失败会一直重试,这个机制可以避免服务器因为连接空闲时间过长关闭连接、服务器重启等造成的客户端连接失效问题,可以完美的解决连接的超时与保活问题。
如果想要等连接建立完再返回(起到创建连接时就检测连接是否可用的目的),可以指定grpc.WithBlock()。连接不上就会报错:
context deadline exceeded
如果服务端没有使用证书,客户端使用了,也是可以成功建立连接,但是访问时报错:
connection error: desc = "transport: authentication handshake failed: tls: first record does not look like a TLS handshake"
因为客户端收不到服务端的TLS握手信息(服务端不使用证书,根本就不知道要TLS握手)。