💬
最近喜欢听蔡琴。
💻
继上次 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 凌晨