前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >grpc-go之身份验证(二)

grpc-go之身份验证(二)

原创
作者头像
Johns
修改2022-09-28 17:35:01
1.4K0
修改2022-09-28 17:35:01
举报
文章被收录于专栏:代码工具

特性介绍

在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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 特性介绍
  • 主要流程
  • 案例演示
    • auth/auth.go
      • client/client.go
        • client/main.go
          • server/server.go
            • server/main.go
              • 启动服务端
                • 启动客户端, 查看服务端控制台变化
                • 参考
                相关产品与服务
                访问管理
                访问管理(Cloud Access Management,CAM)可以帮助您安全、便捷地管理对腾讯云服务和资源的访问。您可以使用CAM创建子用户、用户组和角色,并通过策略控制其访问范围。CAM支持用户和角色SSO能力,您可以根据具体管理场景针对性设置企业内用户和腾讯云的互通能力。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档