特性介绍
在gRPC中,身份验证被抽象为了credentials.PerRPCCredentials
接口:
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的方法也统一放在了这个文件
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