grpc-go之身份验证(二)

2022-09-28 17:35:01 浏览数 (1)

特性介绍

在gRPC中,身份验证被抽象为了credentials.PerRPCCredentials接口:

代码语言:go复制
type PerRPCCredentials interface {
  // GetRequestMetadata 以 map 的形式返回本次调用的授权信息,ctx 是用来控制超时的,并不是从这个 ctx 中获取。
	GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
  // RequireTransportSecurity 指该 Credentials 的传输是否需要需要 TLS 加密,如果返回 true 则说明该 Credentials 需要在一个有 TLS 认证的安全连接上传输,如果当前连接并没有使用 TLS 则会报错
	RequireTransportSecurity() bool
}

主要流程

  • 客户端请求时带上 Credentials gRPC在请求前会将 Credentials 存放在 metadata 中进行传递,请求时gRPC会通过GetRequestMetadata函数, 将用户定义的Credentials提取出来,并添加到 metadata 中, 随着请求一起传递到服务端。
  • 服务端取出 Credentials进行验证 服务端从 metadata 中取出 Credentials 进行有效性校验。一般需要配合拦截器来使用

授权方式

gRPC 中已经内置了部分常用的授权方式,如 oAuth2 和 JWT, 当然我们也可以自定义授权Credentials, 只要实现了credentials.PerRPCCredentials接口就行

案例演示

由于默认提供的JWT方法必须使用谷歌云控制台下载token.json, 所以暂时不考虑演示它的使用, 不过我会通过一个自定义方式集成JWT.

auth/auth.go

定义了一个用户名/密码的授权实现UserPwdAuth和JWT的授权实现JWTAuthToken, 同时把fetchToken的方法也统一放在了这个文件

代码语言:go复制
package auth

import (
	"context"
	"errors"
	"fmt"
	"github.com/dgrijalva/jwt-go"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
	"time"
)

// UserPwdAuth 自定义 Auth 需要实现 credentials.PerRPCCredentials 接口
type UserPwdAuth struct {
	Username string
	Password string
}

// GetRequestMetadata 定义授权信息的具体存放形式,最终会按这个格式存放到 metadata map 中。
func (a *UserPwdAuth) GetRequestMetadata(context.Context, ...string) (map[string]string, error) {
	return map[string]string{"username": a.Username, "password": a.Password}, nil
}

// RequireTransportSecurity 是否需要基于 TLS 加密连接进行安全传输
func (a *UserPwdAuth) RequireTransportSecurity() bool {
	return false
}

const (
	Admin    = "admin"
	Password = "root"
)

// NewUserPwdAuth 自定义授权方式
func NewUserPwdAuth() *UserPwdAuth {
	return &UserPwdAuth{
		Username: Admin,
		Password: Password,
	}
}

// IsValidUserPwd 具体的验证逻辑
func IsValidUserPwd(ctx context.Context) error {
	var (
		user     string
		password string
	)
	// 从 ctx 中获取 metadata
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return status.Errorf(codes.InvalidArgument, "missing metadata")
	}
	// 从metadata中获取授权信息
	// 这里之所以通过md["username"]和md["password"] 可以取到对应的授权信息
	// 是因为我们自定义的 GetRequestMetadata 方法是按照这个格式返回的.
	if val, ok := md["username"]; ok {
		user = val[0]
	}
	if val, ok := md["password"]; ok {
		password = val[0]
	}
	// 简单校验一下 用户名密码是否正确.
	if user != Admin || password != Password {
		return status.Errorf(codes.Unauthenticated, "Unauthorized")
	}

	return nil
}

var (
	headerAuthorize = "jwt"
	secKey          = "abcerqwee"
)

// JWTAuthToken jwt 验证
type JWTAuthToken struct {
	Token string
}

func CreateToken(userName string) (tokenString string) {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"iss":      "grpc-demo-server",
		"aud":      "grpc-demo-server",
		"nbf":      time.Now().Unix(),
		"exp":      time.Now().Add(time.Hour).Unix(),
		"sub":      "user",
		"username": userName,
	})
	tokenString, err := token.SignedString([]byte(secKey))
	if err != nil {
		panic(err)
	}
	return tokenString
}

// NewJWTAuthToken 自定义授权方式
func NewJWTAuthToken() *JWTAuthToken {
	tokenS := CreateToken("ggr")
	return &JWTAuthToken{
		Token: tokenS,
	}
}

func (c JWTAuthToken) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
	return map[string]string{
		headerAuthorize: c.Token,
	}, nil
}

func (c JWTAuthToken) 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"`
}

