假设你有一个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