1、认证方式

gRPC默认内置了两种认证方式:

  • SSL/TLS 认证方式

  • ALTS 认证方式(暂略)

  • 基于 Token 的认证方式

同时,gRPC提供了接口用于扩展自定义认证方式。

2、TLS认证示例

这里直接扩展hello项目,实现TLS认证机制

首先需要准备证书,在根目录新建keys目录用于存放证书文件。

golang 1.15+版本上,用 gRPC通过TLS实现数据传输加密时,证书需要开启SAN扩展(默认是没有开启SAN扩展的)

2.1 ca根证书生成

新建ca.conf,填入以下内容:

[ req ]
default_bits       = 4096
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
countryName                 = Country Name (2 letter code)
countryName_default         = CN
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = GuangDong
localityName                = Locality Name (eg, city)
localityName_default        = ShenZhen
organizationName            = Organization Name (eg, company)
organizationName_default    = top
commonName                  = Common Name (e.g. server FQDN or YOUR name)
commonName_max              = 64
commonName_default          = www.admincms.com  // XXX(自定义)

生成ca.key:

openssl genrsa -out ca.key 4096

生成ca.csr:(直接回车,采用default默认配置值)

openssl req -new -sha256 -out ca.csr -key ca.key -config ca.conf

生成ca.crt:

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

2.2 server证书生成

在keys目录下,新建server.conf,填入以下内容:

[ req ]
default_bits       = 2048
distinguished_name = req_distinguished_name
req_extensions     = req_ext

[ req_distinguished_name ]
countryName                 = Country Name (2 letter code)
countryName_default         = CN
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = GuangDong
localityName                = Locality Name (eg, city)
localityName_default        = ShenZhen
organizationName            = Organization Name (eg, company)
organizationName_default    = Sheld
commonName                  = CommonName (e.g. server FQDN or YOUR name)
commonName_max              = 64
commonName_default          = www.admincms.com    // 自定义,客户端需要此字段做匹配

[ req_ext ]
subjectAltName = @alt_names

[alt_names]
DNS.1 = www.admincms.com    // 自定义,可以还可以创建dns.2  ...  dns.n
IP    = 127.0.0.1

生成server.key:

openssl genrsa -out server.key 2048

生成server.csr:(直接回车,采用default默认配置值)

openssl req -new -sha256 -out server.csr -key server.key -config server.conf

生成server.crt:

openssl x509 -req -days 3650 -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.pem -extensions req_ext -extfile server.conf

2.3 服务端代码示例

// 地址
func main(){
    addr := "127.0.0.1:8180"
    // 1.监听
    listener, err := net.Listen("tcp", addr)
    if err != nil {
        grpclog.Fatalf("Failed to listen: %v", err)
    }
    fmt.Printf("监听端口:%s\n", addr)

    // tls认证
    creds,err := credentials.NewServerTLSFromFile("./framework/grpc/keys/server.pem", "./framework/grpc/keys/server.key")
    if err != nil {
        grpclog.Fatalf("Failed to generate credentials %v", err)
    }

    // 2.实例化gRPC
    //s := grpc.NewServer()
    s := grpc.NewServer(grpc.Creds(creds))

    // 3.在gRPC上注册微服务
    var u = UserInfoService{}
    pb.RegisterUserInfoServiceServer(s, &u)

    var h = HelloService{}
    pb.RegisterHelloServiceServer(s, &h)

    grpclog.Infoln("Listen on " + addr + " with TLS")

    // 4.启动服务端
    s.Serve(listener)
}

运行:

API server listening at: 127.0.0.1:12044
监听端口:127.0.0.1:8180

2.4 客户端代码示例

func main(){
    addr := "127.0.0.1:8180"

    // TLS连接  记得把server name改成你写的服务器地址
    creds, err := credentials.NewClientTLSFromFile("./framework/grpc/keys/server.pem", "www.admincms.com")
    if err != nil {
        grpclog.Fatalf("Failed to create TLS credentials %v", err)
    }

    // 1.连接
    //conn, err := grpc.Dial(addr, grpc.WithInsecure())
    conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(creds))
    if err != nil {
        grpclog.Fatalln(err)
    }
    defer conn.Close()

    // 2. 实例化gRPC客户端
    client := pb.NewUserInfoServiceClient(conn)

    // 3.组装请求参数
    req := new(pb.UserRequest)
    req.Name = "zs"

    // 4. 调用接口
    response, err := client.GetUserInfo(context.Background(), req)
    if err != nil {
        grpclog.Fatalln(err)
    }
    fmt.Printf("响应结果: %v\n", response)

    // 客户端调用HelloService
    hClient := pb.NewHelloServiceClient(conn)
    res1,err := hClient.SayHello(context.Background(), &pb.HelloRequest{
        Name: "grpc",
    })
    if err != nil {
        grpclog.Fatalln(err)
    }
    fmt.Println("响应结果:", res1)
}

运行:

API server listening at: 127.0.0.1:12105
响应结果: id:1  name:"zs"  age:22  hobby:"Sing"  hobby:"Run"
响应结果: result:"hello grpc"

3、Token认证示例

再进一步,继续扩展hello-tls项目,实现 TLS + Token 认证机制

3.1 客户端代码示例

package main

import (
    "context"
    "fmt"
    "go_study/framework/grpc/pb" // 导入proto
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/grpclog"
)

// OpenTLS 是否开启TLS认证
var OpenTLS = true

type customCredential struct {}

func (c *customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    return map[string]string{
        "appid":  "101010",
        "appkey": "i am key",
    }, nil
}

func (c *customCredential) RequireTransportSecurity() bool {
    return OpenTLS
}


