前面篇章的gRPC都是明文传输的,容易被篡改数据,本章将介绍如何为gRPC添加安全机制。
gRPC默认内置了两种认证方式:
SSL/TLS
认证方式
基于Token
的认证方式
同时,gRPC提供了接口用于扩展自定义认证方式。
TLS(Transport Layer Security,安全传输层),TLS是建立在传输层TCP协议之上的协议,服务于应用层,它的前
身是SSL(Secure Socket Layer,安全套接字层),它实现了将应用层的报文进行加密后再交由TCP进行传输的功
能。
TLS协议主要解决如下三个网络安全问题。
保密(message privacy),保密通过加密encryption实现,所有信息都加密传输,第三方无法嗅探;
完整性(message integrity),通过MAC校验机制,一旦被篡改,通信双方会立刻发现;
认证(mutual authentication),双方认证,双方都可以配备证书,防止身份被冒充;
这里实现TLS认证机制,首先需要准备证书,在tls_demo
目录新建keys
目录用于存放证书文件。
openSSL下载安装地址:http://slproweb.com/products/Win32OpenSSL.html
(1)、制作私钥 (server.key)
# 生成RSA私钥
[root@zsx 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
[root@zsx keys]# ls
server.key
(2)、自签名公钥(server.pem)
会生成serve.pem
,其中Common Name
也就是域名,我填的是xgrpc.com
。
[root@zsx 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 server's hostname) []:xgrpc.com
Email Address []:[email protected]
openssl req
生成自签名证书-new
指生成证书请求-sha256
指使用sha256加密-key
指定私钥文件-x509
指输出证书-days 3650
为有效期-out
输出证书的文件名[root@zsx 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
为 x509ignoreCN=0
。
否则将会运行报错:
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 GODEBUG=x509ignoreCN=0"
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.com
3、生成根证书(rootCa
)
使用命令行工具,进入到cert
目录下,并执行如下命令:
# 生成私钥,密码可以输入123456
[root@zsx 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
# 使用私钥来签名证书
[root@zsx 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 server's hostname) []:test.example.com
Email Address []:[email protected]
Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []: # 回车即可
An optional company name []: # 回车即可
# 使用私钥+证书来生成公钥
[root@zsx cert]# openssl x509 -req -days 365 -in ca.csr -signkey ca.key -out ca.crt
Signature ok
subject=/C=CN/ST=Shenzhen/L=Shenzhen/O=Arvin/OU=Arvin/CN=test.example.com/emailAddress=2420309401@qq.com
Getting Private key
Enter pass phrase for ca.key:123456
[root@zsx cert]# ls
ca.crt ca.csr ca.key openssl.cnf
4、在cert
目录下,分别创建server
、client
目录,它们用来保存服务器密钥与客户端密钥。
5、生成服务器密钥
使用命令行工具,进入到cert
目录下,并执行如下命令:
# 生成服务器私钥
[root@zsx cert]# openssl genpkey -algorithm RSA -out server/server.key
................++++++
....++++++
# 使用私钥来签名证书
[root@zsx cert]# openssl req -new -nodes -key server/server.key -out server/server.csr -config openssl.cnf -extensions v3_req
# 生成SAN证书
$ [root@zsx 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=/C=CN/ST=Shenzhen/L=Shenzhen/O=Arvin/OU=Arvin/CN=test.example.com
Getting CA Private Key
Enter pass phrase for ca.key:123456
6、生成客户端密钥
使用命令行工具,进入到cert
目录下,并执行如下命令:
# 生成客户端私钥
[root@zsx cert]# openssl genpkey -algorithm RSA -out client/client.key
...++++++
...++++++
# 使用私钥来签名证书
[root@zsx cert]# openssl req -new -nodes -key client/client.key -out client/client.csr -config openssl.cnf -extensions v3_req
# 生成SAN证书
[root@zsx 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=/C=CN/ST=Shenzhen/L=Shenzhen/O=Arvin/OU=Arvin/CN=test.example.com
Getting CA Private Key
Enter pass phrase for ca.key:123456
[root@zsx 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
3 directories, 11 files
// 指定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;
}
运行:
[root@zsx tls_demo]# protoc --go_out=plugins=grpc:. hello.proto
package main
import (
"context"
"fmt"
pb "tls_demo/hello"
"google.golang.org/grpc"
// 引入grpc认证包
"google.golang.org/grpc/credentials"
"net"
"log"
)
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))
// 注册HelloService
pb.RegisterHelloServer(s, HelloService)
log.Println("Listen on " + Address + " with TLS")
s.Serve(listen)
}
credentials.NewServerTLSFromFile
:从输入证书文件和密钥文件为服务端构造TLS凭证
grpc.Creds
:返回一个ServerOption,用于设置服务器连接的凭证。
运行:
[root@zsx 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
认证是其中之一。
package main
import (
"context"
// 引入proto包
pb "tls_demo/hello"
"google.golang.org/grpc"
// 引入grpc认证包
"google.golang.org/grpc/credentials"
"log"
)
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,
用于连接服务器。
运行:
[root@zsx tls_demo]# go run client.go
2023/02/11 10:00:11 客户端连接!
2023/02/11 10:00:11 Hello gRPC.
客户端添加TLS认证的方式和服务端类似,在创建连接Dial
时,同样可以配置多种选项,后面的示例中会看到更
多的选项。
# 项目结构
[root@zsx 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.go
4 directories, 17 files
到这里,已经完成TLS证书认证了,gRPC传输不再是明文传输。此外,添加自定义的验证方法能使gRPC相对更安
全。下面以TLS + Token
认证为例,介绍gRPC如何添加自定义验证方法
。
客户端发请求时,添加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 auth
import (
"context"
)
// Token token认证
type Token struct {
AppID string
AppSecret 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))
package main
import (
"fmt"
pb "tls_demo/hello"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
// 引入grpc认证包
"google.golang.org/grpc/credentials"
"log"
"net"
)
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 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 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))
// 注册HelloService
pb.RegisterHelloServer(s, HelloService)
log.Println("Listen on " + Address + " with TLS + Token")
s.Serve(listen)
}
metadata.FromIncomingContext
:从上下文中获取元数据运行:
[root@zsx tls_demo]# go run tserver.go
2023/02/11 10:18:05 Listen on 127.0.0.1:50052 with TLS + Token
这里我们定义了一个customCredential
结构,并实现了两个方法GetRequestMetadata
和
RequireTransportSecurity
。这是gRPC提供的自定义认证方式,每次RPC调用都会传输认证信息。
customCredential
其实是实现了grpc/credential
包内的PerRPCCredentials
接口。每次调用,token信息会
通过请求的metadata传输到服务端。下面具体看一下服务端如何获取metadata中的信息。
package main
import (
"context"
// 引入proto包
pb "tls_demo/hello"
"google.golang.org/grpc"
// 引入grpc认证包
"google.golang.org/grpc/credentials"
"log"
)
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 error
var opts []grpc.DialOption
if 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)
}
运行结果:
[root@zsx tls_demo]# go run tclient.go
2023/02/11 10:40:21 Hello gRPC.
Token info: appid=101010,appkey=I am key
修改appkey
的值为i am key
,验证认证失败结果:
[root@zsx tls_demo]# go run tclient.go
2023/02/11 10:40:59 rpc error: code = Unauthenticated desc = Token认证信息无效: appid=101010, appkey=i 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.go
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_out=plugins=grpc:. api/api.proto
/api/authtoken.go
文件的内容如下:
package api
import (
"context"
"fmt"
"github.com/dgrijalva/jwt-go"
"google.golang.org/grpc/metadata"
"time"
)
// 生成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][]string
token, 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 Claims
token, 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
}
api/handler.go
文件的内容如下:
package api
import (
"fmt"
"golang.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" {
// 创建jwt
tokenString := 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"
// 逻辑处理前需要验证jwt
userName := CheckAuth(ctx)
msg += " " + userName
return &PingMessage{Greeting: msg}, nil
}
package main
import (
"demo/api"
"fmt"
"google.golang.org/grpc"
"log"
"net"
)
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}
package main
import (
"context"
"demo/api"
"fmt"
"google.golang.org/grpc"
"log"
)
func main() {
var conn *grpc.ClientConn
conn, 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 SayHello
requestToken := new(api.AuthToekn)
requestToken.Token = loginReply.Token
conn, 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.go
2 directories, 10 files
google.golang.org/grpc/credentials/oauth
包已实现了用于Google API的oauth和jwt验证的方法,使用方
法可以参考[官方文档]:
https://github.com/grpc/grpc-go/blob/master/Documentation/grpc-auth-support.md
在实际应用中,我们可以根据自己的业务需求实现合适的验证方式。
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_out=plugins=grpc:. echo.proto
package main
import (
"context"
"crypto/tls"
pb "demo/proto/proto"
"flag"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"log"
"net"
"strings"
)
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)
}
[root@zsx demo]# go run server.go
server starting on port 50051...
package main
import (
"context"
ecpb "demo/proto/proto"
"flag"
"fmt"
"golang.org/x/oauth2"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/oauth"
"log"
"time"
)
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",
}
}
[root@zsx demo]# go run client.go
UnaryEcho: hello world
# 项目结构
[root@zsx 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.go
5 directories, 17 files
参考地址:https://godoc.org/google.golang.org/grpc/credentials/oauth