简介
传输层安全性协议(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.com
DNS.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)在任意目录下,分别创建 server 和 client 目录存放服务端和客户端文件,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 main
import (
"context"
"fmt"
pb "server/proto"
"net"
"log"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc"
)
// hello server
type 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 程序中,为所有传入的连接启用 TLS
creds, 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 main
import (
"context"
"flag"
"log"
"time"
pb "client/proto"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc"
)
// hello_client
const (
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)在任意目录下,分别创建 server 和 client 目录存放服务端和客户端文件,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 main
import (
"context"
"fmt"
pb "server/proto"
"net"
"log"
"crypto/tls"
"crypto/x509"
"io/ioutil"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc"
)
// hello server
type 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 凭证为所以传入的连接启用 TLS
creds := 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 = iota
RequestClientCert
RequireAnyClientCert
VerifyClientCertIfGiven
RequireAndVerifyClientCert
)
-
- ClientCAs:设置根证书的集合,校验方式使用 ClientAuth 中设定的模式。
(4)在 client 目录下初始化项目( go mod init client
),编写 Client 端程序调用服务,该程序的具体代码如下:
package main
import (
"context"
"flag"
"log"
"time"
"crypto/tls"
"crypto/x509"
"io/ioutil"
pb "client/proto"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc"
)
// hello_client
const (
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", // 证书里面的 commonName
RootCAs: 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)在任意目录下,分别创建 server 和 client 目录存放服务端和客户端文件,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 main
import (
"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 server
type 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.UnaryServerInterceptor
authInterceptor = func(
ctx context.Context,
req interface{
},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (resp interface{
} , err error) {
// 拦截普通方法请求,验证 token
err = 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 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 != "cqupthao" {
return status.Errorf(codes.Unauthenticated,"token 不合法!")
}
return nil
}
(4)在 client 目录下初始化项目( go mod init client
),创建 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 {
// 是否开启 TLS
return true
}
程序说明:
该程序定义了一个 Authentication 结构体并实现了 GetRequestMetadata() 方法和 RequireTransportSecurity() 方法,这是 gRPC 提供的自定义认证方式,每次 RPC 调用都会传输认证信息。
Authentication 结构体实现了 grpc/credential
包内的 PerRPCCredentials 接口,每次调用时,token 信息会通过请求的 metadata 传输到服务端。
(5)在 client 目录下,编写 Client 端程序调用服务,该程序的具体代码如下:
package main
import (
"context"
"flag"
"log"
"time"
pb "client/proto"
"client/auth"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc"
)
// hello_client
const (
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)在任意目录下,分别创建 server 和 client 目录存放服务端和客户端文件,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 main
import (
"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 server
type 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 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 != "cqupthao" {
return status.Errorf(codes.Unauthenticated,"token 不合法!")
}
return nil
}
func main() {
// 进行加载 key pair
cert, 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.UnaryServerInterceptor
authInterceptor = func(
ctx context.Context,
req interface{
},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (resp interface{
} , err error) {
// 拦截普通方法请求,验证 token
err = 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 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 {
// 是否开启 TLS
return true
}
程序说明:
该程序定义了一个 Authentication 结构体并实现了 GetRequestMetadata() 方法和 RequireTransportSecurity() 方法,这是 gRPC 提供的自定义认证方式,每次 RPC 调用都会传输认证信息。
Authentication 结构体实现了 grpc/credential
包内的 PerRPCCredentials 接口,每次调用时,token 信息会通过请求的 metadata 传输到服务端。
(5)在 client 目录下,编写 Client 端程序调用服务,该程序的具体代码如下:
package main
import (
"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_client
const (
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", // 证书里面的 commonName
RootCAs: 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为例》([斯里兰卡] 卡山 • 因德拉西里 丹尼什 • 库鲁普 著)