第十节——gRPC 拦截器
Posted 想学习安全的小白
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第十节——gRPC 拦截器相关的知识,希望对你有一定的参考价值。
第十章——使用 gRPC 拦截器通过 JWT 进行授权
- 实现一个服务器拦截器来授权使用 JSON Web 令牌 (JWT) 访问我们的 gRPC API。使用这个拦截器,我们将确保只有具有某些特定角色的用户才能调用我们服务器上的特定 API。
- 然后,我们将实现一个客户端拦截器来登录用户并将 JWT 附加到请求中,然后再调用 gRPC API。
10.1、一个简单的服务器拦截器
- 拦截器有两种类型:一种是用于一元RPC,另一种用于流RPC
10.1.1、一元拦截器
- 重构cmd/server/main.go里的部分代码
- 在main函数中的
grpc.NewServer()
函数中,让我们添加一个新grpc.UnaryInterceptor()
选项。它期望一元服务器拦截器功能作为输入。- 在unaryInterceptor函数中,我们只写了一个简单的日志,上面写着“一元拦截器”以及被调用的 RPC 的完整方法名称。
- 然后我们使用原始上下文和请求调用实际的处理程序,并返回其结果。
func unaryInterceptor(ctx context.Context, req interface, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface, error)
log.Println("--> unary interceptor: ", info.FullMethod)
return handler(ctx, req)
func main()
...
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(unaryInterceptor),
)
...
10.1.2、流拦截器
- 添加
grpc.StreamInterceptor()
选项。 - 按照定义获取函数签名。
- 将其复制并粘贴到
server/main.go
文件中。 - 将该函数传递给流拦截器选项。
- 使用完整的 RPC 方法名称编写日志。
- 这次将使用原始服务器和流参数调用处理程序。
func streamInterceptor(srv interface, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error
log.Println("--> stream interceptor: ", info.FullMethod)
return handler(srv, stream)
10.1.3、运行客户端与服务端测试拦截器
- 启动服务端再启动客户端,运行评价接口
- 服务器日志中看到的,一元拦截器被调用了 3 次,用于创建笔记本电脑 RPC。
- 之后客户端按y使用流,在服务器端可以看到流拦截器被调用一次。
10.2、使用JWT访问令牌
- 扩展其功能以验证和授权用户请求。
- 为此,我们需要将用户添加到我们的系统,并向登录用户添加服务并返回 JWT 访问令牌。
10.2.1、定义用户结构
- 在service目录下创建user.go文件
- 定义一个
User
结构体。它将包含 三个属性:username
、hashed_password
和role
type User struct
Username string
HashedPassword string
Role string
- 定义一个
NewUser()
函数来创建一个新用户,它接受用户名、密码和角色作为输入,并返回一个User
对象和一个错误- 使用函数bcrypt将密码明文处理成哈希密文,命令:
go get golang.org/x/crypto/bcrypt
- 使用函数bcrypt将密码明文处理成哈希密文,命令:
func NewUser(username string, password string, role string) (*User, error)
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil
return nil, fmt.Errorf("cannot hash password: %w", err)
user := &User
Username: username,
HashedPassword: string(hashedPassword),
Role: role,
return user, nil
- 定义一个方法
IsCorrectPassword
来检查给定的密码是否正确- 调用
bcrypt.CompareHashAndPassword()
函数,传入用户的哈希密码和给定的明文密码。函数返回true则证明一致否则不一致
- 调用
func (user *User) IsCorrectPassword(password string) bool
err := bcrypt.CompareHashAndPassword([]byte(user.HashedPassword), []byte(password))
return err == nil
- 再添加函数用于克隆用户,后面将用户存储时使用
func (user *User) Clone() *User
return &User
Username: user.Username,
HashedPassword: user.HashedPassword,
Role: user.Role,
10.2.2、定义用户存储
- 创建
service/user_store.go
文件 - 定义一个
UserStore
接口,它将有两个功能:- 一种将用户保存到商店的功能。
- 另一个功能是通过用户名在商店中查找用户。
type UserStore interface
Save(user *User) error
Find(username string) (*User, error)
- 定义一个
InMemoryUserStore
结构体来实现接口- 它有一个互斥锁来控制并发访问
- 一个map存储用户
type InMemoryUserStore struct
mutex sync.RWMutex
users map[string]*User
- 编写一个函数来构建一个新的内存用户存储,并在其中初始化用户映射
func NewInMemoryUserStore() *InMemoryUserStore
return &InMemoryUserStore
users: make(map[string]*User),
- 实现Save函数,首先我们获取写锁。然后检查是否已经存在具有相同用户名的用户,没有则将用户克隆后放入map数组中
func (store *InMemoryUserStore) Save(user *User) error
store.mutex.Lock()
defer store.mutex.Unlock()
if store.users[user.Username] != nil
return fmt.Errorf("ErrAlreadyExists")
store.users[user.Username] = user.Clone()
return nil
- 实现Find函数,首先获得一个读锁。然后我们通过用户名从map中获取用户
func (store *InMemoryUserStore) Find(username string) (*User, error)
store.mutex.RLock()
defer store.mutex.RUnlock()
user := store.users[username]
if user == nil
return nil, nil
return user.Clone(), nil
10.2.3、实现一个 JWT 管理器来为用户生成和验证访问令牌
- 创建
service/jwt_manager.go
文件 - 定义一个JWTManager 结构体,包含两个字段:1、用于签名和验证访问令牌的密钥;2、以及令牌的有效期限。
type JWTManager struct
secretKey string
tokenDuration time.Duration
func NewJWTManager(secretKey string, tokenDuration time.Duration) *JWTManager
return &JWTManagersecretKey, tokenDuration
- 安装jwt-go 库,命令:
go get github.com/dgrijalva/jwt-go
- 定义一个
UserClaims
结构体。它将包含 JWTStandardClaims
作为复合字段。
type UserClaims struct
jwt.StandardClaims
Username string `json:"username"`
Role string `json:"role"`
- 编写一个函数
Generate
来为特定用户生成并签署一个新的访问令牌- 首先创建一个新的
UserClaims
对象 - 将令牌持续时间添加到当前时间并将其转换为 Unix 时间
- 然后我们设置用户的用户名和角色
- 调用
jwt.NewWithClaims()
函数来生成一个令牌对象,为简单起见,这里使用HS256
. - 最后也是最重要的一步是使用您的密钥对生成的令牌进行签名
- 首先创建一个新的
func (manager *JWTManager) Generate(user *User) (string, error)
claims := UserClaims
StandardClaims: jwt.StandardClaims
ExpiresAt: time.Now().Add(manager.tokenDuration).Unix(),
,
Username: user.Username,
Role: user.Role,
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(manager.secretKey))
- 添加另一个函数来验证访问令牌
- 调用
jwt.ParseWithClaims()
,传入访问令牌,一个空的用户声明和一个自定义键函数。在这个函数中,检查令牌的签名方法以确保它与我们的服务器使用的算法匹配非常重要,在我们的例子中是 HMAC。 - 从令牌中获取声明并将其转换为
UserClaims
对象
- 调用
func (manager *JWTManager) Verify(accessToken string) (*UserClaims, error)
token, err := jwt.ParseWithClaims(
accessToken,
&UserClaims,
func(token *jwt.Token) (interface, error)
_, ok := token.Method.(*jwt.SigningMethodHMAC)
if !ok
return nil, fmt.Errorf("unexpected token signing method")
return []byte(manager.secretKey), nil
,
)
if err != nil
return nil, fmt.Errorf("invalid token: %w", err)
claims, ok := token.Claims.(*UserClaims)
if !ok
return nil, fmt.Errorf("invalid token claims")
return claims, nil
10.3、实现 Auth 服务服务器
- 创建一个新
proto/auth_service.proto
文件 - 定义了一个
LoginRequest
包含 2 个字段的消息:一个 stringusername
和一个 stringpassword
。然后是LoginResponse
一条只有 1 个字段的消息:access_token
. - 我们定义一个新的
AuthService
.
message LoginRequest
string username = 1;
string password = 2;
message LoginResponse string access_token = 1;
service AuthService
rpc Login(LoginRequest) returns (LoginResponse) ;
- 运行命令:
make gen
生成go代码 - 创建一个新
service/auth_server.go
文件来实现这个新服务 - 定义一个结构体
AuthServer
type AuthServer struct
pb.UnimplementedAuthServiceServer
userStore UserStore
jwtManager *JWTManager
func NewAuthServer(userStore UserStore, jwtManager *JWTManager) *AuthServer
return &AuthServer
userStore: userStore,
jwtManager: jwtManager,
- 实现在proto中定义的Login服务
- 调用
userStore.Find()
通过用户名查找用户 - 找到用户并且密码正确,我们调用
jwtManager.Generate()
生成一个新的访问令牌 - 使用生成的访问令牌创建一个新的登录响应对象,并将其返回给客户端
- 调用
func (server *AuthServer) Login(ctx context.Context, req *pb.LoginRequest) (*pb.LoginResponse, error)
user, err := server.userStore.Find(req.GetUsername())
if err != nil
return nil, status.Errorf(codes.Internal, "cannot find user: %v", err)
if user == nil || !user.IsCorrectPassword(req.GetPassword())
return nil, status.Errorf(codes.NotFound, "incorrect username/password")
token, err := server.jwtManager.Generate(user)
if err != nil
return nil, status.Errorf(codes.Internal, "cannot generate access token")
res := &pb.LoginResponseAccessToken: token
return res, nil
10.4、将身份验证服务添加到 gRPC 服务器
- 打开
cmd/server/main.go
文件 - 定义常量
const (
secretKey = "secret"
tokenDuration = 15 * time.Minute
)
- 重构main函数
func main()
laptopStore := service.NewInMemoryLaptopStore()
imageStore := service.NewDiskImageStore("img")
ratingStore := service.NewInMemoryRatingStore()
laptopServer := service.NewLaptopServer(laptopStore, imageStore, ratingStore)
//
userStore := service.NewInMemoryUserStore()
jwtManager := service.NewJWTManager(secretKey, tokenDuration)
authServer := service.NewAuthServer(userStore, jwtManager)
//
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(unaryInterceptor),
grpc.StreamInterceptor(streamInterceptor),
)
pb.RegisterLaptopServiceServer(grpcServer, laptopServer)
//
pb.RegisterAuthServiceServer(grpcServer, authServer)
//
reflection.Register(grpcServer)
listener, _ := net.Listen("tcp", ":8888")
grpcServer.Serve(listener)
- 测试新的登录 API,我们必须添加一些种子用户。我将编写一个函数来创建一个给定用户名、密码和角色的用户,并将其保存到用户存储中
func createUser(userStore service.UserStore, username, password, role string) error
user, err := service.NewUser(username, password, role)
if err != nil
return err
return userStore.Save(user)
- 在
seedUsers()
函数中,我调用该createUser()
函数 2 次以创建 1 个管理员用户和 1 个普通用户。假设他们有相同的secret
密码。
func seedUsers(userStore service.UserStore) error
err := createUser(userStore, "admin1", "secret", "admin")
if err != nil
return err
return createUser(userStore, "user1", "secret", "user")
- 主函数中,我们在创建用户存储后立即调用 seedUsers()。
func main()
userStore := service.NewInMemoryUserStore()
err := seedUsers(userStore)
if err != nil
log.Fatal("cannot seed users: ", err)
jwtManager := service.NewJWTManager(secretKey, tokenDuration)
authServer := service.NewAuthServer(userStore, jwtManager)
laptopStore := service.NewInMemoryLaptopStore()
imageStore := service.NewDiskImageStore("img")
ratingStore := service.NewInMemoryRatingStore()
laptopServer := service.NewLaptopServer(laptopStore, imageStore, ratingStore)
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(unaryInterceptor),
grpc.StreamInterceptor(streamInterceptor),
)
pb.RegisterLaptopServiceServer(grpcServer, laptopServer)
pb.RegisterAuthServiceServer(grpcServer, authServer)
reflection.Register(grpcServer)
listener, _ := net.Listen("tcp", ":8888")
grpcServer.Serve(listener)
10.5、使用 Evans CLI 尝试身份验证服务
- 启动客户端,命令:
make server
- 新开终端,使用命令:
evans -r -p 8888
- 选择服务,命令:
service AuthService
- 调用login方法,命令:
call Login
- 输入正确的用户名以及密码:admin1、secret
- 选择服务,命令:
10.6、实现服务器的身份验证拦截器
- 创建一个新
service/auth_interceptor.go
文件 - 定义一个新结构
AuthInterceptor
type AuthInterceptor struct
jwtManager *JWTManager
accessibleRoles map[string][]string
- 编写一个
NewAuthInterceptor()
函数来构建并返回一个新的身份验证拦截器对象
func NewAuthInterceptor(jwtManager *JWTManager, accessibleRoles map[string][]string) *AuthInterceptor
return &AuthInterceptorjwtManager, accessibleRoles
- 删除掉cmd/server/main.go文件里的unaryInterceptor函数和
- 在
service/auth_interceptor.go
文件中实现新的一元拦截函数
func (interceptor *AuthInterceptor) Unary() grpc.UnaryServerInterceptor
return func(ctx context.Context, req interface, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface, error)
log.Println("--> unary interceptor: ", info.FullMethod)
// TODO: implement authorization
return handler(ctx, req)
- 添加一个新
Stream()
方法,该方法将创建并返回一个 gRPC 流服务器拦截器函数
func (interceptor *AuthInterceptor) Stream() grpc.StreamServerInterceptor
return func(srv interface, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error
log.Println("--> stream interceptor: ", info.FullMethod)
// TODO: implement authorization
return handler(srv, stream)
- 在
cmd/server/main.go
文件中,我们必须使用 jwt 管理器和可访问角色的映射创建一个新的拦截器对象。在grpc.NewServer()
函数中,我们可以传入interceptor.Unary()
andinterceptor.Stream()
。
func accessibleRoles() map[string][]string
const laptopServicePath = "/techschool.pcbook.LaptopService/"
return map[string][]string
laptopServicePath + "CreateLaptop": "admin",
laptopServicePath + "UploadImage": "admin",
laptopServicePath + "RateLaptop": "admin", "user",
func main()
...
interceptor := service.NewAuthInterceptor(jwtManager, accessibleRoles())
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(interceptor.Unary()),
grpc.StreamInterceptor(interceptor.Stream()),
)
...
- 定义一个新
authorize()
函数,它将以上下文和方法作为输入,如果请求未经授权,将返回错误。
func (interceptor *AuthInterceptor) authorize(ctx context.Context, method string) error
// TODO: implement this
- 在
Unary()
函数和Stream()函数中,我们使用interceptor.authorize()
函数
func (interceptor *AuthInterceptor) Unary() grpc.UnaryServerInterceptor
return func(ctx context.Context, req interface以上是关于第十节——gRPC 拦截器的主要内容,如果未能解决你的问题,请参考以下文章