func IsValidJWToken(ctx context.Context) (bool, error) {
	fmt.Println("开始验证jwt token")
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return false, errors.New("missing metadata")
	}
	// 从metadata中获取授权信息
	tokenStr := ""
	if val, ok := md[headerAuthorize]; ok {
		tokenStr = val[0]
	}
	if len(tokenStr) == 0 {
		return false, errors.New("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(secKey), nil
	})
	if err != nil {
		return false, errors.New("jwt parse error")
	}

	if !token.Valid {
		return false, errors.New("ErrInvalidToken")
	}

	fmt.Println("验证jwt token ok")
	return true, nil
}

// FetchToken simulates a token lookup and omits the details of proper token
// acquisition. For examples of how to acquire an OAuth2 token, see:
// https://godoc.org/golang.org/x/oauth2
func FetchToken() *oauth2.Token {
	return &oauth2.Token{
		AccessToken: "some-secret-token",
	}
}

client/client.go

定义了一个streamAPI和unaryAPI分别实现了游戏战斗数据回传和打招呼的服务

代码语言:go复制
package main

import (
	"context"
	"fmt"
	"grpc-demo/helloworld/pb"
	"io"
	"log"
	"os"
	"time"
)

func bidirectionalStreamBattle(client pb.BattleServiceClient) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	stream, err := client.Battle(ctx)
	if err != nil {
		log.Fatalf("could not battle: %v", err)
	}
	err = stream.SendMsg(&pb.BattleRequest{
		HeroId:  "hero_1",
		SkillId: "Skill_1",
	})
	if err != nil {
		log.Fatalf("could not battle: %v", err)
	}
	err = stream.SendMsg(&pb.BattleRequest{
		HeroId:  "hero_2",
		SkillId: "Skill_2",
	})
	if err != nil {
		log.Fatalf("could not battle: %v", err)
	}
	ch := make(chan struct{})
	go asyncDoBattle(stream, ch)
	err = stream.CloseSend()
	if err != nil {
		log.Fatalf("could not battle: %v", err)
	}
	<-ch
}

func asyncDoBattle(stream pb.BattleService_BattleClient, c chan struct{}) {
	for {
		rsp, err := stream.Recv()
		if err == io.EOF {
			break
		}
		fmt.Println(rsp)
	}
	c <- struct{}{}
}

func sayHello(client pb.GreeterClient) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	// 通过命令行参数指定 name
	name := "world"
	if len(os.Args) > 1 {
		name = os.Args[1]
	}
	r, err := client.SayHello(ctx, &pb.HelloRequest{Name: name})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", r.GetMessage())
}

client/main.go

客户端启动时需要设置身份验证的执行链

代码语言:go复制
package main

import (
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/oauth"
	"google.golang.org/grpc/examples/data"
	"grpc-demo/helloworld/auth"
	"grpc-demo/helloworld/pb"
	"log"
)

const (
	address = "localhost:50051"
)

func main() {
	// 构建一个 PerRPCCredentials。
	// 使用内置的Oauth2进行身份验证
	oauthAuth := oauth.NewOauthAccess(auth.FetchToken())

	// 使用自定义的的身份验证
	userPwdAuth := auth.NewUserPwdAuth()

	// 使用自定义的的身份验证
	jwtAuth := auth.NewJWTAuthToken()

	cred, err := credentials.NewClientTLSFromFile(data.Path("/Users/guirong/go/src/grpc-demo/helloworld/client/ca.crt"),
		"www.ggr.com")
	if err != nil {
		log.Fatalf("failed to load credentials: %v", err)
	}

	conn, err := grpc.Dial(address,
		grpc.WithTransportCredentials(cred),
		grpc.WithPerRPCCredentials(userPwdAuth),
		grpc.WithPerRPCCredentials(oauthAuth),
		grpc.WithPerRPCCredentials(jwtAuth),
	)
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()

	client := pb.NewBattleServiceClient(conn)
	bidirectionalStreamBattle(client)

	client2 := pb.NewGreeterClient(conn)
	sayHello(client2)
}

server/server.go

这里主要是定义游戏战斗数据回传和打招呼的服务的实现, 以及针对这两个服务的制定的身份验证的拦截器

代码语言:go复制
package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
	"grpc-demo/helloworld/auth"
	pb "grpc-demo/helloworld/pb"
	"io"
	"log"
)

var (
	errMissingMetadata = status.Errorf(codes.InvalidArgument, "missing metadata")
	errInvalidToken    = status.Errorf(codes.Unauthenticated, "invalid token")
)

// BattleServer 游戏战斗服务
type BattleServer struct {
	pb.UnimplementedBattleServiceServer
}

// Battle 战斗数据回传
func (h *BattleServer) Battle(steam pb.BattleService_BattleServer) error {
	for {
		req, err := steam.Recv()
		fmt.Println(req)
		if err == io.EOF { //发送最后一次结果给前端
			err = steam.Send(&pb.BattleResponse{})
			if err != nil {
				log.Println(err)
			}
			return nil
		}
		err = steam.Send(&pb.BattleResponse{
			Hero: []*pb.HeroInfo{
				{Id: "hero_1", Life: 999},
			},
			Skill: []*pb.SkillInfo{
				{SkillId: "skill_1", CoolDown: 1664249248},
				{SkillId: "skill_2", CoolDown: 1664249293},
			},
		})
		if err != nil {
			log.Println(err)
		}
	}
}

// GreeterServer 定义一个结构体用于实现 .proto文件中定义的方法
// 新版本 gRPC 要求必须嵌入 pb.UnimplementedGreeterServer 结构体
type GreeterServer struct {
	pb.UnimplementedGreeterServer
}

// SayHello 简单实现一下.proto文件中定义的 SayHello 方法
func (g *GreeterServer) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Received Msg: %v", in.GetName())
	return &pb.HelloReply{Message: "Hello "   in.GetName()}, nil
}

// userPwdCheckInterceptor (用户名/密码)身份验证拦截器
func userPwdCheckInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
	handler grpc.UnaryHandler) (interface{}, error) {
	// 如果返回err不为nil则说明token验证未通过
	err := auth.IsValidUserPwd(ctx)
	if err != nil {
		return nil, err
	}
	return handler(ctx, req)
}

// authTokenInterceptor (jwt和Oauth2 token)身份验证拦截器
func authTokenInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo,
	handler grpc.StreamHandler) error {
	// authentication (token verification)
	md, ok := metadata.FromIncomingContext(ss.Context())
	if !ok {
		return errMissingMetadata
	}

	// 验证oauth2
	if !auth.IsValidOauth2(md["authorization"]) {
		return errInvalidToken
	}

	// 验证jwt
	ok, _ = auth.IsValidJWToken(ss.Context())
	if !ok {
		return errInvalidToken
	}

	err := handler(srv, ss)
	return err
}

server/main.go

服务端启动时需要显示配置身份验证的拦截器

代码语言:go复制
package main

import (
	"crypto/tls"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/examples/data"
	"grpc-demo/helloworld/pb"
	"log"
	"net"
)

const (
	port = ":50051"
)

func main() {
	cert, err := tls.LoadX509KeyPair(
		data.Path("/Users/guirong/go/src/grpc-demo/helloworld/server/server.crt"),
		data.Path("/Users/guirong/go/src/grpc-demo/helloworld/server/server.key"))
	if err != nil {
		log.Fatalf("failed to load key pair: %s", err)
	}

	// s := grpc.NewServer(grpc.UnaryInterceptor(ensureValidToken), grpc.Creds(credentials.NewServerTLSFromCert(&cert)))
	s := grpc.NewServer(
		grpc.UnaryInterceptor(userPwdCheckInterceptor),
		grpc.StreamInterceptor(authTokenInterceptor),
		grpc.Creds(credentials.NewServerTLSFromCert(&cert)),
	)

	pb.RegisterGreeterServer(s, &GreeterServer{})
	// 玩家连续进行了多次战斗请求,服务器将操作结果响应给玩家
	pb.RegisterBattleServiceServer(s, &BattleServer{})

	listen, err := net.Listen("tcp", port)
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	log.Println("Serving gRPC on 0.0.0.0"   port)
	if err := s.Serve(listen); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

启动服务端

代码语言:shell复制
$cd server
$go build
$./server
2022/09/28 17:05:59 Serving gRPC on 0.0.0.0:50051
进入IsValidOauth2验证Oauth2...
离开IsValidOauth2, Oauth2 验证OK...
进入IsValidJWToken验证jwt token...
离开IsValidJWToken,jwt token 验证 OK...
HeroId:"hero_1" SkillId:"Skill_1" 
HeroId:"hero_2" SkillId:"Skill_2" 
<nil>
进入IsValidUserPwd验证用户名密码...
离开IsValidUserPwd用户名密码验证OK...
2022/09/28 17:06:03 Received Msg: world

启动客户端, 查看服务端控制台变化

代码语言:shell复制
$cd client
$go build
$./client
hero:<Id:"hero_1" Life:999 > skill:<SkillId:"skill_1" CoolDown:1664249248 > skill:<SkillId:"skill_2" CoolDown:1664249293 > 
hero:<Id:"hero_1" Life:999 > skill:<SkillId:"skill_1" CoolDown:1664249248 > skill:<SkillId:"skill_2" CoolDown:1664249293 > 

2022/09/28 17:06:03 Greeting: Hello world

参考

https://www.lixueduan.com/posts/grpc/06-auth/

https://github.com/grpc/grpc-go/tree/master/examples/features/authentication

0 人点赞