【grpc】grpc进阶二,grpc认证方式

本章把之前的工程结构改了一下,创建了 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)
	}
}

运行结果:
在这里插入图片描述

相关推荐

  1. grpcgrpc三,服务发现

    2024-04-14 16:44:05       33 阅读
  2. 【Python)】——程序调试方法

    2024-04-14 16:44:05       43 阅读
  3. docker问题

    2024-04-14 16:44:05       43 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-14 16:44:05       98 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-14 16:44:05       106 阅读
  3. 在Django里面运行非项目文件

    2024-04-14 16:44:05       87 阅读
  4. Python语言-面向对象

    2024-04-14 16:44:05       96 阅读

热门阅读

  1. 如何备考蓝桥杯赛事 怎样才能取得好成绩?

    2024-04-14 16:44:05       35 阅读
  2. Arcgis windows webadaptor配置

    2024-04-14 16:44:05       45 阅读
  3. 前端面试问题汇总 - Vue篇

    2024-04-14 16:44:05       32 阅读
  4. 将基于Centos下的Linux 中的man 汉化

    2024-04-14 16:44:05       37 阅读
  5. 学术写作进阶:ChatGPT辅助下的论文撰写技巧

    2024-04-14 16:44:05       36 阅读
  6. ARM-SC2440

    2024-04-14 16:44:05       33 阅读
  7. npm 常用命令详解

    2024-04-14 16:44:05       41 阅读
  8. 每天一个数据分析题(二百六十二)

    2024-04-14 16:44:05       43 阅读
  9. 每天一个数据分析题(二百六十一)

    2024-04-14 16:44:05       41 阅读
  10. 算法与数据结构 顺序栈(C++)

    2024-04-14 16:44:05       33 阅读