func main(){
    addr := "127.0.0.1:8180"

    var err error
    var opts []grpc.DialOption

    if OpenTLS {
        // TLS连接  记得把server name改成你写的服务器地址
        creds, err := credentials.NewClientTLSFromFile("./framework/grpc/keys/server.pem", "www.admincms.com")
        if err != nil {
            grpclog.Fatalf("Failed to create TLS credentials %v", err)
        }
        opts = append(opts, grpc.WithTransportCredentials(creds))
    }else{
        opts = append(opts, grpc.WithInsecure())
    }

    // 使用自定义认证
    opts = append(opts, grpc.WithPerRPCCredentials(new(customCredential)))

    // 1.连接
    conn, err := grpc.Dial(addr, opts...)
    if err != nil {
        grpclog.Fatalln(err)
    }
    defer conn.Close()

    // 2. 实例化gRPC客户端
    client := pb.NewUserInfoServiceClient(conn)

    // 3.组装请求参数
    req := new(pb.UserRequest)
    req.Name = "zs"

    // 4. 调用接口
    response, err := client.GetUserInfo(context.Background(), req)
    if err != nil {
        grpclog.Fatalln(err)
    }
    fmt.Printf("响应结果: %v\n", response)

    // 客户端调用HelloService
    hClient := pb.NewHelloServiceClient(conn)
    res1,err := hClient.SayHello(context.Background(), &pb.HelloRequest{
        Name: "grpc",
    })
    if err != nil {
        grpclog.Fatalln(err)
    }
    fmt.Println("响应结果:", res1)
}

这里定义了一个customCredential结构体,并实现了两个方法GetRequestMetadataRequireTransportSecurity

这是gRPC提供的自定义认证方式,每次RPC调用都会传输认证信息。

customCredential 其实是实现了 grpc/credential 包内的 PerRPCCredentials 接口。

每次调用,token信息会通过请求的 metadata 传输到服务端。

下面具体看一下服务端如何获取 metadata 中的信息。

3.2 服务端代码示例

修改server/main.go中的SayHello方法(如有包含其他服务,也需一并修改)。

package main

import (
    "context"
    "fmt"
    "go_study/framework/grpc/pb" // 导入proto
    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/grpclog"
    "google.golang.org/grpc/metadata"
    "google.golang.org/grpc/status"
    "net"
)

// 定义 UserInfoService 服务
type UserInfoService struct{
    pb.UnimplementedUserInfoServiceServer
}

// GetUserInfo 实现方法
func (s *UserInfoService) GetUserInfo(ctx context.Context, req *pb.UserRequest) (resp *pb.UserResponse, err error) {

    err = tokenAuth(ctx)
    if err != nil {
        return nil, err
    }

    // 通过用户名查询用户信息
    name := req.Name
    // 数据里查用户信息
    if name == "zs" {
        resp = &pb.UserResponse{
            Id:    1,
            Name:  name,
            Age:   22,
            Hobby: []string{"Sing", "Run"},
        }
    }
    return
}

type HelloService struct {
    pb.UnimplementedHelloServiceServer
}

// 使用这种方式验证token 每个服务端每个服务中都验证一次,比较繁琐
func tokenAuth(ctx context.Context) error {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return status.Errorf(codes.Unauthenticated, "无Token认证信息")
    }

    var (
        appid  string
        appkey string
    )

    if val, ok := md["appid"]; ok {
        appid = val[0]
    }

    if val, ok := md["appkey"]; ok {
        appkey = val[0]
    }

    if appid != "101010" || appkey != "i am key" {
        return status.Errorf(codes.Unauthenticated, "Token认证信息无效: appid=%s, appkey=%s", appid, appkey)
    }
    return nil
}

func (h *HelloService) SayHello(ctx context.Context, in *pb.HelloRequest) (resp *pb.HelloResponse, err error) {
    err = tokenAuth(ctx)
    if err != nil {
        return nil, err
    }
    resp = new(pb.HelloResponse)
    resp.Result  = fmt.Sprintf("hello %s", in.Name)
    return resp, nil
}


func main(){
    // 地址
    addr := "127.0.0.1:8180"
    // 1.监听
    listener, err := net.Listen("tcp", addr)
    if err != nil {
        grpclog.Fatalf("Failed to listen: %v", err)
    }
    fmt.Printf("监听端口:%s\n", addr)

    // tls认证
    creds,err := credentials.NewServerTLSFromFile("./framework/grpc/keys/server.pem", "./framework/grpc/keys/server.key")
    if err != nil {
        grpclog.Fatalf("Failed to generate credentials %v", err)
    }

    // 2.实例化gRPC
    //s := grpc.NewServer()
    s := grpc.NewServer(grpc.Creds(creds))

    // 3.在gRPC上注册微服务
    var u = UserInfoService{}
    pb.RegisterUserInfoServiceServer(s, &u)

    var h = HelloService{}
    pb.RegisterHelloServiceServer(s, &h)

    grpclog.Infoln("Listen on " + addr + " with TLS")

    // 4.启动服务端
    s.Serve(listener)
}

服务端可以从context中获取每次请求的metadata,从中读取客户端发送的token信息并验证有效性。

使用这种方式验证token 每个服务端每个服务中都验证一次,比较繁琐。(下一篇介绍拦截器)

运行服务端:

再运行客户端:

google.golang.org/grpc/credentials/oauth 包已实现了用于Google API的oauth和jwt验证的方法,使用方法可以参考官方文档。

在实际应用中,我们可以根据自己的业务需求实现合适的验证方式。

作者:joker.liu  创建时间:2023-05-22 16:42
最后编辑:joker.liu  更新时间:2023-05-23 14:46