Grpc | Token认证和自定义认证
概述
现在有个需求,需要在grpc的拦截器中,通过token进行身份认证。如果认证不通过则返回错误; 认证通过,则放行;
TLS是用作加密通讯通道的,与该需求无关。
Token认证
具体流程
- 客户端通过账号密码登录服务器,登录成功服务器返回一个token给客户端。
- 客户端携带token信息往服务器请求业务逻辑的接口,,服务器收到请求,解析验证token,验证通过,则放行。
协议文件
proto
//hello.proto
syntax = "proto3";
option go_package = "./pb;pb";
import "google/protobuf/empty.proto";
package hello;
message Login{
string login = 1;
string password = 2;
}
message TokenInfo {
string token = 1;
}
message HelloWorld {
string msg = 1;
}
service Hello {
rpc CreateToken(Login) returns(TokenInfo) {}
rpc SayHello(google.protobuf.Empty) returns(HelloWorld) {}
}
生成*.pb.go文件
$ protoc -I. --go_out=. --go-grpc_out=. ./*proto
使用的是jwt-go
包。来帮助服务端创建&解析token
golang
//jwt.go
package jwt
import (
"context"
"fmt"
"pro2d/conf"
"pro2d/utils"
"time"
jwt "github.com/dgrijalva/jwt-go"
"google.golang.org/grpc/metadata"
)
func CreateToken(uid string) (tokenString string) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"iss": "pro2d-app-server",
"aud": "pro2d-app-server",
"nbf": time.Now().Unix(),
"exp": time.Now().Add(time.Hour).Unix(),
"sub": "pro2d",
"uid": uid,
})
tokenString, err := token.SignedString([]byte(utils.Pro2DTokenSignedString))
if err != nil {
panic(err)
}
return tokenString
}
func ParseToken(tokenStr string) string {
var clientClaims Claims
token, err := jwt.ParseWithClaims(tokenStr, &clientClaims, func(token *jwt.Token) (interface{}, error) {
if token.Header["alg"] != "HS256" {
//panic("ErrInvalidAlgorithm")
utils.Sugar.Error("ErrInvalidAlgorithm")
return nil, nil
}
return []byte(utils.Pro2DTokenSignedString), nil
})
if err != nil {
utils.Sugar.Error("jwt parse error")
return ""
}
if !token.Valid {
utils.Sugar.Error("ErrInvalidToken")
return ""
}
return clientClaims.Uid
}
// Claims defines the struct containing the token claims.
type Claims struct {
jwt.StandardClaims
Uid string
}
// 从 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][]string
token, ok := md["authorization"]
if !ok || len(token) == 0 {
return "", fmt.Errorf("ErrNoAuthorizationInMetadata")
}
// 因此,token 是一个字符串数组,我们只用了 token[0]
return token[0], nil
}
func CheckAuth(ctx context.Context) string {
tokenStr, err := getTokenFromContext(ctx)
if err != nil {
utils.Sugar.Errorf("get token from context error")
return ""
}
return ParseToken(tokenStr)
}
// AuthToken 自定义认证 客户端使用
type AuthToken struct {
Token string
}
func (c AuthToken) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"authorization": c.Token,
}, nil
}
func (c AuthToken) RequireTransportSecurity() bool {
return true
}
server端
golang
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/protobuf/types/known/emptypb"
"log"
"net"
"pro2d/components/jwt"
"pro2d/protos/pb"
)
type Server struct {
pb.UnimplementedHelloServer
}
func (s *Server) CreateToken(ctx context.Context, in *pb.Login) (*pb.TokenInfo, error) {
if in.Login == "login" && in.Password == "123456" {
return &pb.TokenInfo{Token: jwt.CreateToken(in.Login)}, nil
}
return nil, fmt.Errorf("login error")
}
func (s *Server) SayHello(ctx context.Context, empty *emptypb.Empty) (*pb.HelloWorld, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.Unauthenticated,"ErrNoMetadataInContext")
}
// md 的类型是 type MD map[string][]string
token, ok := md["authorization"]
if !ok || len(token) == 0 {
return nil, status.Errorf(codes.Unauthenticated,"ErrNoAuthorizationInMetadata")
}
login := jwt.ParseToken(token[0])
return &pb.HelloWorld{Msg: "Hello world: " + login}, nil
}
func main() {
// 监听本地端口
listener, err := net.Listen("tcp", ":8948")
if err != nil {
log.Fatalf("net.Listen err: %v", err)
}
var opts []grpc.ServerOption
// 从输入证书文件和密钥文件为服务端构造TLS凭证
creds, err := credentials.NewServerTLSFromFile("keys/server.pem", "keys/server.key")
if err != nil {
log.Fatalf("Failed to generate credentials %v", err)
}
opts = append(opts, grpc.Creds(creds))
// 新建gRPC服务器实例,并开启TLS认证
grpcServer := grpc.NewServer(opts...)
// 在gRPC服务器注册我们的服务
pb.RegisterHelloServer(grpcServer, &Server{})
log.Println(" net.Listing whth TLS")
//用服务器 Serve() 方法以及我们的端口信息区实现阻塞等待,直到进程被杀死或者 Stop() 被调用
err = grpcServer.Serve(listener)
if err != nil {
log.Fatalf("grpcServer.Serve err: %v", err)
}
}
client端
golang
package main
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/protobuf/types/known/emptypb"
"log"
"pro2d/components/jwt"
_ "pro2d/conf"
"pro2d/protos/pb"
"pro2d/utils"
)
func main() {
var opts []grpc.DialOption
creds, err := credentials.NewClientTLSFromFile("keys/server.pem", "pro2d")
if err != nil {
log.Fatal(err)
return
}
opts = append(opts, grpc.WithTransportCredentials(creds))
conn, err := grpc.Dial("localhost:8948", opts...)
helloClient := pb.NewHelloClient(conn)
token, err := helloClient.CreateToken(context.TODO(), &pb.Login{
Login: "login",
Password: "123456",
})
if err != nil {
log.Fatal(err)
return
}
utils.Sugar.Debugf("token: %s", token.Token)
opts = append(opts, grpc.WithPerRPCCredentials(&jwt.AuthToken{Token: token.Token}))
conn2, err := grpc.Dial("localhost:8948",opts...)
if err != nil {
log.Fatal(err)
return
}
helloClient2 := pb.NewHelloClient(conn2)
rsp, err := helloClient2.SayHello(context.TODO(), &emptypb.Empty{})
if err != nil {
log.Fatal(err)
}
log.Printf("sayhello rsp: %v", rsp)
}
客户端调用CreateToken, 获得token
客户端通过
grpc.WithPerRPCCredentials
, 携带token信息,重新发起grpc请求,再调用SayHello
,接口,结果返回成功服务器在收到
SayHello
请求的时候,从上下文ctx
取出token信息,再解析token获得login
的信息。
自定义信息验证
上面的token认证是通过客户端传递token信息到服务器,服务器从进行解析来做的。 还有一种是客户端和服务器约定固定信息,当客户顿请求接口时,把信息放在请求头部。
server端, 修改SayHello
函数
func (s *Server) SayHello(ctx context.Context, empty *emptypb.Empty) (*pb.HelloWorld, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.Unauthenticated, "无Token认证信息")
}
var (
appId string
appKey string
)
if val, ok := md["appId"]; ok {
appId = val[0]
}
if val, ok := md["appkey"]; ok {
appKey = val[0]
}
if appId != "100" || appKey != "token" {
return nil, status.Errorf(codes.Unauthenticated, "Token认证信息无效: appid=%s, appkey=%s", appId, appKey)
}
return &pb.HelloWorld{Msg: "Hello world"}, nil
}
客户端需要改的地方是把 opts = append(opts, grpc.WithPerRPCCredentials(&jwt.AuthToken{Token: token.Token}))
改为
opts = append(opts, grpc.WithPerRPCCredentials(&CustomToken{}))
type CustomToken struct {
}
func (c CustomToken) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"appId": "100",
"appKey": "token",
}, nil
}
func (c CustomToken) RequireTransportSecurity() bool {
return true
}
SAN证书生成参考: SAN
关于grpc和认证相关的一些想法
我们在开发过程中往往更复杂, 下面是优化过程
- grpc + tls + token验证 + 自定义认证
- tls: 目的是为了加密协议传输通道
- token认证:是为了能从token中获取用户信息,保障用户信息安全。服务器不用存储会话信息,减少内存。但是每次接口请求都增加了token解析操作。
- 自定义认证:客户端服务器约定固定的参数, 客户端每次请求都会带上,服务器做验证(有token认证后,自定义认证没有必要了,个人观点)
- 在拦截器中统一身份token验证, 这样做的目的是减少代码冗余,但是这样没法传递信息给接口,直到我发现了
context.WithValue()
- 服务器收到信息,解析token, 得到用户的唯一标识
uid
(这是客户端在创建token的时候加进去的) - 然后使用
context.WithValue("uid", uid)
, 把uid放到上下文中,传递给SayHello(ctx context.Context, empty *emptypb.Empty) (*pb.HelloWorld, error)
可以通过ctx.Value("uid").(string)
取出来。这样做逻辑处理的时候就知道是哪位用户在处理。
--完--
- 原文作者: 留白
- 原文链接: https://zfunnily.github.io/2022/02/jwt/
- 更新时间:2024-04-16 01:01:05
- 本文声明:转载请标记原文作者及链接