当前位置: 代码迷 >> 综合 >> 第十节——gRPC 拦截器
  详细解决方案

第十节——gRPC 拦截器

热度:84   发布时间:2023-11-19 19:04:27.0

第十章——使用 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       stringHashedPassword stringRole           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) errorFind(username string) (*User, error)
}
  1. 定义一个InMemoryUserStore结构体来实现接口
    • 它有一个互斥锁来控制并发访问
    • 一个map存储用户
type InMemoryUserStore struct {
    mutex sync.RWMutexusers 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     stringtokenDuration time.Duration
}func NewJWTManager(secretKey string, tokenDuration time.Duration) *JWTManager {
    return &JWTManager{
    secretKey, tokenDuration}
}
  1. 安装jwt-go 库,命令:go get github.com/dgrijalva/jwt-go
  2. 定义一个UserClaims结构体。它将包含 JWTStandardClaims作为复合字段。
type UserClaims struct {
    jwt.StandardClaimsUsername 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.UnimplementedAuthServiceServeruserStore  UserStorejwtManager *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.LoginResponse{
    AccessToken: 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      *JWTManageraccessibleRoles map[string][]string
}
  1. 编写一个NewAuthInterceptor()函数来构建并返回一个新的身份验证拦截器对象
func NewAuthInterceptor(jwtManager *JWTManager, accessibleRoles map[string][]string) *AuthInterceptor {
    return &AuthInterceptor{
    jwtManager, 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 authorizationreturn 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 authorizationreturn 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{
    }, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{
    }, error) {
    log.Println("--> unary interceptor: ", info.FullMethod)err := interceptor.authorize(ctx, info.FullMethod)if err != nil {
    return nil, err}return handler(ctx, req)}
}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)err := interceptor.authorize(stream.Context(), info.FullMethod)if err != nil {
    return err}return handler(srv, stream)}
}
  1. 实现这个authorize()功能
    • 得到可以访问目标 RPC 方法的角色列表
    • 调用metadata.FromIncomingContext()以获取请求的元数据
    • authorization元数据键中获取值
    • 访问令牌应存储在值的第一个元素中。然后我们调用jwtManager.Verify()以验证令牌并取回声明。
    • 遍历可访问的角色以检查用户的角色是否可以访问此 RPC。如果在列表中找到用户的角色,我们只需返回nil。如果没有,我们返回PermissionDenied状态码,以及一条消息说用户没有访问这个 RPC 的权限
func (interceptor *AuthInterceptor) authorize(ctx context.Context, method string) error {
    accessibleRoles, ok := interceptor.accessibleRoles[method]if !ok {
    // everyone can accessreturn nil}md, ok := metadata.FromIncomingContext(ctx)if !ok {
    return status.Errorf(codes.Unauthenticated, "metadata is not provided")}values := md["authorization"]if len(values) == 0 {
    return status.Errorf(codes.Unauthenticated, "authorization token is not provided")}accessToken := values[0]claims, err := interceptor.jwtManager.Verify(accessToken)if err != nil {
    return status.Errorf(codes.Unauthenticated, "access token is invalid: %v", err)}for _, role := range accessibleRoles {
    if role == claims.Role {
    return nil}}return status.Error(codes.PermissionDenied, "no permission to access this RPC")
}

10.7、实现 Auth 服务客户端

  1. 在pcbook/client目录下创建auth_client.go文件
  2. 定义AuthClient调用身份验证服务的结构
type AuthClient struct {
    service  pb.AuthServiceClientusername stringpassword string
}
  1. 定义一个函数来创建和返回一个新AuthClient对象
func NewAuthClient(cc *grpc.ClientConn, username string, password string) *AuthClient {
    service := pb.NewAuthServiceClient(cc)return &AuthClient{
    service, username, password}
}
  1. 编写一个Login()函数来调用 Login RPC 来获取访问令牌
func (client *AuthClient) Login() (string, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()req := &pb.LoginRequest{
    Username: client.username,Password: client.password,}res, err := client.service.Login(ctx, req)if err != nil {
    return "", err}return res.GetAccessToken(), nil
}
  1. 在client目录下创建文件client/auth_interceptor.go,在调用服务器之前,我们将拦截所有 gRPC 请求并为它们附加访问令牌
  2. 定义一个AuthInterceptor结构
type AuthInterceptor struct {
    authClient  *AuthClientauthMethods map[string]boolaccessToken string
}
  1. NewAuthInterceptor()函数中,除了 auth 客户端和 auth 方法之外,我们还需要一个刷新令牌持续时间参数。它会告诉我们应该多久调用一次登录 API 来获取新令牌。
    • 用一个内部函数:scheduleRefreshToken()调度刷新访问令牌并传入刷新持续时间
func NewAuthInterceptor(authClient *AuthClient, authMethods map[string]bool, refreshDuration time.Duration) (*AuthInterceptor, error) {
    interceptor := &AuthInterceptor{
    authClient:  authClient,authMethods: authMethods,}err := interceptor.scheduleRefreshToken(refreshDuration)if err != nil {
    return nil, err}return interceptor, nil
}
  1. 实现这个scheduleRefreshToken()功能
    • 需要一个函数来刷新令牌而不进行调度。在这个函数中,我们只是使用 auth 客户端来登录用户。
    • 返回令牌后,我们只需将其存储在interceptor.accessToken字段中。我们在这里写一个简单的日志,稍后观察,最后返回nil
