文章目录

  • 简介
  • 生成自签证证书
    • 服务端
    • 客户端
  • 单向认证
  • 双向认证
  • 基于 Token 的单向认证
  • 基于 Token 的双向认证

简介


传输层安全性协议(Transport Layer Security,缩写作 TLS ),其前身安全套接层(Secure Sockets Layer,缩写作 SSL )是一种安全协议,目的是为互联网通信提供安全及数据完整性保障。根据传输层安全协议的规范,客户端与服务端的连接安全应该具备连接是私密的或连接是可靠的一种以上的特性。

SAN(Subject Alternative Name) 是 SSL 标准 x509 中定义的一个扩展,使用了 SAN 字段的 SSL 证书,可以扩展此证书支持的域名,使得一个证书可以支持多个不同域名的解析。

SSL/TLS 协议通过 X.509 证书的数字文档将网站的公司实体信息绑定到加密密钥,每一个密钥对(key pairs)都有一个私有密钥和一个公有密钥。私有密钥是独有的,一般位于服务器上,用于解密由公有密钥加密过的信息,公有密钥是公开的,与服务器进行交互的每个人都可以持有公有密钥,用公有密钥加密的信息只能由私有密钥来解密,具体过程如下图所示:

SSL/TLS 协议提供以下的服务:

  • 认证用户与服务器,确保数据发送到正确的客户端和服务器;

  • 加密数据以防止数据在传输过程中被窃取;

  • 维护数据的完整性,确保数据在传输过程中不被改变。

SSL/TLS 协议提供的安全通道有如下的特性:

  • 机密性:SSL 协议使用密钥加密通信数据;

  • 可靠性:服务器与客户端都会被认证,客户端的认证是可选的;

  • 完整性:SSL 协议会对传输的数据进行完整性检查。


生成自签证证书


CA 是一个受信任的实体,它管理和发布用于公共网络中安全通信的安全证书和公钥。由该受信任的实体所签署或颁发的证书称为 CA 签名的证书,创建的证书的具体步骤如下所示:

(1)使用 OpenSSL (适用于 TLS 和安全套接字层协议)开源工具集创建一个 CA 私钥(根证书)

openssl genrsa -des3 -out ca.key 2048

执行以上命令后,终端输出如下所示的内容:

Generating RSA private key, 2048 bit long modulus (2 primes)
................................+++++
...............+++++
e is 65537 (0x010001)
// 输入任意密码(123456)
Enter pass phrase for ca.key:
Verifying - Enter pass phrase for ca.key:

根证书(root certificate)是属于根证书颁发机构(CA)的公钥证书。可以通过验证 CA 的签名从而信任 CA ,任何人都可以得到 CA 的证书(含公钥),用以验证它所签发的证书(客户端、服务端)。

(2)创建证书请求

openssl req -new -key ca.key -out ca.csr

执行以上命令后,终端输出如下所示的内容:

Enter pass phrase for ca.key:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:cn
State or Province Name (full name) [Some-State]:cq
Locality Name (eg, city) []:cq
Organization Name (eg, company) [Internet Widgits Pty Ltd]:mszlu
Organizational Unit Name (eg, section) []:mszlu
Common Name (e.g. server FQDN or YOUR name) []:mszlu.com
Email Address []:Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

(3)生成 ca.crt 文件

openssl x509 -req -days 365 -in ca.csr -signkey ca.key  -out ca.crt

执行以上命令后,终端输出如下所示的内容:

Signature ok
subject=C = cn, ST = cq, L = cq, O = mszlu, OU = mszlu, CN = mszlu.com
Getting Private key
Enter pass phrase for ca.key:

打开 /etc/ssl/ 目录下的 openssl.conf 文件,编辑修改如下内容:

# 找到并取消 copy_extentions = copy 前到注释
copy_extentions = copy# 找到并取消 req_extentions = v3_req 前的注释
req_extentions = v3_req # 找到 [ v3_req ] ,添加 subjectAltName = @alt_names 信息
subjectAltName = @alt_names # 添加 [ alt_names ] 和以下形式的标签信息
[ alt_names ]DNS.1 = *.mszlu.comDNS.2 = *.cqupthao.com # 指定任意域名

服务端

(4)生成证书私钥 server.key

openssl genpkey -algorithm RSA -out server.key

执行以上命令后,终端输出如下所示的内容:

........+++++
.........+++++

(5)通过 server.key 生成证书请求文件 server.csr

openssl req -new -nodes -key server.key -out server.csr -days 3650 -config /etc/ssl/openssl.cnf -extensions v3_req

执行以上命令后,终端输出如下所示的内容:

Ignoring -days; not generating a certificate
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:cn
State or Province Name (full name) [Some-State]:cq
Locality Name (eg, city) []:cq
Organization Name (eg, company) [Internet Widgits Pty Ltd]:mszlu
Organizational Unit Name (eg, section) []:mszlu
Common Name (e.g. server FQDN or YOUR name) []:mszlu.com
Email Address []:Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

