本章把之前的工程结构改了一下,创建了 server 和 client 两个目录,分别把 server.go,client.go 移动过去。
接下来会介绍 grpc 的 TLS 认证和 Oauth2
一、TLS认证
在进行功能验证是需要使用 openssl 创建自有证书,下面是创建步骤。
创建 ca 证书:
cat > openssl.cnf << EOF
copy_extensions = copy
[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no
[req_distinguished_name]
# 国家
C = CN
# 省份
ST = BeiJing
# 城市
L = BeiJing
# 组织
O = grpcdemo
# 部门
OU = grpcdemo
# 域名
CN = api.grpcdemo.com
[v3_req]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[alt_names]
# 解析域名
DNS.1 = *.api.grpcdemo.com
# 可配置多个域名,如下
DNS.2 = *.grpcdemo.com
EOF
openssl genrsa -des3 -out ca.key 2048 # 回车后需要输入两次密码,任意即可,123456
openssl req -new -key ca.key -out ca.csr # 回车后需要输入一些信息,如下所示
# Enter pass phrase for ca.key:123456
# You are about to be asked to enter information that will be incorporated
# into your certificate request.
# What you are about to enter is what is called a Distinguished Name or a DN.
# There are quite a few fields but you can leave some blank
# For some fields there will be a default value,
# If you enter '.', the field will be left blank.
# -----
# Country Name (2 letter code) [AU]:CN
# State or Province Name (full name) [Some-State]:BeiJing
# Locality Name (eg, city) []:BeiJing
# Organization Name (eg, company) [Internet Widgits Pty Ltd]:grpcdemo
# Organizational Unit Name (eg, section) []:grpcdemo
# Common Name (e.g. server FQDN or YOUR name) []:api.grpcdemo.com
# Email Address []:grpcdemo@grpc.com
# Please enter the following 'extra' attributes
# to be sent with your certificate request
# A challenge password []:直接回车
# An optional company name []:直接回车
openssl x509 -req -days 365 -in ca.csr -signkey ca.key -out ca.crt # 这边也需要验证刚才的密码,123456
创建 server 证书
mkdir server
openssl genpkey -algorithm RSA -out server/server.key
openssl req -new -nodes -key server/server.key -out server/server.csr -config openssl.cnf -extensions v3_req
openssl x509 -req -in server/server.csr -out server/server.pem -CA ca.crt -CAkey ca.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req # 需要验证密码,123456
创建完成后的文件路径如下:
.
├── ca.crt
├── ca.csr
├── ca.key
├── ca.srl
├── openssl.cnf
└── server
├── server.csr
├── server.key
└── server.pem
接下来把 server/server.key 和 server/server.pem 放到我们的代码工程里去,现在的代码工程结构为:
.
├── calc
│ ├── calc.pb.go
│ ├── calcRequest.pb.go
│ ├── calcServer.go
│ └── calc_grpc.pb.go
├── cert
│ ├── server.key
│ └── server.pem
├── client
│ └── client.go
├── go.mod
├── go.sum
├── log
│ ├── grpc.log -> grpc.log.20240413
│ └── grpc.log.20240413
├── main.go
├── proto
│ ├── calc.proto
│ └── calcRequest.proto
└── server
└── server.go
修改客户端代码
// client.go
package client
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"grpcDemo/calc"
)
func Test(addr string) {
// 注意:这里使用的是服务端证书和证书中的名称
creds, err := credentials.NewClientTLSFromFile("./cert/server.pem", "api.grpcdemo.com")
if err != nil {
panic(err)
}
// 创建rpc连接
cc, err := grpc.NewClient(addr,
grpc.WithTransportCredentials(creds))
if err != nil {
panic(err)
}
defer cc.Close()
// 创建rpc调用客户端
cli := calc.NewCalcClient(cc)
// 调用具体的rpc方法
versionRsp, err := cli.Version(context.Background(), &calc.Empty{})
if err != nil {
panic(err)
}
fmt.Println("server version:", versionRsp.GetStr())
sumRsp, err := cli.Sum(context.Background(), &calc.CalcRequest{
A: 1,
B: 2,
})
fmt.Println("1+2=", sumRsp.GetNum())
}
修改服务端代码
// server.go
package server
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"grpcDemo/calc"
"net"
"time"
)
func Start(addr string) {
// 监听端口
ls, err := net.Listen("tcp", addr)
if err != nil {
panic(err)
}
defer ls.Close()
creds, err := credentials.NewServerTLSFromFile("./cert/server.pem", "./cert/server.key")
if err != nil {
panic(err)
}
// 创建grpc服务
gServer := grpc.NewServer(grpc.Creds(creds))
// 注册rpc
calc.RegisterCalcServer(gServer, &calc.Server{})
if err = gServer.Serve(ls); err != nil {
panic(err)
}
}
二、Oauth2认证
这里以基础使用为例子,oauth2也支持的认证方法比较多,比如 jwt 之类的,大家可以自行探索下。
2.1. grpc 客户端
grpc 客户端提供了为每个 rpc 连接进行认证的配置。
func WithPerRPCCredentials(creds credentials.PerRPCCredentials) DialOption {
return newFuncDialOption(func(o *dialOptions) {
o.copts.PerRPCCredentials = append(o.copts.PerRPCCredentials, creds)
})
}
google.golang.org/grpc/credentials/oauth 包里的 TokenSource
类型实现了 credentials.PerRPCCredentials
的接口。
type TokenSource struct {
oauth2.TokenSource
}
golang.org/x/oauth2 包中的 TokenSource 如下:
type TokenSource interface {
// Token returns a token or an error.
// Token must be safe for concurrent use by multiple goroutines.
// The returned Token must not be modified.
Token() (*Token, error)
}
// ...
func StaticTokenSource(t *Token) TokenSource {
return staticTokenSource{t}
}
// ...
type Token struct {
// AccessToken is the token that authorizes and authenticates
// the requests.
AccessToken string `json:"access_token"`
// TokenType is the type of token.
// The Type method returns either this or "Bearer", the default.
TokenType string `json:"token_type,omitempty"`
// RefreshToken is a token that's used by the application
// (as opposed to the user) to refresh the access token
// if it expires.
RefreshToken string `json:"refresh_token,omitempty"`
// Expiry is the optional expiration time of the access token.
//
// If zero, TokenSource implementations will reuse the same
// token forever and RefreshToken or equivalent
// mechanisms for that TokenSource will not be used.
Expiry time.Time `json:"expiry,omitempty"`
// raw optionally contains extra metadata from the server
// when updating a token.
raw interface{}
// expiryDelta is used to calculate when a token is considered
// expired, by subtracting from Expiry. If zero, defaultExpiryDelta
// is used.
expiryDelta time.Duration
}
所以我们修改客户端代码如下:
// client.go
package client
import (
"context"
"fmt"
"golang.org/x/oauth2"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/oauth"
"grpcDemo/calc"
)
func Test(addr string) {
// 注意:这里使用的是服务端证书和证书中的名称
creds, err := credentials.NewClientTLSFromFile("./cert/server.pem", "api.grpcdemo.com")
if err != nil {
panic(err)
}
// 创建rpc连接
cc, err := grpc.NewClient(addr,
grpc.WithTransportCredentials(creds),
// 默认bearer token
grpc.WithPerRPCCredentials(oauth.TokenSource{TokenSource: oauth2.StaticTokenSource(&oauth2.Token{
AccessToken: "grpc-demo-token",
})}))
if err != nil {
panic(err)
}
defer cc.Close()
// 创建rpc调用客户端
cli := calc.NewCalcClient(cc)
// 调用具体的rpc方法
versionRsp, err := cli.Version(context.Background(), &calc.Empty{})
if err != nil {
panic(err)
}
fmt.Println("server version:", versionRsp.GetStr())
sumRsp, err := cli.Sum(context.Background(), &calc.CalcRequest{
A: 1,
B: 2,
})
fmt.Println("1+2=", sumRsp.GetNum())
}
2.2. grpc 服务端
这里我们介绍下两个知识点
- grpc 元数据
grpc 是基于 HTTP/2 进行通信的,这个元数据类似于 http header,类型定义为
type MD map[string][]string
。在服务端可以用metadata.FromIncomingContext
获取元数据,还有个封装好的方法metadata.ValueFromIncomingContext
可以获取某个指定 key 的元数据。
- grpc 拦截器
grpc 服务端创建时,提供
grpc.UnaryInterceptor
选项可以配置非流式 rcp 的拦截器,具体使用比较简单,这里不赘述了。
下面看下服务端代码:
// server.go
package server
import (
"context"
"errors"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"grpcDemo/calc"
"net"
)
func interceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
tokens := metadata.ValueFromIncomingContext(ctx, "authorization")
if len(tokens) == 0 {
return nil, errors.New("invalid token")
}
fmt.Println("get token:", tokens[0])
return handler(ctx, req)
}
func Start(addr string) {
// 监听端口
ls, err := net.Listen("tcp", addr)
if err != nil {
panic(err)
}
defer ls.Close()
creds, err := credentials.NewServerTLSFromFile("./cert/server.pem", "./cert/server.key")
if err != nil {
panic(err)
}
// 创建grpc服务
gServer := grpc.NewServer(grpc.Creds(creds), grpc.UnaryInterceptor(interceptor))
// 注册rpc
calc.RegisterCalcServer(gServer, &calc.Server{})
if err = gServer.Serve(ls); err != nil {
panic(err)
}
}
运行结果如下:
三、自定义认证
上面可以看到,我们在进行认证配置的时候使用的是grpc.WithPerRPCCredentials
,所以我们实现credentials.PerRPCCredentials
类型的接口即可自定义认证内容。
// PerRPCCredentials defines the common interface for the credentials which need to
// attach security information to every RPC (e.g., oauth2).
type PerRPCCredentials interface {
// GetRequestMetadata gets the current request metadata, refreshing tokens
// if required. This should be called by the transport layer on each
// request, and the data should be populated in headers or other
// context. If a status code is returned, it will be used as the status for
// the RPC (restricted to an allowable set of codes as defined by gRFC
// A54). uri is the URI of the entry point for the request. When supported
// by the underlying implementation, ctx can be used for timeout and
// cancellation. Additionally, RequestInfo data will be available via ctx
// to this call. TODO(zhaoq): Define the set of the qualified keys instead
// of leaving it as an arbitrary string.
GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
// RequireTransportSecurity indicates whether the credentials requires
// transport security.
RequireTransportSecurity() bool
}
下面我们在项目中创建 auth 目录,并创建 auth.go 的文件:
// auth.go
package auth
import (
"context"
"errors"
"google.golang.org/grpc/metadata"
)
type GrpcDemoAuth struct {
Ak, Sk string // 自定义认证内容
UseTls bool // 是否进行使用tls加密
}
func (g GrpcDemoAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{"ak": g.Ak, "sk": g.Sk}, nil
}
func (g GrpcDemoAuth) RequireTransportSecurity() bool {
return g.UseTls
}
// 从元数据中构建认证token
func GetFromCtx(ctx context.Context) (GrpcDemoAuth, error) {
g := GrpcDemoAuth{}
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return g, errors.New("metadata nil")
}
aks := md.Get("ak")
sks := md.Get("sk")
if len(aks) == 0 || len(sks) == 0 {
return g, errors.New("token error")
}
g.Ak = aks[0]
g.Sk = sks[0]
return g, nil
}
客户端实现:
// client.go
package client
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"grpcDemo/auth"
"grpcDemo/calc"
)
func Test(addr string) {
// 注意:这里使用的是服务端证书和证书中的名称
creds, err := credentials.NewClientTLSFromFile("./cert/server.pem", "api.grpcdemo.com")
if err != nil {
panic(err)
}
// 创建rpc连接
cc, err := grpc.NewClient(addr,
grpc.WithTransportCredentials(creds),
// 默认bearer token
grpc.WithPerRPCCredentials(&auth.GrpcDemoAuth{
Ak: "grpc-demo-ak",
Sk: "grpc-demo-sk",
UseTls: true,
}))
if err != nil {
panic(err)
}
defer cc.Close()
// 创建rpc调用客户端
cli := calc.NewCalcClient(cc)
// 调用具体的rpc方法
versionRsp, err := cli.Version(context.Background(), &calc.Empty{})
if err != nil {
panic(err)
}
fmt.Println("server version:", versionRsp.GetStr())
sumRsp, err := cli.Sum(context.Background(), &calc.CalcRequest{
A: 1,
B: 2,
})
fmt.Println("1+2=", sumRsp.GetNum())
}
服务端实现:
// server.go
package server
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"grpcDemo/auth"
"grpcDemo/calc"
"net"
)
func interceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
authToken, err := auth.GetFromCtx(ctx)
if err != nil {
return nil, err
}
fmt.Printf("get token ak:%s, sk:%s\n", authToken.Ak, authToken.Sk)
return handler(ctx, req)
}
func Start(addr string) {
// 监听端口
ls, err := net.Listen("tcp", addr)
if err != nil {
panic(err)
}
defer ls.Close()
creds, err := credentials.NewServerTLSFromFile("./cert/server.pem", "./cert/server.key")
if err != nil {
panic(err)
}
// 创建grpc服务
gServer := grpc.NewServer(grpc.Creds(creds), grpc.UnaryInterceptor(interceptor))
// 注册rpc
calc.RegisterCalcServer(gServer, &calc.Server{})
if err = gServer.Serve(ls); err != nil {
panic(err)
}
}
运行结果: