在Golang中使用OAuth认证令牌进行gRPC客户端和服务器通信

252 阅读2分钟

假设你有一个gRPC服务器,并想识别哪个客户正在试图与它进行通信。幸运的是,gRPC服务器有能力提供这样的功能。客户端在传输层上添加一些信息,gRPC服务器拦截该请求以进行识别检查。由于所有的客户端都使用相同的SSL证书进行验证,所以单单SSL并不能解决这里的问题。在这个例子中,我们将使用一个静态的不记名令牌来代表我们的客户端,服务器将在处理请求前检查它。

SSL证书

首先,你需要创建服务器的SSL证书:

$ openssl genrsa -out private.key 4096
Generating RSA private key, 4096 bit long modulus
.............++
.............++
(0x10001)
$ openssl req -new -x509 -sha256 -days 1825 -key private.key -out public.crt
Country Name (2 letter code) []:UK
State or Province Name (full name) []:London
Locality Name (eg, city) []:City of London
Organization Name (eg, company) []:You Ltd
Organizational Unit Name (eg, section) []:Engineering
Common Name (eg, fully qualified host name) []:localhost
Email Address []:you@you.com

结构

当你建立一个gRPC应用程序时,你首先要创建一个*.proto 文件并进行编译,然后开始开发你的应用程序:

├── Makefile
├── Readme.md
├── client
│   ├── cert
│   │   └── public.crt
│   └── main.go
├── go.mod
├── go.sum
├── pkg
│   └── proto
│       └── credit
│           ├── credit.pb.go
│           └── credit.proto
└── server
    ├── cert
    │   ├── private.key
    │   └── public.crt
    └── main.go

文件

编译文件

.PHONY: compile
compile: ## Compile the proto file.
	protoc -I pkg/proto/credit/ pkg/proto/credit/credit.proto --go_out=plugins=grpc:pkg/proto/credit/

.PHONY: server
server: ## Build and run server.
	go build -race -ldflags "-s -w" -o bin/server server/main.go
	bin/server

.PHONY: client
client: ## Build and run client.
	go build -race -ldflags "-s -w" -o bin/client client/main.go
	bin/client

credit.proto

syntax = "proto3";

package credit;

message CreditRequest {
    float amount = 1;
}

message CreditResponse {
    string confirmation = 1;
}

service CreditService {
    rpc Credit(CreditRequest) returns (CreditResponse) {}
}

credit.pb.go

我不在这里添加内容,因为它是用下面的命令生成的:

make compile

client/main.go

package main

import (
	"context"
	"log"
	"time"

	"github.com/YOU/bank/pkg/proto/credit"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/oauth"
	"golang.org/x/oauth2"
)

func main() {
	log.Println("Client running ...")

	rpcCreds := oauth.NewOauthAccess(&oauth2.Token{AccessToken: "client-x-id"})
	trnCreds, err := credentials.NewClientTLSFromFile("./client/cert/public.crt", "localhost")
	if err != nil {
		log.Fatalln(err)
	}

	opts := []grpc.DialOption{
		grpc.WithTransportCredentials(trnCreds),
		grpc.WithPerRPCCredentials(rpcCreds),
	}
	opts = append(opts, grpc.WithBlock())

	conn, err := grpc.Dial(":50051", opts...)
	if err != nil {
		log.Fatalln(err)
	}
	defer conn.Close()

	client := credit.NewCreditServiceClient(conn)

	request := &credit.CreditRequest{Amount: 1990.01}

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	response, err := client.Credit(ctx, request)
	if err != nil {
		log.Fatalln(err)
	}

	log.Println("Response:", response.GetConfirmation())
}

server/main.go

package main

import (
	"context"
	"crypto/tls"
	"fmt"
	"log"
	"net"
	"strings"

	"github.com/YOU/bank/pkg/proto/credit"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
)

type server struct {
	credit.UnimplementedCreditServiceServer
}

func main() {
	log.Println("Server running ...")

	cert, err := tls.LoadX509KeyPair("./server/cert/public.crt", "./server/cert/private.key")
	if err != nil {
		log.Fatalf("failed to load key pair: %s", err)
	}
	opts := []grpc.ServerOption{
		// Intercept request to check the token.
		grpc.UnaryInterceptor(validateToken),
		// Enable TLS for all incoming connections.
		grpc.Creds(credentials.NewServerTLSFromCert(&cert)),
	}

	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalln(err)
	}

	srv := grpc.NewServer(opts...)
	credit.RegisterCreditServiceServer(srv, &server{})

	log.Fatalln(srv.Serve(lis))
}

func (s *server) Credit(ctx context.Context, request *credit.CreditRequest) (*credit.CreditResponse, error) {
	log.Println(fmt.Sprintf("Request: %g", request.GetAmount()))

	return &credit.CreditResponse{Confirmation: fmt.Sprintf("Credited %g", request.GetAmount())}, nil
}

func validateToken(ctx context.Context, req interface{}, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, status.Errorf(codes.InvalidArgument, "missing metadata")
	}

	if !valid(md["authorization"]) {
		return nil, status.Errorf(codes.Unauthenticated, "invalid token")
	}

	return handler(ctx, req)
}

func valid(authorization []string) bool {
	if len(authorization) < 1 {
		return false
	}

	token := strings.TrimPrefix(authorization[0], "Bearer ")

	// If you have more than one client then you will have to update this line.
	return token == "client-x-id"
}

测试

$ make server
go build -race -ldflags "-s -w" -o bin/server server/main.go
bin/server
2020/04/04 18:07:37 Server running ...
$ make client
go build -race -ldflags "-s -w" -o bin/client client/main.go
bin/client
2020/04/04 18:07:42 Client running ...
2020/04/04 18:07:42 Response: Credited 1990.01

当你运行上面的客户端代码时,服务器也会输出下面的信息:

2020/04/04 18:07:42 Request: 1990.01

如果你把token改成其他东西,客户端将输出以下信息:

2020/04/04 21:47:21 rpc error: code = Unauthenticated desc = invalid token