(6)生成 SAN 证书

openssl x509 -req -days 365 -in server.csr -out server.pem -CA ca.crt -CAkey ca.key -CAcreateserial -extfile /etc/ssl/openssl.cnf -extensions v3_req

执行以上命令后,终端输出如下所示的内容:

Signature ok
subject=C = cn, ST = cq, L = cq, O = mszlu, OU = mszlu, CN = mszlu.com
Getting CA Private Key
Enter pass phrase for ca.key:
  • key :服务器上的私钥文件,用于对发送给客户端数据的加密以及对从客户端接收到数据的解密。

  • csr :证书签名请求文件,用于提交给证书颁发机构(CA)对证书签名。

  • crt :由证书颁发机构(CA)签名后的证书或是开发者自签名的证书,包含证书持有人的信息,持有人的公钥以及签署者的签名等信息。

  • pem :基于 Base64 编码的证书格式,扩展名包括 PEM、CRT 和 CER 。


客户端

(7)生成证书私钥 client.key

openssl genpkey -algorithm RSA -out client.key

执行以上命令后,终端输出如下所示的内容:

.....+++++
...........+++++

(8)通过 client.key 生成证书请求文件 client.csr

openssl req -new -nodes -key client.key -out client.csr -days 3650 -config /etc/ssl/openssl.cnf -extensions v3_req

执行以上命令后,终端输出如下所示的内容:

Ignoring -days; not generating a certificate
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:cn
State or Province Name (full name) [Some-State]:cq
Locality Name (eg, city) []:cq
Organization Name (eg, company) [Internet Widgits Pty Ltd]:mszlu
Organizational Unit Name (eg, section) []:mszlu
Common Name (e.g. server FQDN or YOUR name) []:mszlu.com
Email Address []:Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

(9)生成 SAN 证书

openssl x509 -req -days 365 -in client.csr -out client.pem -CA ca.crt -CAkey ca.key -CAcreateserial -extfile /etc/ssl/openssl.cnf -extensions v3_req

执行以上命令后,终端输出如下所示的内容:

Signature ok
subject=C = cn, ST = cq, L = cq, O = mszlu, OU = mszlu, CN = mszlu.com
Getting CA Private Key
Enter pass phrase for ca.key:

最终生成的文件结构如下所示:

cert
├── ca.crt
├── ca.csr
├── ca.key
├── ca.srl
├── client.csr
├── client.key
├── client.pem
├── server.csr
├── server.key
└── server.pem

单向认证


在单向安全连接中,只有客户端会检验服务器端,确保它所接收的数据来自预期的服务器。
单向认证(基于 TLS 证书认证)的过程如下图所示:

  • Go gRPC 程序实现单向认证

(1)在任意目录下,分别创建 serverclient 目录存放服务端和客户端文件,proto 目录用于编写 IDL 的 tls.proto 文件,cert 目录存放证书文件,具体的目录结构如下所示:

TLSSingleAuth
├── client
│   ├── cert
│   └── proto
│       └── tls.proto
└── server├── cert└── proto└── tls.proto

tls.proto 文件的具体内容如下所示:

syntax = "proto3"; // 版本声明,使用 Protocol Buffers v3 版本option go_package = "../proto";  // 指定生成的 Go 代码在项目中的导入路径package pb; // 包名// 定义服务
service Greeter {// SayHello 方法rpc SayHello (HelloRequest) returns (HelloResponse) {}
}// 请求消息
message HelloRequest {string name = 1;
}// 响应消息
message HelloResponse {string reply = 1;
}

(2)根据生成的 CA 证书,移动以下相应的证书到服务端和客户端目录下,进入 proto 目录生成 gRPC 源代码程序,在 proto 目录下执行以下的命令:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto

正确生成后的目录结构如下所示:

TLSSingleAuth
├── client
│   ├── cert
│   └── server.pem
│   └── proto
│       ├── tls_grpc.pb.go
│       ├── tls.pb.go
│       └── tls.proto
└── server├── cert│   ├── server.key│   └── server.pem└── proto├── tls_grpc.pb.go├── tls.pb.go└── tls.proto

(3)在 server 目录下初始化项目( go mod init server ),编写 Server 端程序重写定义的方法,该程序的具体代码如下:

package mainimport ("context""fmt"pb "server/proto""net""log""google.golang.org/grpc/credentials""google.golang.org/grpc"
)// hello servertype server struct {pb.UnimplementedGreeterServer
}func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {return &pb.HelloResponse{Reply: "Hello " + in.Name}, nil
}func main() {// 进行加载  key pair , 添加证书到 copy 程序中,为所有传入的连接启用 TLScreds, err := credentials.NewServerTLSFromFile("cert/server.pem", "cert/server.key")if err != nil {log.Println("加载证书失败!\n", err)}// 通过传入 TLS 服务器凭证来创建新的 gRPC 服务器实例s := grpc.NewServer(grpc.Creds(creds))pb.RegisterGreeterServer(s, &server{}) // 在 gRPC 服务端注册服务lis, err := net.Listen("tcp", "127.0.0.1:8972")if err != nil{log.Fatalf("net.Listen err: %v", err)}// 启动服务err = s.Serve(lis)if err != nil {fmt.Printf("failed to serve: %v", err)return}
}

