本文,将续上一个博客未完成的尾巴,对gRPC加密通信进行一个升级。主要是分为:
- 使用CA,为server端和client端颁发证书并通信。
- 使用token对不同的方法访问进行权限管理。
我们将在上一次的代码中进行重构,参考:
基于CA证书的认证和加密通信
如下图所示,为CA与服务端和客户端的交互过程:
在上一篇博客中,我们已经为server端颁发了证书,现在,我们需要为client端颁发证书:
## 生成client私钥
$ openssl genrsa -out client.key 2048
## 根据私钥client.key生成证书请求文件client.csr
$ openssl req -new -nodes -key client.key -out client.csr -subj "/C=cn/OU=myclient/O=clientcomp/CN=clientname" -config ./openssl.cfg - extensions v3_req
## 请求CA对证书请求文件签名,生成最终证书文件
$ openssl x509 -req -days 365 -in client.csr -out client.pem -CA ca.crt -CAkey ca.key -CAcreateserial -extfile ./openssl.cfg -extensions v3_req
目录结构
λ tree /F
D:.
│ go.mod
│ go.sum
├─cert ## CA根证书
│ ca.crt
│ ca.csr
│ ca.key
│ openssl.cfg
├─client ## client证书和代码
│ client.csr
│ client.go
│ client.key
│ client.pem
├─proto
│ helloworld.pb.go
│ helloworld.proto
└─server ## server证书和代码
server.csr
server.go
server.key
server.pem
server
package main
......
func main() {
// 从证书相关文件中读取和解析信息,得到证书公钥、密钥对
certificate, err := tls.LoadX509KeyPair("./server/server.pem", "./server/server.key")
if err != nil {
log.Fatal(err)
}
// 创建一个新的、空的 CertPool
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile("./cert/ca.crt")
if err != nil {
log.Fatal(err)
}
// 尝试解析所传入的 PEM 编码的证书。如果解析成功会将其加到 CertPool 中,便于后面的使用
if ok := certPool.AppendCertsFromPEM(ca); !ok {
log.Fatal("failed to append certs")
}
// credentials.NewTLS:构建基于 TLS 的 TransportCredentials 选项
creds := credentials.NewTLS(&tls.Config{ // Config 结构用于配置 TLS 客户端或服务器
Certificates: []tls.Certificate{certificate},
// tls.RequireAndVerifyClientCert
// 表示 Server 也会使用 CA 认证的根证书对 Client 端的证书进行校验
ClientAuth: tls.RequireAndVerifyClientCert, // NOTE: this is optional!
ClientCAs: certPool,
})
// 初始化一个gRPC的结构体对象,传入creds
s := grpc.NewServer(grpc.Creds(creds))
// 注册服务
pb.RegisterGreeterServer(s, &server{})
......
}
有一篇大佬的博客,对这个介绍得更详细:带入gRPC:基于 CA 的 TLS 证书认证
client
package main
.....
func main() {
// 从证书相关文件中读取和解析信息,得到证书公钥、密钥对
certificate, err := tls.LoadX509KeyPair("./client/client.pem", "./client/client.key")
if err != nil {
log.Fatal(err)
}
// 创建一个新的、空的 CertPool
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile("./cert/ca.crt")
if err != nil {
log.Fatal(err)
}
if ok := certPool.AppendCertsFromPEM(ca); !ok {
log.Fatal("failed to append ca certs")
}
// 在 Client 请求 Server 端时,Client 端会使用根证书和 ServerName 去对 Server 端进行校验项
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{certificate},
ServerName: "hello.org.haha.com", // NOTE: this is required!
RootCAs: certPool,
})
// 连接 gRPC 服务器,WithTransportCredentials表示需要证书
conn, err := grpc.Dial(address, grpc.WithTransportCredentials(creds))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
......
}
有一篇大佬的博客,对这个介绍得更详细:带入gRPC:基于 CA 的 TLS 证书认证
基于Token的认证和权限控制
gRPC还为每个gRPC方法调用提供了认证支持,这样就基于用户Token对不同的方法访问进行权限管理。
我们将在上一次的代码中进行重构,参考:
目录结构
λ tree /F
D:.
│ go.mod
│ go.sum
├─cert
│ ca.crt
│ ca.csr
│ ca.key
│ myInterface.go
│ openssl.cfg
├─client
│ client.csr
│ client.go
│ client.key
│ client.pem
├─proto
│ helloworld.pb.go
│ helloworld.proto
└─server
server.csr
server.go
server.key
server.pem
要实现对每个gRPC方法进行认证,需要实现grpc.PerRPCCredentials
接口,可以看到,该接口的样子为:
这里,我们增添了一个新的目录cert/myInterface.go
,这个文件负责实现需要实现grpc.PerRPCCredentials
接口。
Authentication接口
在GetRequestMetadata
方法中返回认证需要的必要信息。RequireTransportSecurity
方法表示是否要求底层使用安全链接。
在真实的环境中建议必须要求底层启用安全的链接,否则认证信息有泄露和被篡改的风险。
我们可以创建一个Authentication类型,用于实现用户名和密码的认证:
package cert
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
)
// 用户Token验证的结构体
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
}
// 在GetRequestMetadata方法中,我们返回地认证信息包装login和password两个信息。
// 这里,我们令其为 true,需要证书
func (a *Authentication) RequireTransportSecurity() bool {
return true
}
// 增加Authentication方法,进行权限判断
func (a *Authentication) Auth(ctx context.Context) error {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return fmt.Errorf("missing credentials")
}
var appId, appKey string
if val, ok := md["user"]; ok {
appId = val[0]
}
if val, ok := md["password"]; ok {
appKey = val[0]
}
if appId != a.User || appKey != a.Password {
return grpc.Errorf(codes.Unauthenticated, "invalid token")
}
return nil
}
详细地认证工作主要在Authentication.Auth
方法中完成。首先通过metadata.FromIncomingContext
从ctx
上下文中获取元信息,然后取出相应的认证信息进行认证。如果认证失败,则返回一个codes.Unauthenticated
类型地错误。
server
在gRPC服务端的每个方法中通过Authentication类型的 Auth
方法进行身份认证:
package main
......
// server 用于实现 helloworld.GreeterServer接口
type server struct{
auth *cert.Authentication
}
// SayHello 实现 helloworld.GreeterServer 接口里的方法
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
auth := cert.Authentication{ // 设置密码
User: "myName",
Password: "myPassword",
}
s.auth = &auth
// 在每一个方法中都要进行这样的判断
if err := s.auth.Auth(ctx); err != nil {
return nil, err
}
log.Printf("Received: %v", in.GetName())
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}
func main() {
// 引入证书
certificate, err := tls.LoadX509KeyPair("./server/server.pem", "./server/server.key")
......
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{certificate},
ClientAuth: tls.RequireAndVerifyClientCert, // NOTE: this is optional!
ClientCAs: certPool,
})
......
}
client
然后在每次请求gRPC服务时就可以将Token信息作为参数选项传入。
通过grpc.WithPerRPCCredentials
函数将Authentication对象转为grpc.Dial
参数。
因为启用了安全链接grpc.WithTransportCredentials()
,需要进行安全认证。
package main
......
func main() {
certificate, err := tls.LoadX509KeyPair("./client/client.pem", "./client/client.key")
......
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{certificate},
ServerName: "hello.org.haha.com", // NOTE: this is required!
RootCAs: certPool,
})
// 从终端读取密码
auth := cert.Authentication{
User: "myName",
Password: "myPassword",
}
//auth.User = os.Args[1]
//auth.Password = os.Args[2]
// grpc.WithPerRPCCredentials(&auth),开启token权限
conn, err := grpc.Dial(address, grpc.WithTransportCredentials(creds),grpc.WithPerRPCCredentials(&auth))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
//初始化gRPC 客户端
c := pb.NewGreeterClient(conn)
// 输入参数
name := defaultName
if len(os.Args) > 3 { //如果外部没有接收到参数,则使用默认参数
name = os.Args[3]
}
......
}
实验
总结
在本文中,介绍了如何通过CA证书,为服务端和客户端颁发证书,并且基于此,服务端和客户端如何进行TLS加密通信。
此外,在TLS加密的基础上,对指定的gRPC 服务方法进行token权限限制调用。
自此,我们的gRPC 通信相对来说,已经很安全了。