func (interceptor *AuthInterceptor) refreshToken() error {
    accessToken, err := interceptor.authClient.Login()if err != nil {
    return err}interceptor.accessToken = accessTokenlog.Printf("token refreshed: %v", accessToken)return nil
}func (interceptor *AuthInterceptor) scheduleRefreshToken(refreshDuration time.Duration) error {
    err := interceptor.refreshToken()if err != nil {
    return err}go func() {
    wait := refreshDurationfor {
    time.Sleep(wait)err := interceptor.refreshToken()if err != nil {
    wait = time.Second} else {
    wait = refreshDuration}}}()return nil
}
  1. 定义一个Unary()函数来返回一个 gRPC 一元客户端拦截器。
    • 在这个拦截器函数中,检查此方法是否需要身份验证
    • 需要则必须在调用实际的 RPC 之前将访问令牌附加到上下文
func (interceptor *AuthInterceptor) Unary() grpc.UnaryClientInterceptor {
    return func(ctx context.Context, method string, req, reply interface{
    }, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
    log.Printf("--> unary interceptor: %s", method)if interceptor.authMethods[method] {
    return invoker(interceptor.attachToken(ctx), method, req, reply, cc, opts...)}return invoker(ctx, method, req, reply, cc, opts...)}
}
  1. 定义一个新attachToken()函数来将令牌附加到输入上下文并返回结果。
    • 使用metadata.AppendToOutgoingContext(), 将输入上下文连同授权密钥和访问令牌值一起传递。
func (interceptor *AuthInterceptor) attachToken(ctx context.Context) context.Context {
    return metadata.AppendToOutgoingContext(ctx, "authorization", interceptor.accessToken)
}
  1. 实现流拦截器
func (interceptor *AuthInterceptor) Stream() grpc.StreamClientInterceptor {
    return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
    log.Printf("--> stream interceptor: %s", method)if interceptor.authMethods[method] {
    return streamer(interceptor.attachToken(ctx), desc, cc, method, opts...)}return streamer(ctx, desc, cc, method, opts...)}
}
  1. 重构cmd/client/main.go中main函数
    • 为 auth 客户端创建一个单独的连接,因为它将用于创建一个 auth 拦截器,该拦截器将用于为笔记本电脑客户端创建另一个连接
    • 连接名称更改为cc1,并用它创建一个新的身份验证客户端
    • 用 auth 客户端创建一个新的拦截器
    • 编写一个函数authMethods()来定义需要身份验证的方法列表。
const (username        = "admin1"password        = "secret"refreshDuration = 30 * time.Second
)func authMethods() map[string]bool {
    const laptopServicePath = "/techschool.pcbook.LaptopService/"return map[string]bool{
    laptopServicePath + "CreateLaptop": true,laptopServicePath + "UploadImage":  true,laptopServicePath + "RateLaptop":   true,}
}func main() {
    cc1, _ := grpc.Dial("localhost:8888", grpc.WithInsecure())authClient := client.NewAuthClient(cc1, username, password)interceptor, _ := client.NewAuthInterceptor(authClient, authMethods(), refreshDuration)
}
  1. 将拨号服务器以创建另一个连接。但这一次,我们还添加了 2 个拨号选项:一元拦截器和流拦截器。
func main() {
    cc1, _ := grpc.Dial("localhost:8888", grpc.WithInsecure())authClient := client.NewAuthClient(cc1, username, password)interceptor, _ := client.NewAuthInterceptor(authClient, authMethods(), refreshDuration)cc2, _ := grpc.Dial("localhost:8888",grpc.WithInsecure(),grpc.WithUnaryInterceptor(interceptor.Unary()),grpc.WithStreamInterceptor(interceptor.Stream()),)laptopClient := pb.NewLaptopServiceClient(cc2)testRateLaptop(laptopClient)
}
  1. 向Makefile文件中添加一句话:.PHONY: gen clean server client test
gen:protoc --proto_path=proto --go_out=pb --go-grpc_out=pb proto/*.protoclean:rm pb/*.gotest:go test -cover -race serializer/file_test.goserver:go run cmd/server/main.goclient:go run cmd/client/main.go.PHONY: gen clean server client test

10.8、启动服务观察效果

  1. 启动服务端与客户端,可以看到能够正常使用
  2. 修改cmd/cliwnt/main.go文件中的常量,修改为:将username=“admin"
const (username        = "admin"password        = "secret"refreshDuration = 30 * time.Second
)
  1. 在启动客户端发现无法正常工作,与服务器所规定的账户不一致
    grpc.WithInsecure(),
    grpc.WithUnaryInterceptor(interceptor.Unary()),
    grpc.WithStreamInterceptor(interceptor.Stream()),
    )

    laptopClient := pb.NewLaptopServiceClient(cc2)
    testRateLaptop(laptopClient)
    }


15. 向Makefile文件中添加一句话:`.PHONY: gen clean server client test````makefile
gen:protoc --proto_path=proto --go_out=pb --go-grpc_out=pb proto/*.protoclean:rm pb/*.gotest:go test -cover -race serializer/file_test.goserver:go run cmd/server/main.goclient:go run cmd/client/main.go.PHONY: gen clean server client test

10.8、启动服务观察效果

  1. 启动服务端与客户端,可以看到能够正常使用
  2. 修改cmd/cliwnt/main.go文件中的常量,修改为:将username=“admin"
const (username        = "admin"password        = "secret"refreshDuration = 30 * time.Second
)
  1. 在启动客户端发现无法正常工作,与服务器所规定的账户不一致
  相关解决方案