程序说明:

  • credentials.NewServerTLSFromFile() :根据服务端输入的证书文件和密钥构造 TLS 凭证。
func NewServerTLSFromFile(certFile, keyFile string) (TransportCredentials, error) {cert, err := tls.LoadX509KeyPair(certFile, keyFile)if err != nil {return nil, err}return NewTLS(&tls.Config{Certificates: []tls.Certificate{cert}}), nil
}
  • grpc.Creds() :返回一个 ServerOption ,用于设置服务器连接的凭据,grpc.NewServer(opt …ServerOption) 为 gRPC Server 设置连接选项。
func Creds(c credentials.TransportCredentials) ServerOption {return func(o *options) {o.creds = c}
}

经过以上两个简单步骤,gRPC Server 建立了需证书认证的服务。

(4)在 client 目录下初始化项目( go mod init client ),编写 Client 端程序调用服务,该程序的具体代码如下:

package mainimport ("context""flag""log""time"pb "client/proto""google.golang.org/grpc/credentials""google.golang.org/grpc"
)// hello_clientconst (defaultName = "cqupthao!"
)var (addr = flag.String("addr", "127.0.0.1:8972", "the address to connect to")name = flag.String("name", defaultName, "Name to greet")
)func main() {flag.Parse()// 和server 端一样,先创建证书池,读取并解析公开证书,创建启用 TLS 的证书creds , err := credentials.NewClientTLSFromFile("cert/server.pem","*.mszlu.com")if err!= nil{log.Println("加载 pem 失败!\n",err)}// 建立连接并添加传输凭证conn, err := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(creds))if err != nil {log.Fatalf("grpc.Dial err: %v", err)}defer conn.Close()c := pb.NewGreeterClient(conn)// 执行 RPC 调用并打印收到的响应数据ctx, cancel := context.WithTimeout(context.Background(), time.Second)defer cancel()r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})if err != nil {log.Fatalf("调用服务失败: %v", err)}log.Printf("调用服务成功: %s", r.GetReply())
}

程序说明:

  • credentials.NewClientTLSFromFile() :根据客户端输入的证书文件和密钥构造 TLS 凭证,serverNameOverride 为服务名称。
func NewClientTLSFromFile(certFile, serverNameOverride string) (TransportCredentials, error) {b, err := ioutil.ReadFile(certFile)if err != nil {return nil, err}cp := x509.NewCertPool()if !cp.AppendCertsFromPEM(b) {return nil, fmt.Errorf("credentials: failed to append certificates")}return NewTLS(&tls.Config{ServerName: serverNameOverride, RootCAs: cp}), nil
}
  • grpc.WithTransportCredentials():返回一个配置连接的 DialOption 选项,用于 grpc.Dial(target string, opts …DialOption) 设置连接选项。
func WithTransportCredentials(creds credentials.TransportCredentials) DialOption {return newFuncDialOption(func(o *dialOptions) {o.copts.TransportCredentials = creds})
}

Client 端是基于 Server 端的证书和服务名称来建立请求的,需要将Server 的证书。

执行 Server 端和 Client 端的程序,输出如下的结果:

2023/02/16 19:30:13 调用服务成功: Hello cqupthao!

双向认证


客户端和服务端采用 mTLS 连接可以控制连接服务器端的客户端,该方式会将服务器配置为仅接受来自一组范围有限,已经验证的客户端的连接,双方彼此共享公共证书并校验对方的身份。

  • 双向认证(基于 CA 的 TLS 证书认证)的过程如下图所示:

  • Go gRPC 程序实现双向认证

(1)在任意目录下,分别创建 serverclient 目录存放服务端和客户端文件,proto 目录用于编写 IDL 的 tls.proto 文件,cert 目录存放证书文件,具体的目录结构如下所示:

TLSDoubleAuth
├── client
│   ├── cert
│   └── proto
│       └── tls.proto
└── server├── cert└── proto└── tls.proto

tls.proto 文件的具体内容如下所示:

syntax = "proto3"; // 版本声明,使用 Protocol Buffers v3 版本option go_package = "../proto";  // 指定生成的 Go 代码在项目中的导入路径package pb; // 包名// 定义服务
service Greeter {// SayHello 方法rpc SayHello (HelloRequest) returns (HelloResponse) {}
}// 请求消息
message HelloRequest {string name = 1;
}// 响应消息
message HelloResponse {string reply = 1;
}

