一、什么是SSL/TLS?
传输层安全性协议(Transport Layer Security,缩写作 TLS ),其前身安全套接层(Secure Sockets Layer,缩写作 SSL )是一种安全协议,目的是为互联网通信提供安全及数据完整性保障。根据传输层安全协议的规范,客户端与服务端的连接安全应该具备连接是私密的或连接是可靠的一种以上的特性。
SSL/TLS 协议通过 X.509 证书的数字文档将网站的公司实体信息绑定到加密密钥,每一个密钥对都有一个私有密钥和一个公有密钥。私有密钥是独有的,一般位于服务器上,用于解密由公有密钥加密过的信息,公有密钥是公开的,与服务器进行交互的每个人都可以持有公有密钥,用公有密钥加密的信息只能由私有密钥来解密。
SSL/TLS 协议提供以下的服务:
认证用户与服务器,确保数据发送到正确的客户端和服务器;
加密数据以防止数据在传输过程中被窃取;
维护数据的完整性,确保数据在传输过程中不被改变。
二、什么是 SAN?
SAN(Subject Alternative Name) 是 SSL 标准 x509 中定义的一个扩展,使用了 SAN 字段的 SSL 证书,可以扩展此证书支持的域名,使得一个证书可以支持多个不同域名的解析。
三、gRPC 的 SSL/TLS 加密认证的实现
该部分内容基于上一个博客gRPC-入门示例的代码,进行进一步实现!如图所示,回顾上一个博客中gRPC入门示例代码,可以看出客户端和服务端之间无任务加密认证的过程,具有不安全性。因此,接下来的内容将在原有代码基础上,分别实现SSL/TLS的单向认证、双向认证、Token认证的过程,增加安全性。
1、生成自签证证书
CA 是一个受信任的实体,它管理和发布用于公共网络中安全通信的安全证书和公钥。由该受信任的实体所签署或颁发的证书称为 CA 签名的证书。需要使用根证书CA来生成服务端、客户端证书。
使用 OpenSSL 开源工具集创建一个 CA 根证书
根证书(root certificate)是属于根证书颁发机构(CA)的公钥证书。可以通过验证 CA 的签名从而信任 CA ,任何人都可以得到 CA 的证书(含公钥),用以验证它所签发的证书(客户端、服务端)。
1、去openssl的官网下载openssl,安装完成后,在终端运行以下命令,可以看到相应的版本号。
C:\Users\xxxxx>openssl -version
OpenSSL 3.2.1 30 Jan 2024 (Library: OpenSSL 3.2.1 30 Jan 2024)
2、在原有项目工程下,新建一个目录cert目录,进入该目录下,打开终端运行以下命令:
openssl genrsa -out ca.key 2048
最终,在cert目录将会生成ca.key的文件。
3、在cert目录下,新建 ca.conf文件,并且写入以下内容:
[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = CN
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = ChongQing
localityName = Locality Name (eg, city)
localityName_default = ChongQing
organizationName = Organization Name (eg, company)
organizationName_default = JiangShui
commonName = CommonName (e.g. server FQDN or YOUR name)
commonName_max = 64
commonName_default = jiangshui
然后运行在终端运行以下命令,创建ca证书签发请求,得到ca.csr
openssl req -new -sha256 -out ca.csr -key ca.key -config ca.conf
如下图所示,运行命令后,填写内容均默认,全部按下enter即可,全部确认完毕后,cert目录下将会生成对应ca.csr文件。
4、在终端下运行以下命令,通过ca.key和ca.csr生成ca根证书,得到ca.crt
openssl x509 -req -days 3650 -in ca.csr -signkey ca.key -out ca.crt
2、单向认证
生成服务端证书
1、运行以下命令,生成服务端私钥,server.key
openssl genrsa -out server.key 2048
2、在cert目录下,新建server.conf文件,并且写入以下内容:
[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = CN
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = ChongQing
localityName = Locality Name (eg, city)
localityName_default = ChongQing
organizationName = Organization Name (eg, company)
organizationName_default = JiangShui
commonName = CommonName (e.g. server FQDN or YOUR name)
commonName_max = 64
commonName_default = jiangshui
[ req_ext ]
# 添加subjectAltName
subjectAltName = @alt_names
# 文件末尾添加. www.p-pp.cn代表允许的ServerName,自己随便写
[alt_names]
DNS.1 = www.jiangshui.cn
DNS.2 = www.libai.cn
IP = 127.0.0.1
然后运行在终端运行以下命令,创建证书签发请求,得到server.csr
openssl req -new -sha256 -out server.csr -key server.key -config server.conf
3、在终端运行以下命令,用CA证书和server.key、server.csr生成服务端证书(公钥),得到server.pem
openssl x509 -req -days 3650 -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.pem -extensions req_ext -extfile server.conf
4、完成以上步骤后,查看cert目录,是否存在以下图片中所示的文件,如果对应上,则说明生成各文件完毕。
修改服务端代码
修改后服务端grpc_server.go代码如下:
package main
import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/grpclog"
"grpc1/service"
"net"
)
func main() {
//引入TLS认证相关文件(传入公钥和私钥)
//配置 TLS认证相关文件
creds, err := credentials.NewServerTLSFromFile("./cert/server.pem", "./cert/server.key")
if err != nil {
grpclog.Fatal("添加证书失败", err)
}
//获取grpc服务端,添加TLS认证
rpcServer := grpc.NewServer(grpc.Creds(creds))
//服务端映射(注册)到结构体,调用product_grpc.pb.go中的RegisterProductServiceServer方法
service.RegisterProductServiceServer(rpcServer, service.ProductService)
// 建立tcp端口监听
listener, err := net.Listen("tcp", ":8002")
if err != nil {
grpclog.Fatal("启动监听出错", err)
}
//服务端监听tcp连接
err = rpcServer.Serve(listener)
if err != nil {
grpclog.Fatal("启动服务出错", err)
}
}
如图所示,此时,启动服务端代码,然后启动客户端调用服务端方法查询库存,出现失败。
修改客户端代码
修改后服务端grpc_client.go代码如下:
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/grpclog"
"grpc1/service"
)
func main() {
//先创建证书池,读取并解析公开证书,创建启用 TLS 的证书(公钥域名)
creds, err := credentials.NewClientTLSFromFile("./cert/server.pem", "www.jiangshui.cn")
if err != nil {
grpclog.Fatal("加载pem失败", err)
}
//建立连接并添加传输凭证
conn, err := grpc.Dial(":8002", grpc.WithTransportCredentials(creds))
if err != nil {
grpclog.Fatal("did not connect", err)
}
//关闭连接
defer conn.Close()
//调用product_grpc.pb.go中的NewProductServiceClient方法建立客户端
prodClient := service.NewProductServiceClient(conn)
request := &service.ProductRequest{
ProdId: 123,
}
//远程调用方法GetProductStock
stockResponse, err := prodClient.GetProductStock(context.Background(), request)
if err != nil {
grpclog.Fatal("查询库存失败", err)
}
fmt.Println("查询成功", stockResponse.ProdStock)
}
如图所示,此时,启动服务端代码,然后启动客户端调用服务端方法查询库存,查询成功,实现了一个单向认证。
3、双向认证
双向认证需要在单向认证的基础上,继续生成客户端证书,然后对客户端和服务端代码进一步整改,才能完成双向认证。本人在实施双向认证的时候,也遇到了许多问题。当我利用OpenSSL 开源工具集生成自签证证书,然后按步骤生成完客户端和服务端公私钥,最后修改客户端和服务端双向认证代码并且运行测试后,始终出现以下问题:
FATAL: 查询库存出错rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: tls: failed to verify certificate:
x509: certificate signed by unknown authority (possibly because of \"x509: invalid signature: parent certificate cannot sign this kind of certificate\" while trying to verify candid
ate authority certificate \"jiangshui.com\")"
该错误在我多次尝试解决该bug,甚至直接搬用别人的代码直接使用也未能解决!其中包括参考了许多博客的教程以及码神之路的教程,都未能解决相关问题!
参考博客1:gRPC教程 — TLS单向认证、双向认证、Token认证、拦截器
参考博客2:【gRPC】自签CA、服务端和客户端双向认证
参考博客3:码神之路教程
大家可以使用以上博客教程中的其中方法先进行尝试,看看如果能够测试成功,自然皆大欢喜!如果测试失败,出现与我相同错误!接下来,我将提供一种与以上博客不同的方法来实现双向认证过程!与上述博客中描述的方法不同,此处我不在利用OpenSSL 开源工具集生成自签证书以及服务端、客户端的公私钥。此处,我将直接使用go语言代码和相关库实现自签证书以及服务端、客户端的公私钥生成。
用go代码生成自签证书和客户端、服务端公私钥
在cert文件目录新建一个cert.go文件,并且在文件中写入以下代码:
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"os"
"time"
)
func main() {
// 生成CA证书
caCert, caPrivateKey, err := generateCACertificate()
if err != nil {
panic(err)
}
// 生成服务端证书
err = generateCert("server.crt", "server.key", caCert, caPrivateKey, "localhost", true)
if err != nil {
panic(err)
}
// 生成客户端证书
err = generateCert("client.crt", "client.key", caCert, caPrivateKey, "Client", false)
if err != nil {
panic(err)
}
}
func generateCACertificate() (*x509.Certificate, *ecdsa.PrivateKey, error) {
caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, err
}
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return nil, nil, err
}
caTemplate := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Example CA"},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0), // 10 years
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
IsCA: true,
}
caCertBytes, err := x509.CreateCertificate(rand.Reader, &caTemplate, &caTemplate, &caPrivateKey.PublicKey, caPrivateKey)
if err != nil {
return nil, nil, err
}
caCert, err := x509.ParseCertificate(caCertBytes)
if err != nil {
return nil, nil, err
}
// Save the CA certificate
saveCert("ca.crt", caCertBytes)
// Save the CA private key
savePrivateKey("ca.key", caPrivateKey)
return caCert, caPrivateKey, nil
}
func generateCert(certFilename, keyFilename string, caCert *x509.Certificate, caPrivateKey *ecdsa.PrivateKey, commonName string, isServer bool) error {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return err
}
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return err
}
certTemplate := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: commonName,
Organization: []string{"Example Org"},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0), // 1 year
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
}
// 为证书添加SANs字段
certTemplate.DNSNames = append(certTemplate.DNSNames, "localhost")
certTemplate.IPAddresses = append(certTemplate.IPAddresses, net.ParseIP("127.0.0.1"))
if isServer {
certTemplate.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
}
certBytes, err := x509.CreateCertificate(rand.Reader, &certTemplate, caCert, &privateKey.PublicKey, caPrivateKey)
if err != nil {
return err
}
saveCert(certFilename, certBytes)
savePrivateKey(keyFilename, privateKey)
return nil
}
func saveCert(filename string, certBytes []byte) {
certPEMFile, err := os.Create(filename)
if err != nil {
panic(err)
}
defer certPEMFile.Close()
pem.Encode(certPEMFile, &pem.Block{Type: "CERTIFICATE", Bytes: certBytes})
}
func savePrivateKey(filename string, privateKey *ecdsa.PrivateKey) {
keyFile, err := os.Create(filename)
if err != nil {
panic(err)
}
defer keyFile.Close()
privBytes, err := x509.MarshalECPrivateKey(privateKey)
if err != nil {
panic(err)
}
pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: privBytes})
}
代码中的caTemplate、certTemplate部分类似于OpenSSL 开源工具集生成证书,相关部分可以根据自己的需求进行修改!此外,证书保存的路径根据自己的具体情况,进行修改!或者生成完证书后,自己将证书文件移动到对应的目录下。
此处图片中的内容则是类似于OpenSSL 开源工具集生成SAN证书部分,添加SANs字段,DNSName可以根据自身需求填写(添加多个)。
修改服务端代码
server_grpc.go
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"google.golang.org/grpc/grpclog"
"grpc2/service"
"io/ioutil"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
func main() {
// 证书认证-双向认证
// 从证书相关文件中读取和解析信息,得到证书公钥、密钥对
cert, err := tls.LoadX509KeyPair("./cert/server.crt", "./cert/server.key")
if err != nil {
grpclog.Fatalf("failed to load server certificate: %v", err)
}
caCert, err := ioutil.ReadFile("./cert/ca.crt")
if err != nil {
grpclog.Fatalf("failed to read ca certificate: %v", err)
}
// 创建一个新的、空的证书池
caCertPool := x509.NewCertPool()
// 附加来自 CA 的证书
if !caCertPool.AppendCertsFromPEM(caCert) {
grpclog.Fatalf("failed to append ca certs")
}
// 构建基于 TLS 的 TransportCredentials 选项
config := &tls.Config{
Certificates: []tls.Certificate{cert}, // 服务器的证书和私钥
ClientAuth: tls.RequireAndVerifyClientCert, //启用并要求客户端证书验证
ClientCAs: caCertPool, //用于验证客户端证书的 CA 证书
}
listener, err := net.Listen("tcp", ":50051")
if err != nil {
grpclog.Fatalf("failed to listen: %v", err)
}
rpcServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(config)))
// 在此注册您的 gRPC 服务处理程序
service.RegisterProductServiceServer(rpcServer, service.ProductService)
if err != nil {
grpclog.Fatal("启动监听出错", err)
}
err = rpcServer.Serve(listener)
if err != nil {
grpclog.Fatal("启动服务出错", err)
}
fmt.Println("启动grpc服务端成功")
}
修改客户端代码
client_grpc.go
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/grpclog"
"grpc2/service"
"io/ioutil"
)
func main() {
// 证书认证-双向认证
// 从证书相关文件中读取和解析信息,得到证书公钥、密钥对
clientCert, err := tls.LoadX509KeyPair("./cert/client.crt", "./cert/client.key")
if err != nil {
grpclog.Fatalf("failed to load client certificate: %v", err)
}
// 创建一个新的、空的证书池
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile("./cert/ca.crt")
if err != nil {
grpclog.Fatalf("could not read ca certificate: %v", err)
}
// 附加来自 CA 的证书
if ok := certPool.AppendCertsFromPEM(ca); !ok {
grpclog.Fatalf("failed to append ca certs")
}
// 构建基于 TLS 的 TransportCredentials 选项
config := &tls.Config{
Certificates: []tls.Certificate{clientCert}, // 客户端的证书和私钥
ServerName: "localhost", //验证DNSName
RootCAs: certPool, // CA证书用于验证服务器证书
}
conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(credentials.NewTLS(config)))
if err != nil {
grpclog.Fatalf("failed to dial server: %v", err)
}
defer conn.Close()
// 现在您可以使用 `conn` 创建客户端并进行 gRPC 调用
prodClient := service.NewProductServiceClient(conn)
request := &service.ProductRequest{
ProdId: 123,
}
stockResponse, err := prodClient.GetProductStock(context.Background(), request)
if err != nil {
grpclog.Fatal("查询库存出错", err)
}
fmt.Println("查询成功", stockResponse.ProdStock)
}
客户端和服务端代码修改完成后,启动服务端,运行客户端进行测试,此时不再出现之前所遇到的错误。如下图所示,成功调用到获取库存的方法,查询库存成功!
4、token认证
gRPC为每个gRPC方法调用提供了Token认证支持,可以基于用户传入的Token判断用户是否登陆、以及权限等。
服务端添加用户名密码的校验
server_grpc.go
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"grpc2/service"
"io/ioutil"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
func main() {
// 证书认证-双向认证
// 从证书相关文件中读取和解析信息,得到证书公钥、密钥对
cert, err := tls.LoadX509KeyPair("./cert/server.crt", "./cert/server.key")
if err != nil {
grpclog.Fatalf("failed to load server certificate: %v", err)
}
caCert, err := ioutil.ReadFile("./cert/ca.crt")
if err != nil {
grpclog.Fatalf("failed to read ca certificate: %v", err)
}
// 创建一个新的、空的证书池
caCertPool := x509.NewCertPool()
// 附加来自 CA 的证书
if !caCertPool.AppendCertsFromPEM(caCert) {
grpclog.Fatalf("failed to append ca certs")
}
// 构建基于 TLS 的 TransportCredentials 选项
config := &tls.Config{
Certificates: []tls.Certificate{cert}, // 服务器的证书和私钥
ClientAuth: tls.RequireAndVerifyClientCert, //启用并要求客户端证书验证
ClientCAs: caCertPool, //用于验证客户端证书的 CA 证书
}
listener, err := net.Listen("tcp", ":50051")
if err != nil {
grpclog.Fatalf("failed to listen: %v", err)
}
//实现token认证,需要合法的用户名和密码
//实现一个拦截器
var authInterceptor grpc.UnaryServerInterceptor
authInterceptor = func(
ctx context.Context,
req any,
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (resp any, err error) {
//拦截普通方法请求,验证Token
err = Auth(ctx)
if err != nil {
return
}
return handler(ctx, req)
}
//添加在服务端拦截器
rpcServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(config)), grpc.UnaryInterceptor(authInterceptor))
// 在此注册您的 gRPC 服务处理程序
service.RegisterProductServiceServer(rpcServer, service.ProductService)
if err != nil {
grpclog.Fatal("启动监听出错", err)
}
err = rpcServer.Serve(listener)
if err != nil {
grpclog.Fatal("启动服务出错", err)
}
fmt.Println("启动grpc服务端成功")
}
func Auth(ctx context.Context) error {
//拿到传输的用户名和密码
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return fmt.Errorf("missing credentials")
}
var user string
var password string
if val, ok := md["user"]; ok {
user = val[0]
}
if val, ok := md["password"]; ok {
password = val[0]
}
if user != "admin" || password != "admin" {
return status.Errorf(codes.Unauthenticated, "token不合法")
}
return nil
}
此时,启动客户端远程调用方法失败,出现token不合法的提示,如下图所示
修改客户端代码,以适应token认证
客户段需要携带token进行传入,使得服务端能进行正确验证,因此需要修改客户端代码,其中主要修改以下代码:
//客户端携带token传入
conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(credentials.NewTLS(config)), grpc.WithPerRPCCredentials(token))
通过grpc.WithPerRPCCredentials(token)函数携带token。通过追踪该函数,可得知该函数接收的参数为一个接口,因此客户端传入的token,需要定义一个结构体将数据传入,并且将该结构体需要实现grpc.PerRPCCredentials接口。
type PerRPCCredentials interface {
// 返回需要认证的必要信息
GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
// 是否使用安全链接(TLS)
RequireTransportSecurity() bool
}
实现grpc.PerRPCCredentials接口
在原来项目的基础上,新建auth文件目录,并且新建auth.go文件,写入以下代码实现相关接口:
package auth
import "context"
type Authentication struct {
User string
Password string
}
func (a *Authentication) GetRequestMetadata(context.Context, ...string) (
map[string]string, error,
) {
return map[string]string{"user": a.User, "password": a.Password}, nil
}
func (a *Authentication) RequireTransportSecurity() bool {
return false
}
修改客户端代码
client_grpc.go
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/grpclog"
"grpc2/grpc_client/auth"
"grpc2/service"
"io/ioutil"
)
func main() {
// 证书认证-双向认证
// 从证书相关文件中读取和解析信息,得到证书公钥、密钥对
clientCert, err := tls.LoadX509KeyPair("./cert/client.crt", "./cert/client.key")
if err != nil {
grpclog.Fatalf("failed to load client certificate: %v", err)
}
// 创建一个新的、空的证书池
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile("./cert/ca.crt")
if err != nil {
grpclog.Fatalf("could not read ca certificate: %v", err)
}
// 附加来自 CA 的证书
if ok := certPool.AppendCertsFromPEM(ca); !ok {
grpclog.Fatalf("failed to append ca certs")
}
// 构建基于 TLS 的 TransportCredentials 选项
config := &tls.Config{
Certificates: []tls.Certificate{clientCert}, // 客户端的证书和私钥
ServerName: "localhost", //验证DNSName
RootCAs: certPool, // CA证书用于验证服务器证书
}
token := &auth.Authentication{
User: "admin",
Password: "admin",
}
//客户端携带token传入
conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(credentials.NewTLS(config)), grpc.WithPerRPCCredentials(token))
if err != nil {
grpclog.Fatalf("failed to dial server: %v", err)
}
defer conn.Close()
// 现在您可以使用 `conn` 创建客户端并进行 gRPC 调用
prodClient := service.NewProductServiceClient(conn)
request := &service.ProductRequest{
ProdId: 123,
}
stockResponse, err := prodClient.GetProductStock(context.Background(), request)
if err != nil {
grpclog.Fatal("查询库存出错", err)
}
fmt.Println("查询成功", stockResponse.ProdStock)
}
最后,启动服务端,运行客户端代码进行测试。当携带的token与服务端要求的不一致时,出现下图所示的token不合法,查询失败。
文章来源:https://www.toymoban.com/news/detail-845754.html
当携带的token与服务端要求的一致时,出现下图所示查询成功!文章来源地址https://www.toymoban.com/news/detail-845754.html
到了这里,关于gRPC — SSL/TLS单向认证、双向认证、Token认证的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!