第十节——gRPC 拦截器

Posted 想学习安全的小白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第十节——gRPC 拦截器相关的知识,希望对你有一定的参考价值。

第十章——使用 gRPC 拦截器通过 JWT 进行授权

  1. 实现一个服务器拦截器来授权使用 JSON Web 令牌 (JWT) 访问我们的 gRPC API。使用这个拦截器,我们将确保只有具有某些特定角色的用户才能调用我们服务器上的特定 API。
  2. 然后,我们将实现一个客户端拦截器来登录用户并将 JWT 附加到请求中,然后再调用 gRPC API。

10.1、一个简单的服务器拦截器

  1. 拦截器有两种类型:一种是用于一元RPC,另一种用于流RPC

10.1.1、一元拦截器

  1. 重构cmd/server/main.go里的部分代码
  2. 在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、流拦截器

  1. 添加grpc.StreamInterceptor()选项。
  2. 按照定义获取函数签名。
  3. 将其复制并粘贴到server/main.go文件中。
  4. 将该函数传递给流拦截器选项。
  5. 使用完整的 RPC 方法名称编写日志。
  6. 这次将使用原始服务器和流参数调用处理程序。
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、运行客户端与服务端测试拦截器

  1. 启动服务端再启动客户端,运行评价接口
  2. 服务器日志中看到的,一元拦截器被调用了 3 次,用于创建笔记本电脑 RPC。
  3. 之后客户端按y使用流,在服务器端可以看到流拦截器被调用一次。

10.2、使用JWT访问令牌

  1. 扩展其功能以验证和授权用户请求。
  2. 为此,我们需要将用户添加到我们的系统,并向登录用户添加服务并返回 JWT 访问令牌。

10.2.1、定义用户结构

  1. 在service目录下创建user.go文件
  2. 定义一个User结构体。它将包含 三个属性: usernamehashed_passwordrole
type User struct 
	Username       string
	HashedPassword string
	Role           string

  1. 定义一个NewUser()函数来创建一个新用户,它接受用户名、密码和角色作为输入,并返回一个User对象和一个错误
    • 使用函数bcrypt将密码明文处理成哈希密文,命令:go get golang.org/x/crypto/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

  1. 定义一个方法IsCorrectPassword来检查给定的密码是否正确
    • 调用bcrypt.CompareHashAndPassword()函数,传入用户的哈希密码和给定的明文密码。函数返回true则证明一致否则不一致
func (user *User) IsCorrectPassword(password string) bool 
    err := bcrypt.CompareHashAndPassword([]byte(user.HashedPassword), []byte(password))
    return err == nil

  1. 再添加函数用于克隆用户,后面将用户存储时使用
func (user *User) Clone() *User 
    return &User
        Username:       user.Username,
        HashedPassword: user.HashedPassword,
        Role:           user.Role,
    

10.2.2、定义用户存储

  1. 创建service/user_store.go文件
  2. 定义一个UserStore接口,它将有两个功能:
    • 一种将用户保存到商店的功能。
    • 另一个功能是通过用户名在商店中查找用户。
type UserStore interface 
    Save(user *User) error
    Find(username string) (*User, error)

  1. 定义一个InMemoryUserStore结构体来实现接口
    • 它有一个互斥锁来控制并发访问
    • 一个map存储用户
type InMemoryUserStore struct 
    mutex sync.RWMutex
    users map[string]*User

  1. 编写一个函数来构建一个新的内存用户存储,并在其中初始化用户映射
func NewInMemoryUserStore() *InMemoryUserStore 
    return &InMemoryUserStore
        users: make(map[string]*User),
    

  1. 实现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

  1. 实现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 管理器来为用户生成和验证访问令牌

  1. 创建service/jwt_manager.go文件
  2. 定义一个JWTManager 结构体,包含两个字段:1、用于签名和验证访问令牌的密钥;2、以及令牌的有效期限。
type JWTManager struct 
    secretKey     string
    tokenDuration time.Duration


func NewJWTManager(secretKey string, tokenDuration time.Duration) *JWTManager 
    return &JWTManagersecretKey, tokenDuration

  1. 安装jwt-go 库,命令:go get github.com/dgrijalva/jwt-go
  2. 定义一个UserClaims结构体。它将包含 JWTStandardClaims作为复合字段。
type UserClaims struct 
    jwt.StandardClaims
    Username string `json:"username"`
    Role     string `json:"role"`

  1. 编写一个函数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))

  1. 添加另一个函数来验证访问令牌
    • 调用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 服务服务器

  1. 创建一个新proto/auth_service.proto文件
  2. 定义了一个LoginRequest包含 2 个字段的消息:一个 stringusername和一个 string password。然后是LoginResponse一条只有 1 个字段的消息:access_token.
  3. 我们定义一个新的AuthService.
message LoginRequest 
  string username = 1;
  string password = 2;


message LoginResponse  string access_token = 1; 

service AuthService 
  rpc Login(LoginRequest) returns (LoginResponse) ;

  1. 运行命令:make gen生成go代码
  2. 创建一个新service/auth_server.go文件来实现这个新服务
  3. 定义一个结构体AuthServer
type AuthServer struct 
	pb.UnimplementedAuthServiceServer
	userStore  UserStore
	jwtManager *JWTManager


func NewAuthServer(userStore UserStore, jwtManager *JWTManager) *AuthServer 
	return &AuthServer
		userStore:  userStore,
		jwtManager: jwtManager,
	

  1. 实现在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 服务器

  1. 打开cmd/server/main.go文件
  2. 定义常量
const (
    secretKey     = "secret"
    tokenDuration = 15 * time.Minute
)
  1. 重构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)

  1. 测试新的登录 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)

  1. 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")

  1. 主函数中,我们在创建用户存储后立即调用 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 尝试身份验证服务

  1. 启动客户端,命令:make server
  2. 新开终端,使用命令:evans -r -p 8888
    • 选择服务,命令:service AuthService
    • 调用login方法,命令:call Login
    • 输入正确的用户名以及密码:admin1、secret

10.6、实现服务器的身份验证拦截器

  1. 创建一个新service/auth_interceptor.go文件
  2. 定义一个新结构AuthInterceptor
type AuthInterceptor struct 
    jwtManager      *JWTManager
    accessibleRoles map[string][]string

  1. 编写一个NewAuthInterceptor()函数来构建并返回一个新的身份验证拦截器对象
func NewAuthInterceptor(jwtManager *JWTManager, accessibleRoles map[string][]string) *AuthInterceptor 
    return &AuthInterceptorjwtManager, accessibleRoles

  1. 删除掉cmd/server/main.go文件里的unaryInterceptor函数和
  2. 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)
	

  1. 添加一个新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)
	

  1. cmd/server/main.go文件中,我们必须使用 jwt 管理器和可访问角色的映射创建一个新的拦截器对象。在grpc.NewServer()函数中,我们可以传入interceptor.Unary()and interceptor.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()),
    )

    ...

  1. 定义一个新authorize()函数,它将以上下文和方法作为输入,如果请求未经授权,将返回错误。
func (interceptor *AuthInterceptor) authorize(ctx context.Context, method string) error 
    // TODO: implement this

  1. Unary()函数和Stream()函数中,我们使用interceptor.authorize()函数
func (interceptor *AuthInterceptor) Unary() grpc.UnaryServerInterceptor 
	return func(ctx context.Context, req interface以上是关于第十节——gRPC 拦截器的主要内容,如果未能解决你的问题,请参考以下文章

第十节——gRPC 拦截器

grpc-源码解析

第十节:numpy之数组文件操作

第十节

linux第十节课(补三月二十九)

第十节:python异常处理类