(2)根据生成的 CA 证书,移动以下相应的证书到服务端和客户端目录下,进入 proto 目录生成 gRPC 源代码程序,在 proto 目录下执行以下的命令:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto

正确生成后的目录结构如下所示:

TLSDoubleAuth
├── client
│   ├── cert
│   │   ├── ca.crt
│   │   ├── client.key
│   │   └── client.pem
│   └── proto
│       ├── tls_grpc.pb.go
│       ├── tls.pb.go
│       └── tls.proto
└── server├── cert│   ├── ca.crt│   ├── server.key│   └── server.pem└── proto├── tls_grpc.pb.go├── tls.pb.go└── tls.proto

(3)在 server 目录下初始化项目( go mod init server ),编写 Server 端程序重写定义的方法,该程序的具体代码如下:

package mainimport ("context""fmt"pb "server/proto""net""log""crypto/tls""crypto/x509""io/ioutil""google.golang.org/grpc/credentials""google.golang.org/grpc"
)// hello servertype server struct {pb.UnimplementedGreeterServer
}func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {return &pb.HelloResponse{Reply: "Hello " + in.Name}, nil
}func main() {// 进行加载 key pair,读取和解析公钥-私钥对并创建 X.509 密钥对cert, err := tls.LoadX509KeyPair("cert/server.pem", "cert/server.key")if err != nil {log.Println("加载 x509 证书失败!\n", err)}// 通过 CA 创建证书池certPool := x509.NewCertPool()// 向证书池中加入证书cafileBytes, err := ioutil.ReadFile("cert/ca.crt")if err != nil {log.Println("读取 ca.crt 证书失败!\n", err)}// 加载客户端证书//certPool.AddCert()// 加载证书从 pem 文件里面,将来自 CA 的客户端证书附加到证书池中certPool.AppendCertsFromPEM(cafileBytes)// 创建 credentials 对象,通过创建 TLS 凭证为所以传入的连接启用 TLScreds := credentials.NewTLS(&tls.Config{Certificates: []tls.Certificate{cert},        // 服务端证书ClientAuth:   tls.RequireAndVerifyClientCert, // 需要并且验证客户端证书ClientCAs:    certPool,                       // 客户端证书池})s := grpc.NewServer(grpc.Creds(creds))pb.RegisterGreeterServer(s, &server{}) // 在 gRPC 服务端注册服务lis, err := net.Listen("tcp", "127.0.0.1:8972")if err != nil{log.Fatalf("net.Listen err: %v", err)}// 启动服务,绑定 gRPC 服务器到监听器并开始在端口上监听传入的消息err = s.Serve(lis)if err != nil {fmt.Printf("failed to serve: %v", err)return}
}

程序说明:

  • tls.LoadX509KeyPair() :从证书相关文件中读取和解析信息,得到证书公钥、密钥对。
func LoadX509KeyPair(certFile, keyFile string) (Certificate, error) {certPEMBlock, err := ioutil.ReadFile(certFile)if err != nil {return Certificate{}, err}keyPEMBlock, err := ioutil.ReadFile(keyFile)if err != nil {return Certificate{}, err}return X509KeyPair(certPEMBlock, keyPEMBlock)
}
  • x509.NewCertPool() :创建一个新的、空的 CertPool 。

  • certPool.AppendCertsFromPEM() :尝试解析所传入的 PEM 编码的证书,如果解析成功会将其加到 CertPool 中,便于后面的使用。

  • credentials.NewTLS() :构建基于 TLS 的 TransportCredentials 选项
    tls.Config:Config 结构用于配置 TLS 客户端或服务器。

在 Server 中共使用了以下三个 Config 配置项:

    • Certificates:设置证书链,允许包含一个或多个。
    • ClientAuth:要求必须校验客户端的证书。可以根据实际情况选用以下参数:
const (NoClientCert ClientAuthType = iotaRequestClientCertRequireAnyClientCertVerifyClientCertIfGivenRequireAndVerifyClientCert
)
  • ClientCAs:设置根证书的集合,校验方式使用 ClientAuth 中设定的模式。

(4)在 client 目录下初始化项目( go mod init client ),编写 Client 端程序调用服务,该程序的具体代码如下:

