「go-zero 系列」gRPC SSL/TLS 双向认证

2,325 阅读4分钟

💬

最近喜欢听蔡琴。

💻

继上次 grpc tls 单向认证之后,本文简单介绍一下双向认证以及 gRPC 自定义认证。

双向认证

双向认证与单向认证的区别在于,单向认证通常指的是客户端认证服务端,服务端不需要认证客户端,一般是到了应用层才做校验。而双向认证不仅是客户端认证服务端,服务端也认证客户端,不过不是允许的客户端则无法访问。具体见SSL/TLS 工作原理

核心步骤如下:

1、生成客户端证书,脚本可参考 grpc-auth-sample tls/gen.sh

echo "生成客户端 SAN 证书"
openssl genpkey -algorithm RSA -out client.key
openssl req -new -nodes -key client.key -out client.csr -days 3650 -subj "/C=$Country/O=$Organization/OU=$Organizational/CN=$CommonName" -config ./openssl.cnf -extensions v3_req
openssl x509 -req -days 3650 -in client.csr -out client.pem -CA ca.pem -CAkey ca.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req

2、添加凭证

server side

import (
	"context"
	"crypto/tls"
	"crypto/x509"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
)

//getCreds 添加凭证
func getCreds(err error) credentials.TransportCredentials {
  // reads and parses a public/private key pair from a pair of files.
	cert, err := tls.LoadX509KeyPair("../../tls/server.pem", "../../tls/server.key")
	if err != nil {
		log.Fatalf("tls.LoadX509KeyPair err: %v", err)
	}

	certPool := x509.NewCertPool() // returns a new, empty CertPool.
	ca, err := os.ReadFile("../../tls/ca.pem")
	if err != nil {
		log.Fatalf("ioutil.ReadFile err: %v", err)
	}

	if ok := certPool.AppendCertsFromPEM(ca); !ok { // 添加 ca 公钥
		log.Fatalf("certPool.AppendCertsFromPEM err")
	}
	
  // NewTLS uses c to construct a TransportCredentials based on TLS.
	creds := credentials.NewTLS(&tls.Config{ 
    // Certificates contains one or more certificate chains to present to the
    // other side of the connection. The first certificate compatible with the
    // peer's requirements is selected automatically.
		Certificates: []tls.Certificate{cert},				// 服务端证书
    // ClientAuth determines the server's policy for
    // TLS Client Authentication. The default is NoClientCert.
		ClientAuth:   tls.RequireAndVerifyClientCert, // 要求客户端需要携带证书并且客户端会认证
    // ClientCAs defines the set of root certificate authorities
    // that servers use if required to verify a client certificate
    // by the policy in ClientAuth.
		ClientCAs:    certPool,												// CA 
	})

	return creds
}

func main() {
  // 省略部分代码...
  s := grpc.NewServer(grpc.Creds(creds))
  // 省略部分代码...
}

client side

import (
	"crypto/tls"
	"crypto/x509"
	"log"
	"os"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
)

func main() {

	cert, err := tls.LoadX509KeyPair("../../tls/client.pem", "../../tls/client.key") // 注意这里是 client 的证书
	if err != nil {
		log.Fatalf("tls.LoadX509KeyPair err: %v", err)
	}

	certPool := x509.NewCertPool()
	ca, err := os.ReadFile("../../tls/ca.pem")
	if err != nil {
		log.Fatalf("ioutil.ReadFile err: %v", err)
	}

	if ok := certPool.AppendCertsFromPEM(ca); !ok {
		log.Fatalf("certPool.AppendCertsFromPEM err")
	}

	creds := credentials.NewTLS(&tls.Config{
		Certificates: []tls.Certificate{cert},
		ServerName:   "localhost",
		RootCAs:      certPool,
	})
  
	conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(creds))
  // 省略部分代码...
}

效果就不演示了,可以尝试失败的场景,例如使用不带证书的 client 连接 server,看看报错~

gRPC 自定义认证

自定义认证就是 client 已经连接 server 并发起请求了,然后我们做一层校验拦截,如果通过就放行,不通过则返回相应的 err,更具体的说明可参考4.8 对 RPC 方法做自定义认证

核心步骤如下:

1、首先 client 需要实现 google.golang.org/grpc/credentials 这个 package 下面的 PerRPCCredentials 接口

type PerRPCCredentials interface {
	// GetRequestMetadata gets the current request metadata, refreshing
	// tokens if required. 
  // 省略部分官方代码注释...
	GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) // 获取当前请求认证所需的元数据
	// RequireTransportSecurity indicates whether the credentials requires
	// transport security.
	RequireTransportSecurity() bool // 是否需要基于 TLS 认证进行安全传输
}
type Auth struct {
	AppKey    string `json:"app_key"`
	SecretKey string `json:"secret_key"`
}

func (a *Auth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
	return map[string]string{"app_key": a.AppKey, "secret_key": a.SecretKey}, nil
}

func (a *Auth) RequireTransportSecurity() bool { // 是否 TLS 认证
	// 如果设置了 true 但是 client 连接的时候使用 grpc.WithInsecure() 会直接 Fatal
	// grpc: the credentials require transport level security (use grpc.WithTransportCredentials() to set)
	return true
}


func main() {
  // ...
  auth := Auth{
		AppKey:    keys.GenAppKey(),
		SecretKey: keys.GenSecretKey(),
	}

	conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(&auth))
  // ...
}

2、server 端进行自定义校验,可以通过拦截器的方式或者在具体的 RPC 方法中进行认证校验

func Check(ctx context.Context) error {
	//从上下文中获取元数据
	md, ok := metadata.FromIncomingContext(ctx)
	grpclog.Infof("md is %+v\n", md)
	if !ok {
		return status.Errorf(codes.Unauthenticated, "获取 Token 失败")
	}
	var (
		appKey    string
		secretKey string
	)
	if value, ok := md["app_key"]; ok {
		appKey = value[0]
	}
	if value, ok := md["secret_key"]; ok {
		secretKey = value[0]
	}

	if len(appKey) != 16 || len(secretKey) != 32 {
		return status.Errorf(codes.Unauthenticated, "Token err")
	}

	grpclog.Infof("auth is %v, %v\n", appKey, secretKey)
	return nil
}

//getInterceptor 添加拦截器
func getInterceptor() grpc.UnaryServerInterceptor {
	interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
		//拦截普通方法请求,验证 Token
		//req proto.HelloRequest
		//info {"Server": "main.helloService", "FullMethod": "/hello.Hello/SayHello"}
		//handler real rpc func
		err = Check(ctx)
		if err != nil {
			return
		}
		// 继续处理请求
		return handler(ctx, req)
	}
	return interceptor
}

func main() {
  // ...
	interceptor := getInterceptor()
	s := grpc.NewServer(grpc.Creds(creds), grpc.ChainUnaryInterceptor(interceptor))
  // ...
}

效果也可以参考代码然后自己跑一下~

PS: 那么接下来就是要将认证方式整合到 go-zero ,发觉这个框架目前不支持的东西也不少, stream rpc message 无法生成对应的代码,mongo 支持的也不是很好,慢慢来吧

📖

最近偶尔看几篇《念楼学短》 很短的文言文加上旁边有【念楼读】以及【念楼曰】,挺有意思 值得一看

🌞

那么 活着才有希望。

写于 2021-04-11 凌晨