自己做视频网站流量钱,桂林技术交流站,青岛网站建设电话,中山免费企业网站建设1、gRPC认证
前面篇章的gRPC都是明文传输的#xff0c;容易被篡改数据#xff0c;本章将介绍如何为gRPC添加安全机制。
gRPC默认内置了两种认证方式#xff1a; SSL/TLS认证方式 基于Token的认证方式
同时#xff0c;gRPC提供了接口用于扩展自定义认证方式。
1.1 TLS…1、gRPC认证
前面篇章的gRPC都是明文传输的容易被篡改数据本章将介绍如何为gRPC添加安全机制。
gRPC默认内置了两种认证方式 SSL/TLS认证方式 基于Token的认证方式
同时gRPC提供了接口用于扩展自定义认证方式。
1.1 TLS认证
1.1.1 什么是TLS
TLSTransport Layer Security安全传输层)TLS是建立在传输层TCP协议之上的协议服务于应用层它的前
身是SSLSecure Socket Layer安全套接字层它实现了将应用层的报文进行加密后再交由TCP进行传输的功
能。
1.1.2 TLS的作用
TLS协议主要解决如下三个网络安全问题。 保密(message privacy)保密通过加密encryption实现所有信息都加密传输第三方无法嗅探 完整性(message integrity)通过MAC校验机制一旦被篡改通信双方会立刻发现 认证(mutual authentication)双方认证,双方都可以配备证书防止身份被冒充
这里实现TLS认证机制首先需要准备证书在tls_demo目录新建keys目录用于存放证书文件。
1.1.3 证书制作
openSSL下载安装地址http://slproweb.com/products/Win32OpenSSL.html
(1)、制作私钥 (server.key)
# 生成RSA私钥
[rootzsx keys]# openssl genrsa -out server.key 2048
Generating RSA private key, 2048 bit long modulus
.................
...................
e is 65537 (0x10001)
# 或者可以生成ECC私钥
# 生成ECC私钥,命令为椭圆曲线密钥参数生成及操作,这里ECC曲线选择的是secp384r1
openssl ecparam -genkey -name secp384r1 -out server.key[rootzsx keys]# ls
server.key(2)、自签名公钥(server.pem)
会生成serve.pem其中Common Name也就是域名我填的是xgrpc.com。
[rootzsx keys]# openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650
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) [XX]:cn
State or Province Name (full name) []:tj
Locality Name (eg, city) [Default City]:tj
Organization Name (eg, company) [Default Company Ltd]:ndty
Organizational Unit Name (eg, section) []:ndty
Common Name (eg, your name or your servers hostname) []:xgrpc.com
Email Address []:2420309401qq.comopenssl req生成自签名证书-new指生成证书请求-sha256指使用sha256加密-key指定私钥文件-x509指输出证书-days 3650为有效期-out输出证书的文件名
[rootzsx keys]# ls
server.key server.pem
# 使用方式
# credentials.NewServerTLSFromFile(server.pem,server.key)上面的两个步骤是不带密码的可以生成带密码的这里只简单的列举命令具体的使用请参考下面SAN证书生
成
# 1、生成CA私钥(ca.key)
openssl genrsa -des3 -out ca.key 2048
# 2、生成CA证书签名请求(ca.csr)
openssl req -new -key ca.key -out ca.csr
# 该命令需要输入密码,如果不想输入命令简单使用可以先执行下面的这条命令,在执行该命令
# 这条命令会去掉密码
# openssl rsa -in ca.key -out ca.key
# 生成自签名CA证书(ca.cert)
openssl x509 -req -days 3650 -in ca.csr -signkey ca.key -out ca.crt
# 生成的ca.key和ca.crt就可以使用了
# credentials.NewServerTLSFromFile(ca.crt,ca.key)go1.15 版本开始废弃CommonName因此推荐使用SAN证书。
如果想兼容之前的方式需要设置环境变量 GODEBUG为 x509ignoreCN0。
否则将会运行报错
rpc error: code Unavailable desc connection error: desc transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs or temporarily enable Common Name matching with GODEBUGx509ignoreCN01.1.4 使用openssl生成SAN证书
SAN(Subject Alternative Name)是 SSL 标准 x509 中定义的一个扩展。使用了 SAN 字段的 SSL 证书可以扩
展此证书支持的域名使得一个证书可以支持多个不同域名的解析。
由于Golang 1.17以上强制使用SAN证书故需要在此进行生成。
1、创建一个cert目录用于保存证书和配置文件。
2、创建配置文件(openssl.cnf)并保存到cert目录下内容如下
[CA_default]
copy_extensions copy[req]
distinguished_name req_distinguished_name
x509_extensions v3_req
prompt no[req_distinguished_name]
# 国家
C CN
# 省份
ST Shenzhen
# 城市
L Shenzhen
# 组织
O Arvin
# 部门
OU Arvin
# 域名
CN test.example.com[v3_req]
basicConstraints CA:FALSE
keyUsage nonRepudiation,digitalSignature,keyEncipherment
subjectAltName alt_names[alt_names]
# 解析域名
DNS.1 *.test.example.com
# 可配置多个域名
DNS.2 *.example.com3、生成根证书rootCa
使用命令行工具进入到cert目录下并执行如下命令
# 生成私钥,密码可以输入123456
[rootzsx cert]# openssl genrsa -des3 -out ca.key 2048
Generating RSA private key, 2048 bit long modulus
..........................................
....
e is 65537 (0x10001)
Enter pass phrase for ca.key:123456
Verifying - Enter pass phrase for ca.key:123456# 使用私钥来签名证书
[rootzsx cert]# 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) [XX]:CN
State or Province Name (full name) []:Shenzhen
Locality Name (eg, city) [Default City]:Shenzhen
Organization Name (eg, company) [Default Company Ltd]:Arvin
Organizational Unit Name (eg, section) []:Arvin
Common Name (eg, your name or your servers hostname) []:test.example.com
Email Address []:2420309401qq.comPlease enter the following extra attributes
to be sent with your certificate request
A challenge password []: # 回车即可
An optional company name []: # 回车即可# 使用私钥证书来生成公钥
[rootzsx cert]# openssl x509 -req -days 365 -in ca.csr -signkey ca.key -out ca.crt
Signature ok
subject/CCN/STShenzhen/LShenzhen/OArvin/OUArvin/CNtest.example.com/emailAddress2420309401qq.com
Getting Private key
Enter pass phrase for ca.key:123456[rootzsx cert]# ls
ca.crt ca.csr ca.key openssl.cnf4、在cert目录下分别创建server、client目录它们用来保存服务器密钥与客户端密钥。
5、生成服务器密钥
使用命令行工具进入到cert目录下并执行如下命令
# 生成服务器私钥
[rootzsx cert]# openssl genpkey -algorithm RSA -out server/server.key
................
....# 使用私钥来签名证书
[rootzsx cert]# openssl req -new -nodes -key server/server.key -out server/server.csr -config openssl.cnf -extensions v3_req# 生成SAN证书
$ [rootzsx cert]# openssl x509 -req -in server/server.csr -out server/server.pem -CA ca.crt -CAkey ca.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req
Signature ok
subject/CCN/STShenzhen/LShenzhen/OArvin/OUArvin/CNtest.example.com
Getting CA Private Key
Enter pass phrase for ca.key:1234566、生成客户端密钥
使用命令行工具进入到cert目录下并执行如下命令
# 生成客户端私钥
[rootzsx cert]# openssl genpkey -algorithm RSA -out client/client.key
...
...# 使用私钥来签名证书
[rootzsx cert]# openssl req -new -nodes -key client/client.key -out client/client.csr -config openssl.cnf -extensions v3_req# 生成SAN证书
[rootzsx cert]# openssl x509 -req -in client/client.csr -out client/client.pem -CA ca.crt -CAkey ca.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req
Signature ok
subject/CCN/STShenzhen/LShenzhen/OArvin/OUArvin/CNtest.example.com
Getting CA Private Key
Enter pass phrase for ca.key:123456[rootzsx protoc]# tree tls_demo/
tls_demo/
└── cert├── ca.crt├── ca.csr├── ca.key├── ca.srl├── client│ ├── client.csr│ ├── client.key│ └── client.pem├── openssl.cnf└── server├── server.csr├── server.key└── server.pem3 directories, 11 files1.1.5 编写hello.proto
// 指定proto版本
syntax proto3;
// 指定包名
package hello;
option go_package./hello;// 定义Hello服务
service Hello {// 定义SayHello方法rpc SayHello(HelloRequest) returns (HelloReply) {}
}// HelloRequest 请求结构
message HelloRequest {string name 1;
}// HelloReply 响应结构
message HelloReply {string message 1;
}运行
[rootzsx tls_demo]# protoc --go_outpluginsgrpc:. hello.proto1.1.6 服务端server.go
package mainimport (contextfmtpb tls_demo/hellogoogle.golang.org/grpc// 引入grpc认证包google.golang.org/grpc/credentialsnetlog
)const (// Address gRPC服务地址Address 127.0.0.1:50052
)// 定义helloService并实现约定的接口
type helloService struct{}// HelloService Hello服务
var HelloService helloService{}// SayHello 实现Hello服务接口
func (h helloService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {resp : new(pb.HelloReply)resp.Message fmt.Sprintf(Hello %s., in.Name)return resp, nil
}func main() {log.Println(服务端启动!)listen, err : net.Listen(tcp, Address)if err ! nil {log.Fatalf(Failed to listen: %v, err)}// TLS认证creds, err : credentials.NewServerTLSFromFile(./cert/server/server.pem, ./cert/server/server.key)if err ! nil {log.Fatalf(Failed to generate credentials %v, err)}// 实例化grpc Server, 并开启TLS认证s : grpc.NewServer(grpc.Creds(creds))// 注册HelloServicepb.RegisterHelloServer(s, HelloService)log.Println(Listen on Address with TLS)s.Serve(listen)
}credentials.NewServerTLSFromFile从输入证书文件和密钥文件为服务端构造TLS凭证 grpc.Creds返回一个ServerOption用于设置服务器连接的凭证。
运行
[rootzsx tls_demo]# go run server.go
2023/02/11 09:55:59 服务端启动!
2023/02/11 09:55:59 Listen on 127.0.0.1:50052 with TLS服务端在实例化grpc Server时可配置多种选项TLS认证是其中之一。
1.1.7 客户端client.go
package mainimport (context// 引入proto包pb tls_demo/hellogoogle.golang.org/grpc// 引入grpc认证包google.golang.org/grpc/credentialslog
)const (// Address gRPC服务地址Address 127.0.0.1:50052
)func main() {log.Println(客户端连接!)// TLS连接creds, err : credentials.NewClientTLSFromFile(./cert/server/server.pem, test.example.com)if err ! nil {log.Fatalf(Failed to create TLS credentials %v, err)}conn, err : grpc.Dial(Address, grpc.WithTransportCredentials(creds))if err ! nil {log.Fatalln(err:, err)}defer conn.Close()// 初始化客户端c : pb.NewHelloClient(conn)// 调用方法req : pb.HelloRequest{Name: gRPC}res, err : c.SayHello(context.Background(), req)if err ! nil {log.Fatalln(err)}log.Println(res.Message)
}credentials.NewClientTLSFromFile从输入的证书文件中为客户端构造TLS凭证。 grpc.WithTransportCredentials配置连接级别的安全凭证例如TLS/SSL返回一个DialOption 用于连接服务器。
运行
[rootzsx tls_demo]# go run client.go
2023/02/11 10:00:11 客户端连接!
2023/02/11 10:00:11 Hello gRPC.客户端添加TLS认证的方式和服务端类似在创建连接Dial时同样可以配置多种选项后面的示例中会看到更
多的选项。
# 项目结构
[rootzsx protoc]# tree tls_demo/
tls_demo/
├── cert
│ ├── ca.crt
│ ├── ca.csr
│ ├── ca.key
│ ├── ca.srl
│ ├── client
│ │ ├── client.csr
│ │ ├── client.key
│ │ └── client.pem
│ ├── openssl.cnf
│ └── server
│ ├── server.csr
│ ├── server.key
│ └── server.pem
├── client.go
├── go.mod
├── go.sum
├── hello
│ └── hello.pb.go
├── hello.proto
└── server.go4 directories, 17 files1.2 Token认证
到这里已经完成TLS证书认证了gRPC传输不再是明文传输。此外添加自定义的验证方法能使gRPC相对更安
全。下面以TLS Token认证为例介绍gRPC如何添加自定义验证方法。
1.2.1 Token认证原理
客户端发请求时添加Token到上下文context.Context中服务器接收到请求先从上下文中获取Token验
证验证通过才进行下一步处理。
客户端请求添加Token到上下文中
conn, err : grpc.Dial(Address, grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(token))type PerRPCCredentials interface {GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)RequireTransportSecurity() bool
}gRPC 中默认定义了 PerRPCCredentials是提供用于自定义认证的接口它的作用是将所需的安全认证信息添
加到每个RPC方法的上下文中。其包含 2 个方法
GetRequestMetadata获取当前请求认证所需的元数据。RequireTransportSecurity是否需要基于 TLS 认证进行安全传输。
package authimport (context
)// Token token认证
type Token struct {AppID stringAppSecret string
}// GetRequestMetadata 获取当前请求认证所需的元数据
func (t *Token) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {return map[string]string{app_id: t.AppID, app_secret: t.AppSecret}, nil
}// RequireTransportSecurity 是否需要基于 TLS 认证进行安全传输
func (t *Token) RequireTransportSecurity() bool {return true
}//构建Token
token : auth.Token{AppID: grpc_token,AppSecret: 123456,
}
// 连接服务器
conn, err : grpc.Dial(Address, grpc.WithTransportCredentials(creds),grpc.WithPerRPCCredentials(token))1.2.2 服务端tserver.go
package mainimport (fmtpb tls_demo/hellogolang.org/x/net/contextgoogle.golang.org/grpcgoogle.golang.org/grpc/codesgoogle.golang.org/grpc/metadata// 引入grpc认证包google.golang.org/grpc/credentialslognet
)const (// Address gRPC服务地址Address 127.0.0.1:50052
)// 定义helloService并实现约定的接口
type helloService struct{}// HelloService ...
var HelloService helloService{}// SayHello 实现Hello服务接口
func (h helloService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {// 解析metada中的信息并验证md, ok : metadata.FromIncomingContext(ctx)if !ok {return nil, grpc.Errorf(codes.Unauthenticated, 无Token认证信息)}// metadata: map[:authority:[test.example.com] appid:[101010] appkey:[I am key] content-type:[application/grpc] user-agent:[grpc-go/1.53.0]]log.Println(metadata: ,md)var (appid stringappkey 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 nil, grpc.Errorf(codes.Unauthenticated, Token认证信息无效: appid%s, appkey%s, appid, appkey)}resp : new(pb.HelloReply)resp.Message fmt.Sprintf(Hello %s.\nToken info: appid%s,appkey%s, in.Name, appid, appkey)return resp, nil
}func main() {listen, err : net.Listen(tcp, Address)if err ! nil {log.Fatalf(failed to listen: %v, err)}// TLS认证creds, err : credentials.NewServerTLSFromFile(./cert/server/server.pem, ./cert/server/server.key)if err ! nil {log.Fatalf(Failed to generate credentials %v, err)}// 实例化grpc Server, 并开启TLS认证s : grpc.NewServer(grpc.Creds(creds))// 注册HelloServicepb.RegisterHelloServer(s, HelloService)log.Println(Listen on Address with TLS Token)s.Serve(listen)
}metadata.FromIncomingContext从上下文中获取元数据
运行
[rootzsx tls_demo]# go run tserver.go
2023/02/11 10:18:05 Listen on 127.0.0.1:50052 with TLS Token1.2.3 客户端tclient.go
这里我们定义了一个customCredential结构并实现了两个方法GetRequestMetadata和
RequireTransportSecurity。这是gRPC提供的自定义认证方式每次RPC调用都会传输认证信息。
customCredential其实是实现了grpc/credential包内的PerRPCCredentials接口。每次调用token信息会
通过请求的metadata传输到服务端。下面具体看一下服务端如何获取metadata中的信息。
package mainimport (context// 引入proto包pb tls_demo/hellogoogle.golang.org/grpc// 引入grpc认证包google.golang.org/grpc/credentialslog
)const (// Address gRPC服务地址Address 127.0.0.1:50052// OpenTLS 是否开启TLS认证OpenTLS true
)// customCredential 自定义认证
type customCredential struct{}// GetRequestMetadata 实现自定义认证接口
func (c customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {return map[string]string{appid: 101010,appkey: I am key,}, nil
}// RequireTransportSecurity 自定义认证是否开启TLS
func (c customCredential) RequireTransportSecurity() bool {return OpenTLS
}func main() {var err errorvar opts []grpc.DialOptionif OpenTLS {// TLS连接creds, err : credentials.NewClientTLSFromFile(./cert/server/server.pem, test.example.com)if err ! nil {log.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)))conn, err : grpc.Dial(Address, opts...)if err ! nil {log.Fatalln(err)}defer conn.Close()// 初始化客户端c : pb.NewHelloClient(conn)// 调用方法req : pb.HelloRequest{Name: gRPC}res, err : c.SayHello(context.Background(), req)if err ! nil {log.Fatalln(err)}log.Println(res.Message)
}运行结果
[rootzsx tls_demo]# go run tclient.go
2023/02/11 10:40:21 Hello gRPC.
Token info: appid101010,appkeyI am key修改appkey的值为i am key验证认证失败结果
[rootzsx tls_demo]# go run tclient.go
2023/02/11 10:40:59 rpc error: code Unauthenticated desc Token认证信息无效: appid101010, appkeyi am key
exit status 1# 项目结构
$ tree tls_demo/
tls_demo/
├── cert
│ ├── ca.crt
│ ├── ca.csr
│ ├── ca.key
│ ├── ca.srl
│ ├── client
│ │ ├── client.csr
│ │ ├── client.key
│ │ └── client.pem
│ ├── openssl.cnf
│ └── server
│ ├── server.csr
│ ├── server.key
│ └── server.pem
├── client.go
├── go.mod
├── go.sum
├── hello
│ └── hello.pb.go
├── hello.proto
├── server.go
├── tclient.go
└── tserver.go1.3 JWT认证
1.3.1 proto编写和编译
syntax proto3;
package api;
option go_package ./api;api;service Ping {rpc Login (LoginRequest) returns (LoginReply) {}rpc SayHello(PingMessage) returns (PingMessage) {}
}message LoginRequest {string username 1;string password 2;
}message LoginReply {string status 1;string token 2;
}message PingMessage {string greeting 1;
}$ protoc --go_outpluginsgrpc:. api/api.proto1.3.2 jwt工具类
/api/authtoken.go文件的内容如下
package apiimport (contextfmtgithub.com/dgrijalva/jwt-gogoogle.golang.org/grpc/metadatatime
)// 生成token
func CreateToken(userName string) (tokenString string) {token : jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{iss: lora-app-server,aud: lora-app-server,nbf: time.Now().Unix(),exp: time.Now().Add(time.Hour).Unix(),sub: user,username: userName,})tokenString, err : token.SignedString([]byte(verysecret))if err ! nil {panic(err)}return tokenString
}// AuthToekn自定义认证
type AuthToekn struct {Token string
}// AuthToekn实现了该方法,相当于实现了PerRPCCredentials接口
func (c AuthToekn) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {return map[string]string{authorization: c.Token,}, nil
}// AuthToekn实现了该方法,相当于实现了PerRPCCredentials接口
// 是否验证证书
func (c AuthToekn) RequireTransportSecurity() bool {return false
}// Claims defines the struct containing the token claims.
type Claims struct {jwt.StandardClaims// Username defines the identity of the user.Username string json:username
}// Step1. 从 context 的 metadata 中取出 token
func getTokenFromContext(ctx context.Context) (string, error) {md, ok : metadata.FromIncomingContext(ctx)if !ok {return , fmt.Errorf(ErrNoMetadataInContext)}// md 的类型是 type MD map[string][]stringtoken, ok : md[authorization]if !ok || len(token) 0 {return , fmt.Errorf(ErrNoAuthorizationInMetadata)}// 因此token 是一个字符串数组我们只用了 token[0]return token[0], nil
}func CheckAuth(ctx context.Context) (username string) {tokenStr, err : getTokenFromContext(ctx)if err ! nil {panic(get token from context error)}var clientClaims Claimstoken, err : jwt.ParseWithClaims(tokenStr, clientClaims, func(token *jwt.Token) (interface{}, error) {if token.Header[alg] ! HS256 {panic(ErrInvalidAlgorithm)}return []byte(verysecret), nil})if err ! nil {panic(jwt parse error)}if !token.Valid {panic(ErrInvalidToken)}fmt.Println(parse token is: , token)return clientClaims.Username
}1.3.3 逻辑处理
api/handler.go文件的内容如下
package apiimport (fmtgolang.org/x/net/context
)// Server represents the gRPC server
type Server struct {
}// 登录处理
func (s *Server) Login(ctx context.Context, in *LoginRequest) (*LoginReply, error) {fmt.Println(Loginrequest: , in.Username)if in.Username gavin in.Password gavin {// 创建jwttokenString : CreateToken(in.Username)fmt.Println(generate token is: , tokenString)return LoginReply{Status: 200, Token: tokenString}, nil} else {return LoginReply{Status: 403, Token: }, nil}
}// SayHello generates response to a Ping request
func (s *Server) SayHello(ctx context.Context, in *PingMessage) (*PingMessage, error) {msg : bar// 逻辑处理前需要验证jwtuserName : CheckAuth(ctx)msg userNamereturn PingMessage{Greeting: msg}, nil
}1.3.4 服务端
package mainimport (demo/apifmtgoogle.golang.org/grpclognet
)func main() {lis, err : net.Listen(tcp, fmt.Sprintf(:%d, 7777))if err ! nil {log.Fatalf(failed to listen: %v, err)}s : api.Server{}grpcServer : grpc.NewServer()api.RegisterPingServer(grpcServer, s)if err : grpcServer.Serve(lis); err ! nil {log.Fatalf(failed to serve: %s, err)}
}$ go run server.go
Loginrequest: gavin
generate token is: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJsb3JhLWFwcC1zZXJ2ZXIiLCJleHAiOjE2NzY2MDE3MTgsImlzcyI6ImxvcmEtYXBwLXNlcnZlciIsIm5iZiI6MTY3NjU5ODExOCwic3ViIjoidXNlciIsInVzZXJuYW1lIjoiZ2F2aW4ifQ.IoAmUq2Vm90I5dWEgNEGc22c7YspVJN4cLeOWS16gaA
parse token is: {eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJsb3JhLWFwcC1zZXJ2ZXIiLCJleHAiOjE2NzY2MDE3MTgsImlzcyI6ImxvcmEtYXBwLXNlcnZlciIsIm5iZiI6MTY3NjU5ODExOCwic3ViIjoidXNlciIsInVzZXJuYW1lIjoiZ2F2aW4ifQ.IoAmUq2Vm90I5dWEgNEGc22c7YspVJN4cLeOWS16gaA 0xc00000e600 map[alg:HS256 typ:JWT] 0xc0001684d0 IoAmUq2Vm90I5dWEgNEGc22c7YspVJN4cLeOWS16gaA true}1.3.5 客户端
package mainimport (contextdemo/apifmtgoogle.golang.org/grpclog
)func main() {var conn *grpc.ClientConnconn, err : grpc.Dial(:7777, grpc.WithInsecure())if err ! nil {log.Fatalf(did not connect: %s, err)}defer conn.Close()c : api.NewPingClient(conn)loginReply, err : c.Login(context.Background(), api.LoginRequest{Username: gavin, Password: gavin})if err ! nil {log.Fatalf(Error when calling SayHello: %s, err)}fmt.Println(Login Reply:, loginReply)//Call SayHellorequestToken : new(api.AuthToekn)requestToken.Token loginReply.Tokenconn, err grpc.Dial(:7777, grpc.WithInsecure(), grpc.WithPerRPCCredentials(requestToken))if err ! nil {log.Fatalf(did not connect: %s, err)}defer conn.Close()c api.NewPingClient(conn)helloreply, err : c.SayHello(context.Background(), api.PingMessage{Greeting: foo})if err ! nil {log.Fatalf(Error when calling SayHello: %s, err)}log.Printf(Response from server: %s, helloreply.Greeting)
}$ go run client.go
Login Reply: status:200 token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJsb3JhLWFwcC1zZXJ2ZXIiLCJleHAiOjE2NzY2MDE3MTgsImlzcyI6ImxvcmEtYXBwLXNlcnZlciIsIm5iZiI6MTY3NjU5ODExOCwic3ViIjoidXNlciIsInVzZXJuYW1lIjoiZ2F2aW4ifQ.IoAmUq2Vm90I5dWEgNEGc22c7YspVJN4cLeOWS16gaA
2023/02/17 09:41:58 Response from server: bar gavin# 项目结构
$ tree demo/
demo/
├── api
│ ├── api.pb.go
│ ├── api.proto
│ ├── authtoken.go
│ └── handler.go
├── client1.go
├── echo.proto
├── go.mod
├── go.sum
├── proto
│ └── echo.pb.go
└── server1.go2 directories, 10 filesgoogle.golang.org/grpc/credentials/oauth包已实现了用于Google API的oauth和jwt验证的方法使用方
法可以参考[官方文档]
https://github.com/grpc/grpc-go/blob/master/Documentation/grpc-auth-support.md
在实际应用中我们可以根据自己的业务需求实现合适的验证方式。
1.4 oauth认证
1.4.1 proto编写和编译
syntax proto3;
option go_package ./proto;
package proto;message EchoRequest {string message 1;
}message EchoResponse {string message 1;
}service Echo {rpc UnaryEcho(EchoRequest) returns (EchoResponse) {}rpc ServerStreamingEcho(EchoRequest) returns (stream EchoResponse) {}rpc ClientStreamingEcho(stream EchoRequest) returns (EchoResponse) {}rpc BidirectionalStreamingEcho(stream EchoRequest) returns (stream EchoResponse) {}
}$ protoc --go_outpluginsgrpc:. echo.proto1.4.2 服务端编写
package mainimport (contextcrypto/tlspb demo/proto/protoflagfmtgoogle.golang.org/grpcgoogle.golang.org/grpc/codesgoogle.golang.org/grpc/credentialsgoogle.golang.org/grpc/metadatagoogle.golang.org/grpc/statuslognetstrings
)var (errMissingMetadata status.Errorf(codes.InvalidArgument, missing metadata)errInvalidToken status.Errorf(codes.Unauthenticated, invalid token)
)var port flag.Int(port, 50051, the port to serve on)func main() {flag.Parse()fmt.Printf(server starting on port %d...\n, *port)cert, err : tls.LoadX509KeyPair(./cert/server/server.pem, ./cert/server/server.key)if err ! nil {log.Fatalf(failed to load key pair: %s, err)}opts : []grpc.ServerOption{grpc.UnaryInterceptor(ensureValidToken),grpc.Creds(credentials.NewServerTLSFromCert(cert)),}s : grpc.NewServer(opts...)pb.RegisterEchoServer(s, ecServer{})lis, err : net.Listen(tcp, fmt.Sprintf(:%d, *port))if err ! nil {log.Fatalf(failed to listen: %v, err)}if err : s.Serve(lis); err ! nil {log.Fatalf(failed to serve: %v, err)}
}type ecServer struct {pb.UnimplementedEchoServer
}func (s *ecServer) UnaryEcho(ctx context.Context, req *pb.EchoRequest) (*pb.EchoResponse, error) {return pb.EchoResponse{Message: req.Message}, nil
}func valid(authorization []string) bool {if len(authorization) 1 {return false}token : strings.TrimPrefix(authorization[0], Bearer )return token some-secret-token
}func ensureValidToken(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {md, ok : metadata.FromIncomingContext(ctx)if !ok {return nil, errMissingMetadata}if !valid(md[authorization]) {return nil, errInvalidToken}return handler(ctx, req)
}[rootzsx demo]# go run server.go
server starting on port 50051...1.4.3 客户端编写
package mainimport (contextecpb demo/proto/protoflagfmtgolang.org/x/oauth2google.golang.org/grpcgoogle.golang.org/grpc/credentialsgoogle.golang.org/grpc/credentials/oauthlogtime
)var addr flag.String(addr, localhost:50051, the address to connect to)func callUnaryEcho(client ecpb.EchoClient, message string) {ctx, cancel : context.WithTimeout(context.Background(), 10*time.Second)defer cancel()resp, err : client.UnaryEcho(ctx, ecpb.EchoRequest{Message: message})if err ! nil {log.Fatalf(client.UnaryEcho(_) _, %v: , err)}fmt.Println(UnaryEcho: , resp.Message)
}func main() {flag.Parse()perRPC : oauth.TokenSource{TokenSource: oauth2.StaticTokenSource(fetchToken())}creds, err : credentials.NewClientTLSFromFile(./cert/server/server.pem, x.test.example.com)if err ! nil {log.Fatalf(failed to load credentials: %v, err)}opts : []grpc.DialOption{grpc.WithPerRPCCredentials(perRPC),grpc.WithTransportCredentials(creds),}conn, err : grpc.Dial(*addr, opts...)if err ! nil {log.Fatalf(did not connect: %v, err)}defer conn.Close()rgc : ecpb.NewEchoClient(conn)callUnaryEcho(rgc, hello world)
}func fetchToken() *oauth2.Token {return oauth2.Token{AccessToken: some-secret-token,}
}[rootzsx demo]# go run client.go
UnaryEcho: hello world# 项目结构
[rootzsx protoc]# tree demo/
demo/
├── cert
│ ├── ca.crt
│ ├── ca.csr
│ ├── ca.key
│ ├── ca.srl
│ ├── client
│ │ ├── client.csr
│ │ ├── client.key
│ │ └── client.pem
│ ├── openssl.cnf
│ └── server
│ ├── server.csr
│ ├── server.key
│ └── server.pem
├── client.go
├── go.mod
├── go.sum
├── proto
│ ├── echo.proto
│ └── proto
│ └── echo.pb.go
└── server.go5 directories, 17 files参考地址https://godoc.org/google.golang.org/grpc/credentials/oauth