package mainimport ("context""flag""log""time""crypto/tls""crypto/x509""io/ioutil"pb "client/proto""google.golang.org/grpc/credentials""google.golang.org/grpc"
)// hello_clientconst (defaultName = "cqupthao!"
)var (addr = flag.String("addr", "127.0.0.1:8972", "the address to connect to")name = flag.String("name", defaultName, "Name to greet")
)func main() {flag.Parse()// 和 server 端一样,通过服务器端的证书和密钥直接创建 X.509 密钥对cert, err := tls.LoadX509KeyPair("cert/client.pem","cert/client.key")if err!= nil{log.Println("加载 client pem, key 失败!\n",err)}// 通过 CA 创建证书池certPool := x509.NewCertPool()caFile ,err :=  ioutil.ReadFile("cert/ca.crt")if err!= nil{log.Println("加载 ca 失败!\n",err)}// 将来自 CA 的客户端证书附加到证书池中certPool.AppendCertsFromPEM(caFile)// 添加传输凭证作为连接选项,ServerName 必须与证书中的 common Name 保持一致creds := credentials.NewTLS(&tls.Config{Certificates: []tls.Certificate{cert}, // 放入客户端证书ServerName: "*.mszlu.com", // 证书里面的 commonNameRootCAs: certPool, // 证书池})conn, err := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(creds))if err != nil {log.Fatalf("grpc.Dial err: %v", err)}defer conn.Close()c := pb.NewGreeterClient(conn)// 执行 RPC 调用并打印收到的响应数据ctx, cancel := context.WithTimeout(context.Background(), time.Second)defer cancel()r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})if err != nil {log.Fatalf("调用服务失败: %v", err)}log.Printf("调用服务成功: %s", r.GetReply())
}

在 Client 中绝大部分与 Server 一致,不同点的地方是在 Client 请求 Server 端时,Client 端会使用根证书和 ServerName 去对 Server 端进行校验,简单流程大致如下:

  • Client 端通过请求得到 Server 端的证书;

  • 使用 CA 认证的根证书对 Server 端的证书进行可靠性、有效性等校验;

  • 校验 ServerName 是否可用、有效。

执行 Server 端和 Client 端的程序,输出如下的结果:

2023/02/16 19:40:33 调用服务成功: Hello cqupthao!

基于 Token 的单向认证


(1)在任意目录下,分别创建 serverclient 目录存放服务端和客户端文件,proto 目录用于编写 IDL 的 tls.proto 文件,cert 目录存放证书文件,具体的目录结构如下所示:

TLSSingleTokenAuth
├── client
│   ├── cert
│   └── proto
│       └── tls.proto
└── server├── cert└── proto└── tls.proto

tls.proto 文件的具体内容如下所示:

syntax = "proto3"; // 版本声明,使用 Protocol Buffers v3 版本option go_package = "../proto";  // 指定生成的 Go 代码在项目中的导入路径package pb; // 包名// 定义服务
service Greeter {// SayHello 方法rpc SayHello (HelloRequest) returns (HelloResponse) {}
}// 请求消息
message HelloRequest {string name = 1;
}// 响应消息
message HelloResponse {string reply = 1;
}

(2)根据生成的 CA 证书,移动以下相应的证书到服务端和客户端目录下,进入 proto 目录生成 gRPC 源代码程序,在 proto 目录下执行以下的命令:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto

正确生成后的目录结构如下所示:

TLSSingleTokenAuth
├── client
│   ├── cert
│   │   ├── ca.crt
│   │   ├── client.key
│   │   └── client.pem
│   └── proto
│       ├── tls_grpc.pb.go
│       ├── tls.pb.go
│       └── tls.proto
└── server├── cert│   ├── ca.crt│   ├── server.key│   └── server.pem└── proto├── tls_grpc.pb.go├── tls.pb.go└── tls.proto

(3)在 server 目录下初始化项目( go mod init server ),编写 Server 端程序重写定义的方法,该程序的具体代码如下:

package mainimport ("context""fmt"pb "server/proto""net""log""google.golang.org/grpc/credentials""google.golang.org/grpc""google.golang.org/grpc/codes""google.golang.org/grpc/metadata" // 引入 grpc meta 包"google.golang.org/grpc/status"
)// hello servertype server struct {pb.UnimplementedGreeterServer
}func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {return &pb.HelloResponse{Reply: "Hello " + in.Name}, nil
}func main() {// 进行加载  key pair , 添加证书到 copy 程序中creds, err := credentials.NewServerTLSFromFile("cert/server.pem", "cert/server.key")if err != nil {log.Println("加载证书失败!\n", err)}// 实现 token 认证,合法的用户名和密码// 实现一个拦截器var authInterceptor grpc.UnaryServerInterceptorauthInterceptor = func(ctx context.Context,req interface{},info *grpc.UnaryServerInfo,handler grpc.UnaryHandler,) (resp interface{} , err error) {// 拦截普通方法请求,验证 tokenerr = Auth(ctx)if err != nil {return }// 继续处理请求return handler(ctx, req)}s := grpc.NewServer(grpc.Creds(creds),grpc.UnaryInterceptor(authInterceptor))pb.RegisterGreeterServer(s, &server{}) // 在 gRPC 服务端注册服务lis, err := net.Listen("tcp", "127.0.0.1:8972")if err != nil{log.Fatalf("net.Listen err: %v", err)}// 启动服务err = s.Serve(lis)if err != nil {fmt.Printf("failed to serve: %v", err)return}
}func Auth(ctx context.Context) error {// 获取用户名和密码md,ok := metadata.FromIncomingContext(ctx)if !ok {return fmt.Errorf("missing the credentials!")}var user stringvar password stringif val,ok := md["user"]; ok {user = val[0]}if val,ok := md["password"]; ok {password = val[0]}if user != "admin" || password != "cqupthao" {return status.Errorf(codes.Unauthenticated,"token 不合法!")}return nil
}

