当前位置: 首页 > news >正文

gRPC之gRPC认证

1、gRPC认证

前面篇章的gRPC都是明文传输的,容易被篡改数据,本章将介绍如何为gRPC添加安全机制。

gRPC默认内置了两种认证方式:

  • SSL/TLS认证方式

  • 基于Token的认证方式

同时,gRPC提供了接口用于扩展自定义认证方式。

1.1 TLS认证

1.1.1 什么是TLS

TLS(Transport Layer Security,安全传输层),TLS是建立在传输层TCP协议之上的协议,服务于应用层,它的前

身是SSL(Secure 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私钥
[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 []:2420309401@qq.com
  • 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证书。

如果想兼容之前的方式,需要设置环境变量 GODEBUGx509ignoreCN=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"

1.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.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 []:2420309401@qq.comPlease 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目录下,分别创建serverclient目录,它们用来保存服务器密钥与客户端密钥。

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.pem3 directories, 11 files

1.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;
}

运行:

[root@zsx tls_demo]# protoc --go_out=plugins=grpc:. hello.proto

1.1.6 服务端server.go

package mainimport ("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))// 注册HelloServicepb.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认证是其中之一。

1.1.7 客户端client.go

package mainimport ("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.go4 directories, 17 files

1.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 ("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  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:从上下文中获取元数据

运行:

[root@zsx tls_demo]# go run tserver.go
2023/02/11 10:18:05 Listen on 127.0.0.1:50052 with TLS + Token

1.2.3 客户端tclient.go

这里我们定义了一个customCredential结构,并实现了两个方法GetRequestMetadata

RequireTransportSecurity。这是gRPC提供的自定义认证方式,每次RPC调用都会传输认证信息。

customCredential其实是实现了grpc/credential包内的PerRPCCredentials接口。每次调用,token信息会

通过请求的metadata传输到服务端。下面具体看一下服务端如何获取metadata中的信息。

package mainimport ("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 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)
}

运行结果:

[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

1.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_out=plugins=grpc:. api/api.proto

1.3.2 jwt工具类

/api/authtoken.go文件的内容如下:

package apiimport ("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][]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 ("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" {// 创建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/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}

1.3.5 客户端

package mainimport ("context""demo/api""fmt""google.golang.org/grpc""log"
)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 files

google.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_out=plugins=grpc:. echo.proto

1.4.2 服务端编写

package mainimport ("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...

1.4.3 客户端编写

package mainimport ("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.go5 directories, 17 files

参考地址:https://godoc.org/google.golang.org/grpc/credentials/oauth

相关文章:

gRPC之gRPC认证

1、gRPC认证 前面篇章的gRPC都是明文传输的&#xff0c;容易被篡改数据&#xff0c;本章将介绍如何为gRPC添加安全机制。 gRPC默认内置了两种认证方式&#xff1a; SSL/TLS认证方式 基于Token的认证方式 同时&#xff0c;gRPC提供了接口用于扩展自定义认证方式。 1.1 TLS…...

简易虚拟培训系统-UI控件的应用3

目录 Button组件的组成 Button组件方法1-在Button组件中设置OnClick()回调 Button组件方法2-在脚本中添加Button类的监听 上一篇使用了文件流读取硬盘数据并显示在Text组件中&#xff0c;本篇增加使用按钮来控制显示哪一篇文字信息。 Button组件的组成 1. 新建Button&#…...

语言模型(language model)

文章目录 引言1. 什么是语言模型2. 语言模型的主要用途2.1 言模型-语音识别2.2 语言模型-手写识别2.3 语言模型-输入法 3. 语言模型的分类4. N-gram语言模型4.1 N-gram语言模型-平滑方法4.2 ngram代码4.3 语言模型的评价指标4.4 两类语言模型的对比 5. 神经网络语言模型6. 语言…...

【3.Vue子组件调用父组件方法】

1.概述 使用组件建抛出事件的方式来调用父组件的方法&#xff0c;不直接用this.$parent.function的方法&#xff0c;当然这种方式是可以的。 2.代码实现 2.1 父组件代码 父组件写一个方法给子组件调用 // 设备点击事件// equipId:设备id// leftValue:left值// topValue:top…...

算法系列-876-求链表的中间节点

求链表中间节点&#xff0c;如果有两个中间节点取后面那个 链表定义 // lc codestart /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val) { this.val val; }* ListNode(…...

h5 ws 客户端 监听ws服务器广播的信息

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>AI智能写作</title><!-- Bootstrap CSS --><meta charset"utf-8"><meta name"viewport" content"widt…...

网络基础之重中之重

目录 IP协议 ​编辑 基本概念&#xff1a; 协议头格式&#xff1a; ​编辑 网段划分 DHCP &#xff1a; CIDR&#xff1a; 特殊的IP地址&#xff1a; IP地址的数量限制&#xff1a; 私有IP和公网IP 路由 路由的过程&#xff1a; 数据链路层 认识以太网&#x…...

HarmonyOS应用开发者-----基础认证试题及答案

HarmonyOS应用开发者基础认证试题及答案 试题会不定时刷新,本试题仅供大家学习参考 【判断题】 2.5/2.5 所有使用@Component修饰的自定义组件都支持onPageShow,onBackPress和onPageHide生命周期函数。 正确(True)错误(False) 回答正确【判断题】 2.5/2.5 在Column和Row容器组…...

C++:string并非以0作为结束符,c_str和data的返回却包含结束符0

C语言中使用char数组保存字符串时,是以字符为0或者\0作为字符串的结束符标志的。 所以一个char str[10]的数组只能合法的保存9个字符(因为最后还要加一个结束符)。 #include <cstring> #include <iostream>using namespace std;int main() {char str[10] ="…...

ChatGPT插件的优缺点

虽然西弗吉尼亚大学的研究人员看到了最新的官方ChatGPT插件——名为“代码解释器”&#xff08; Code Interpreter&#xff09;的教育应用潜力&#xff0c;但他们也发现&#xff0c;对于使用计算方法处理针对癌症和遗传疾病的定向治疗的生物数据的科学家来说&#xff0c;这款插…...

北京985学校,交叉学科考英一数三408

北京师范大学(B) 考研难度&#xff08;☆☆☆&#xff09; 内容&#xff1a;23考情概况&#xff08;拟录取和复试分析&#xff09;、院校概况、23专业目录、23复试详情、各专业考情分析、各科目考情分析。 正文1096字&#xff0c;预计阅读&#xff1a;3分钟 2023考情概况 北…...

ChatGPT 总结前端HTML, JS, Echarts都包含哪些内容

AIGC ChatGPT ,BI商业智能, 可视化Tableau, PowerBI, FineReport, 数据库Mysql Oracle, Office, Python ,ETL Excel 2021 实操,函数,图表,大屏可视化 案例实战 http://t.csdn.cn/zBytu...

企业架构LNMP学习笔记1

项目开发流程&#xff1a; 公司老板或者产品经理&#xff0c;根据市场调查&#xff0c;决定开发一整套互联网产品。 互动社交电商用户论坛&#xff08;BBS&#xff09; 产品决策 &#xff08;老板产品UI设计&#xff09; 业务实施、代码开发 程序开发人员 前端开发&#x…...

【位运算】leetcode371:两整数之和

一.题目描述 两整数之和 二.思路分析 题目要求我们实现两整数相加&#xff0c;但是不能使用加号&#xff0c;应该立马想到是用位运算来解决问题。之前说过&#xff0c;异或就是“无进位相加”&#xff0c;故本题可以先将两数异或&#xff0c;然后想办法让得到的结果进位即可。…...

【爬虫小知识】如何利用爬虫爬网页——python爬虫

前言 网络时代的到来&#xff0c;给我们提供了海量的信息资源&#xff0c;但是&#xff0c;想要获取这些信息&#xff0c;手动一个一个网页进行查找&#xff0c;无疑是一项繁琐且效率低下的工作。这时&#xff0c;爬虫技术的出现&#xff0c;为我们提供了一种高效的方式去获取…...

什么是跨域问题 ?Spring MVC 如何解决跨域问题 ?Spring Boot 如何解决跨域问题 ?

目录 1. 什么是跨域问题 &#xff1f; 2. Spring MVC 如何解决跨域问题 &#xff1f; 3. Spring Boot 如何解决跨域问题 &#xff1f; 1. 什么是跨域问题 &#xff1f; 跨域问题指的是不同站点之间&#xff0c;使用 ajax 无法相互调用的问题。 跨域问题的 3 种情况&#x…...

线性代数的学习和整理17:向量空间的基,自然基,基变换等(未完成)

目录 3 向量空间的基&#xff1a;矩阵的基础/轴 3.1 从颜色RGB说起 3.2 附属知识 3.3 什么样的向量可以做基&#xff1f; 3.4 基的分类 3.1.1 不同空间的基---向量组的数量可能不同 3.1.2 自然基 3.1.3 正交基 3.1.4 标准正交基 3.1.5 基和向量/矩阵 3.1.6 基变换 …...

Java中支持分库分表的框架/组件/中间件简介

文章目录 1 sharding-jdbc2 TSharding3 Atlas4 Cobar5 MyCAT6 TDDL7 Vitess 列举一些比较常见的&#xff0c;简单介绍一下&#xff1a; sharding-jdbc&#xff08;当当&#xff09; TSharding&#xff08;蘑菇街&#xff09; Atlas&#xff08;奇虎360&#xff09; Cobar&#…...

7.2 项目2 学生通讯录管理:文本文件增删改查(C 版本)(自顶向下设计+断点调试) (A)

C自学精简教程 目录(必读) 该作业是 作业 学生通讯录管理&#xff1a;文本文件增删改查&#xff08;C版本&#xff09; 的C 语言版本。 具体的作业题目描述&#xff0c;要求&#xff0c;可以参考 学生通讯录管理&#xff1a;文本文件增删改查&#xff08;C版本&#xff09;。…...

excel怎么设置任意选一个单元格纵横竖横都有颜色

有时excel表格内容过多的时候&#xff0c;我们通过excel设置任意选一个单元格纵横&#xff0c;竖横背景颜色&#xff0c;这样会更加具有辨识度。设置方式截图如下 设置成功后&#xff0c;预览的效果图...

多模态2025:技术路线“神仙打架”,视频生成冲上云霄

文&#xff5c;魏琳华 编&#xff5c;王一粟 一场大会&#xff0c;聚集了中国多模态大模型的“半壁江山”。 智源大会2025为期两天的论坛中&#xff0c;汇集了学界、创业公司和大厂等三方的热门选手&#xff0c;关于多模态的集中讨论达到了前所未有的热度。其中&#xff0c;…...

基于距离变化能量开销动态调整的WSN低功耗拓扑控制开销算法matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.算法仿真参数 5.算法理论概述 6.参考文献 7.完整程序 1.程序功能描述 通过动态调整节点通信的能量开销&#xff0c;平衡网络负载&#xff0c;延长WSN生命周期。具体通过建立基于距离的能量消耗模型&am…...

智慧工地云平台源码,基于微服务架构+Java+Spring Cloud +UniApp +MySql

智慧工地管理云平台系统&#xff0c;智慧工地全套源码&#xff0c;java版智慧工地源码&#xff0c;支持PC端、大屏端、移动端。 智慧工地聚焦建筑行业的市场需求&#xff0c;提供“平台网络终端”的整体解决方案&#xff0c;提供劳务管理、视频管理、智能监测、绿色施工、安全管…...

UE5 学习系列(三)创建和移动物体

这篇博客是该系列的第三篇&#xff0c;是在之前两篇博客的基础上展开&#xff0c;主要介绍如何在操作界面中创建和拖动物体&#xff0c;这篇博客跟随的视频链接如下&#xff1a; B 站视频&#xff1a;s03-创建和移动物体 如果你不打算开之前的博客并且对UE5 比较熟的话按照以…...

spring:实例工厂方法获取bean

spring处理使用静态工厂方法获取bean实例&#xff0c;也可以通过实例工厂方法获取bean实例。 实例工厂方法步骤如下&#xff1a; 定义实例工厂类&#xff08;Java代码&#xff09;&#xff0c;定义实例工厂&#xff08;xml&#xff09;&#xff0c;定义调用实例工厂&#xff…...

CRMEB 框架中 PHP 上传扩展开发:涵盖本地上传及阿里云 OSS、腾讯云 COS、七牛云

目前已有本地上传、阿里云OSS上传、腾讯云COS上传、七牛云上传扩展 扩展入口文件 文件目录 crmeb\services\upload\Upload.php namespace crmeb\services\upload;use crmeb\basic\BaseManager; use think\facade\Config;/*** Class Upload* package crmeb\services\upload* …...

全志A40i android7.1 调试信息打印串口由uart0改为uart3

一&#xff0c;概述 1. 目的 将调试信息打印串口由uart0改为uart3。 2. 版本信息 Uboot版本&#xff1a;2014.07&#xff1b; Kernel版本&#xff1a;Linux-3.10&#xff1b; 二&#xff0c;Uboot 1. sys_config.fex改动 使能uart3(TX:PH00 RX:PH01)&#xff0c;并让boo…...

pikachu靶场通关笔记22-1 SQL注入05-1-insert注入(报错法)

目录 一、SQL注入 二、insert注入 三、报错型注入 四、updatexml函数 五、源码审计 六、insert渗透实战 1、渗透准备 2、获取数据库名database 3、获取表名table 4、获取列名column 5、获取字段 本系列为通过《pikachu靶场通关笔记》的SQL注入关卡(共10关&#xff0…...

【7色560页】职场可视化逻辑图高级数据分析PPT模版

7种色调职场工作汇报PPT&#xff0c;橙蓝、黑红、红蓝、蓝橙灰、浅蓝、浅绿、深蓝七种色调模版 【7色560页】职场可视化逻辑图高级数据分析PPT模版&#xff1a;职场可视化逻辑图分析PPT模版https://pan.quark.cn/s/78aeabbd92d1...

Aspose.PDF 限制绕过方案:Java 字节码技术实战分享(仅供学习)

Aspose.PDF 限制绕过方案&#xff1a;Java 字节码技术实战分享&#xff08;仅供学习&#xff09; 一、Aspose.PDF 简介二、说明&#xff08;⚠️仅供学习与研究使用&#xff09;三、技术流程总览四、准备工作1. 下载 Jar 包2. Maven 项目依赖配置 五、字节码修改实现代码&#…...