(4)在 client 目录下初始化项目( go mod init client ),创建 auth 目录,在该目录下编写 auth.go 程序,该程序的具体代码如下:

package authimport "context"type Authentication struct {User stringPassword 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 {// 是否开启 TLSreturn true
}

程序说明:

该程序定义了一个 Authentication 结构体并实现了 GetRequestMetadata() 方法和 RequireTransportSecurity() 方法,这是 gRPC 提供的自定义认证方式,每次 RPC 调用都会传输认证信息。

Authentication 结构体实现了 grpc/credential 包内的 PerRPCCredentials 接口,每次调用时,token 信息会通过请求的 metadata 传输到服务端。

(5)在 client 目录下,编写 Client 端程序调用服务,该程序的具体代码如下:

package mainimport ("context""flag""log""time"pb "client/proto""client/auth""google.golang.org/grpc/credentials""google.golang.org/grpc"
)// hello_clientconst (defaultName = "cqupthao!"
)var (addr = flag.String("addr", "127.0.0.1:8972", "the address to connect to")name = flag.String("name", defaultName, "Name to greet")
)func main() {flag.Parse()// 和 server 端一样,先创建证书池creds , err := credentials.NewClientTLSFromFile("cert/server.pem","*.mszlu.com")if err!= nil{log.Println("加载 pem 失败!\n",err)}// 设置用户名和密码token := &auth.Authentication{User:           "admin",Password:       "cqupthao",}conn, err := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(token))if err != nil {log.Fatalf("grpc.Dial err: %v", err)}defer conn.Close()c := pb.NewGreeterClient(conn)// 执行 RPC 调用并打印收到的响应数据ctx, cancel := context.WithTimeout(context.Background(), time.Second)defer cancel()r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})if err != nil {log.Fatalf("调用服务失败: %v", err)}log.Printf("调用服务成功: %s", r.GetReply())
}

执行 Server 端和 Client 端的程序,输出如下的结果:

2023/02/16 19:30:53  调用服务成功: Hello cqupthao!

若传递错误的用户名和密码,则输出如下的结果:

2023/02/16 19:41:43 调用服务失败: rpc error: code = Unauthenticated desc = token 不合法!
exit status 1

基于 Token 的双向认证


(1)在任意目录下,分别创建 serverclient 目录存放服务端和客户端文件,proto 目录用于编写 IDL 的 tls.proto 文件,cert 目录存放证书文件,具体的目录结构如下所示:

TLSDoubleTokenAuth
├── client
│   ├── cert
│   └── proto
│       └── tls.proto
└── server├── cert└── proto└── tls.proto

tls.proto 文件的具体内容如下所示:

syntax = "proto3"; // 版本声明,使用 Protocol Buffers v3 版本option go_package = "../proto";  // 指定生成的 Go 代码在项目中的导入路径package pb; // 包名// 定义服务
service Greeter {// SayHello 方法rpc SayHello (HelloRequest) returns (HelloResponse) {}
}// 请求消息
message HelloRequest {string name = 1;
}// 响应消息
message HelloResponse {string reply = 1;
}

(2)根据生成的 CA 证书,移动以下相应的证书到服务端和客户端目录下,进入 proto 目录生成 gRPC 源代码程序,在 proto 目录下执行以下的命令:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto

正确生成后的目录结构如下所示:

TLSDoubleTokenAuth
├── client
│   ├── cert
│   │   ├── ca.crt
│   │   ├── client.key
│   │   └── client.pem
│   └── proto
│       ├── tls_grpc.pb.go
│       ├── tls.pb.go
│       └── tls.proto
└── server├── cert│   ├── ca.crt│   ├── server.key│   └── server.pem└── proto├── tls_grpc.pb.go├── tls.pb.go└── tls.proto

(3)在 server 目录下初始化项目( go mod init server ),编写 Server 端程序重写定义的方法,该程序的具体代码如下:

package mainimport ("context""fmt"pb "server/proto""net""log""crypto/tls""crypto/x509""io/ioutil""google.golang.org/grpc/credentials""google.golang.org/grpc""google.golang.org/grpc/codes""google.golang.org/grpc/metadata" // 引入 grpc meta 包"google.golang.org/grpc/status"
)// hello servertype server struct {pb.UnimplementedGreeterServer
}func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {return &pb.HelloResponse{Reply: "Hello " + in.Name}, nil
}func Auth(ctx context.Context) error {// 获取用户名和密码md,ok := metadata.FromIncomingContext(ctx)if !ok {return fmt.Errorf("missing the credentials!")}var user stringvar password stringif val,ok := md["user"]; ok {user = val[0]}if val,ok := md["password"]; ok {password = val[0]}if user != "admin" || password != "cqupthao" {return status.Errorf(codes.Unauthenticated,"token 不合法!")}return nil
}func main() {// 进行加载  key paircert, err := tls.LoadX509KeyPair("cert/server.pem", "cert/server.key")if err != nil {log.Println("加载 x509 证书失败!\n", err)}// 创建证书池certPool := x509.NewCertPool()// 向证书池中加入证书cafileBytes, err := ioutil.ReadFile("cert/ca.crt")if err != nil {log.Println("读取 ca.pem 证书失败!\n", err)}// 加载客户端证书//certPool.AddCert()// 加载证书从 pem 文件里面certPool.AppendCertsFromPEM(cafileBytes)// 创建 credentials 对象creds := credentials.NewTLS(&tls.Config{Certificates: []tls.Certificate{cert},        // 服务端证书ClientAuth:   tls.RequireAndVerifyClientCert, // 需要并且验证客户端证书ClientCAs:    certPool,                       // 客户端证书池})// 实现 token 认证,合法的用户名和密码// 实现一个拦截器var authInterceptor grpc.UnaryServerInterceptorauthInterceptor = func(ctx context.Context,req interface{},info *grpc.UnaryServerInfo,handler grpc.UnaryHandler,) (resp interface{} , err error) {// 拦截普通方法请求,验证 tokenerr = Auth(ctx)if err != nil {return }// 继续处理请求return handler(ctx, req)}s := grpc.NewServer(grpc.Creds(creds),grpc.UnaryInterceptor(authInterceptor))pb.RegisterGreeterServer(s, &server{}) // 在 gRPC 服务端注册服务lis, err := net.Listen("tcp", "127.0.0.1:8972")if err != nil{log.Fatalf("net.Listen err: %v", err)}// 启动服务err = s.Serve(lis)if err != nil {fmt.Printf("failed to serve: %v", err)return}
}

(4)在 client 目录下初始化项目( go mod init client ),创建 auth 目录,在该目录下编写 auth.go 程序,该程序的具体代码如下:

package authimport "context"type Authentication struct {User stringPassword 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 {// 是否开启 TLSreturn true
}

程序说明:

该程序定义了一个 Authentication 结构体并实现了 GetRequestMetadata() 方法和 RequireTransportSecurity() 方法,这是 gRPC 提供的自定义认证方式,每次 RPC 调用都会传输认证信息。

Authentication 结构体实现了 grpc/credential 包内的 PerRPCCredentials 接口,每次调用时,token 信息会通过请求的 metadata 传输到服务端。

(5)在 client 目录下,编写 Client 端程序调用服务,该程序的具体代码如下:

package mainimport ("context""flag""log""time""crypto/tls""crypto/x509""io/ioutil"pb "client/proto""client/auth""google.golang.org/grpc/credentials""google.golang.org/grpc"
)// hello_clientconst (defaultName = "cqupthao!"
)var (addr = flag.String("addr", "127.0.0.1:8972", "the address to connect to")name = flag.String("name", defaultName, "Name to greet")
)func main() {flag.Parse()// 和 server 端一样,先创建证书池cert, err := tls.LoadX509KeyPair("cert/client.pem","cert/client.key")if err!= nil{log.Println("加载 client pem, key 失败!\n",err)}certPool := x509.NewCertPool()caFile ,err :=  ioutil.ReadFile("cert/ca.crt")if err!= nil{log.Println("加载 ca 失败!\n",err)}certPool.AppendCertsFromPEM(caFile)creds := credentials.NewTLS(&tls.Config{Certificates: []tls.Certificate{cert},// 放入客户端证书ServerName: "*.mszlu.com", // 证书里面的 commonNameRootCAs: certPool, // 证书池})// 设置用户名和密码token := &auth.Authentication{User:      "admin",Password:     "cqupthao",}conn, err := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(token))if err != nil {log.Fatalf("grpc.Dial err: %v", err)}defer conn.Close()c := pb.NewGreeterClient(conn)// 执行 RPC 调用并打印收到的响应数据ctx, cancel := context.WithTimeout(context.Background(), time.Second)defer cancel()r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})if err != nil {log.Fatalf("调用服务失败: %v", err)}log.Printf("调用服务成功: %s", r.GetReply())
}

执行 Server 端和 Client 端的程序,输出如下的结果:

2023/02/16 19:50:53  调用服务成功: Hello cqupthao!

若传递错误的用户名和密码,则输出如下的结果:

2023/02/16 19:51:43 调用服务失败: rpc error: code = Unauthenticated desc = token 不合法!
exit status 1

  • 参考视频:【码神之路】gRPC 系列完整教程

  • 参考链接:gRPC 教程

  • 参考链接:gRPC 官网

  • 参考书籍:《gRPC与云原生应用开发:以Go和Java为例》([斯里兰卡] 卡山 • 因德拉西里 丹尼什 • 库鲁普 著)

gRPC 的 SSL/TLS 加密认证相关推荐

  1. 二、Prometheus TLS加密认证和基于 basic_auth 用户名密码访问

    文章目录 Prometheus 基于用户名密码访问 1. `Node Export`端配置密码 2. 在被监控端这里生成密码 3. 在node_exporter中新增配置文件 4. node_expo ...

  2. SSL/TLS单向认证和双向认证介绍

    为了便于理解SSL/TLS的单向认证和双向认证执行流程,这里先介绍一些术语. 1. 散列函数(Hash function):又称散列算法.哈希函数,是一种从任何一种数据中创建小的数字"指纹& ...

  3. DM8的TLS加密认证配置相关

    1.为什么要使用SSL/TLS数字证书?   安装了SSL/TLS证书之后,可以保证客户端到服务器端之间的安全通信,数字证书采用非对称加密方式.虽然经过对称加密方式后的数据也无法被破译,但在使用了数字 ...

  4. SSL/TLS 双向认证(一) -- SSL/TLS工作原理

    本文部分参考: https://www.wosign.com/faq/faq2016-0309-03.htm https://www.wosign.com/faq/faq2016-0309-04.ht ...

  5. 第四篇:网络安全,SSL/TLS加密技术

    文章目录 一.前言 二.SSL/TLS 2.1 SSL/TLS是什么 2.2 SSL/TLS加密基本原理 2.3 SSL/TLS建立握手过程 三.CA & SSL Server & S ...

  6. SSL/TLS 双向认证(一) -- SSL/TLS 工作原理

    本文部分参考: https://www.wosign.com/faq/faq2016-0309-03.htm https://www.wosign.com/faq/faq2016-0309-04.ht ...

  7. SSL/TLS 双向认证

    其他参考链接 链接: https://blog.csdn.net/xxss120/article/details/78758832. 链接: https://blog.csdn.net/gx_1983 ...

  8. 开源项目SMSS发开指南(四)——SSL/TLS加密通信详解

    本文将详细介绍如何在Java端.C++端和NodeJs端实现基于SSL/TLS的加密通信,重点分析Java端利用SocketChannel和SSLEngine从握手到数据发送/接收的完整过程.本文也涵 ...

  9. 使用openssl进行ssl/tls加密传输会话测试

    [小蜗牛嘻哈之作] 我们首先看看下面一段"对话": [root@pps ~]# openssl s_client -connect localhost:110 -starttls ...

最新文章

  1. OpenCV-图像几何变换:旋转,缩放,斜切 .
  2. VSS2005 添加文件夹方法!
  3. iOS之深入解析单例的实现和销毁的底层原理
  4. 基因组装配新前沿:长片段完成完整的基因组
  5. 20温控f1什么意思_欧姆龙温控器是什么 欧姆龙温控器介绍【图文】
  6. JS继承之寄生类继承
  7. 伊洛纳登录显示服务器连接中,伊洛纳萌新入坑常见问题汇总
  8. 洛谷 2017.7月赛解题报告
  9. automak 和 autoconf 介绍
  10. 计算电磁学中的矩量法及其求解过程介绍
  11. 微信小程序 源码资源汇总
  12. 应用未安装!安装包似乎已经损坏
  13. 腾讯市值首破5000亿美元;阿里224亿港币入股高鑫零售;特斯拉新超跑在华接受预定丨价值早报
  14. 美团2018校园招聘 研发工程师(三)
  15. 洛谷P5149 会议座位
  16. 港科夜闻|罗康锦教授获委任为香港科大工学院院长
  17. 干货!JVM 基础面试题总结(持续更新)
  18. wxtemple.class.php,ThinkPHP3.2.3实现推送微信模板消息
  19. Android一键锁屏,去除锁屏密码
  20. 【python】解决给文件写入汉字,中文字符乱码问题

热门文章

  1. 职业教育标准教材·计算机组装与维修,中等职业教育计算机专业系列教材:计算机组装与维护...
  2. 天翼云 centos7+下挂载磁盘和宝塔环境安装
  3. Struts2动态方法调用(DMI)的三种方法
  4. Win32DiskImage写入123报错
  5. OpenGL ES基础教程,绘制三角形(补充,附代码)
  6. c# RestSharp 发送 x-www-form-urlundecoded 请求
  7. python如何控制输出格式_python格式化输出
  8. 打开eclipse的时候会自动在桌面创建文件夹,删除之后再次打开还是会自动创建
  9. 他大三就在顶刊发文并被邀成为审稿人:我只是比要求的多做一点
  10. WRF-4.0如何运行