Dive into gRPC(5):验证客户端 - Go语言中文社区

Dive into gRPC(5):验证客户端


安全通信中,我们在我们的simplemath服务中加入了SSL/TLS,使得客户端可以验证服务器。在这篇文章中,我们在simplemath服务中加入验证客户端的功能。

在gRPC中,有一个有意思的功能,就是允许服务器对来自客户端的每一个请求进行拦截,客户端可以把一些信息加入到请求中,服务器就可以在拦截中获取这些信息来对客户端进行验证。

为了达到这个效果,我们需要更新我们的客户端代码,将验证信息(比如用户名和密码)加入每一次请求中的metadata中,然后服务器就可以在每一次请求中进行验证。

1. 修改客户端代码

在客户端,我们需要在我们的grpc.Dial()函数中指定一个DialOption。具体的代码如下(simplemath/client/rpc/simplemath.go):

// AuthItem holds the username/password
type AuthItem struct {
    Username string
    Password string
}

// GetRequestMetadata gets the current request metadata
func (a *AuthItem) GetRequestMetadata(context.Context, ...string) (map[string]string, error) {
    return map[string]string{
        "username": a.Username,
        "password": a.Password,
    }, nil
}

// RequireTransportSecurity indicates whether the credentials requires transport security
func (a *AuthItem) RequireTransportSecurity() bool {
    return true
}

func getGRPCConn() (conn *grpc.ClientConn, err error) {
    // Setup the username/password
    auth := AuthItem{
        Username: "valineliu",
        Password: "root",
    }
    creds, err := credentials.NewClientTLSFromFile("../cert/server.crt", "")
    return grpc.Dial(address, grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(&auth))
}

上面就是主要改变的地方,其它的地方没有变化。

2. Break it down

首先,我们定义了一个结构体AuthItem用来存储服务器需要客户端携带的验证信息,即用户名(Username)和密码(Password)。在客户端的每一次调用中,都会携带这个结构体定义的信息。不过,这里仅仅是用户名和密码,在工程中,我们可以定义任何需要的信息;

在我们的getGRPCConn函数中,我们使用auth变量来存储我们的信息;

接下来,我们使用grpc.WithPerRPCCredentials函数来创建一个DialOption对象,并使用这个对象来作为grpc.Dial的参数(grpc.Dial函数是一个可变数量参数函数,可以放入多个参数,前面的grpc.WithTransportCredentials(creds)就是一个DialOption);

函数grpc.WithPerRPCCredentials的参数是一个接口credentials.PerRPCCredentials,因此我们的AuthItem需要实现相应的函数,包括GetRequestMetadataRequireTransportSecurity

在我们的GetRequestMetadata函数中,我们仅仅返回一个AuthItem结构体的map;

最后,在我们的RequireTransportSecurity函数中,我们总是返回true,表明我们的grpc客户端是否需要将metadata数据注入到传输层。我们也可以通过读取配置文件进行设置这个返回值。

这样,我们客户端的部分就修改完毕了。

3. 修改服务器端代码

在上面的代码中,我们的客户端可以将一些验证信息加入到每一次请求中,并发送给服务器。所以我们的服务器也要做一些相应的修改。代码如下(simplemath/server/main.go):

package main

import (
    "fmt"
    "golang.org/x/net/context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/metadata"
    "google.golang.org/grpc/reflection"
    "log"
    "net"
    pb "simplemath/api"
    "simplemath/server/rpcimpl"
    "strings"
)

const (
    port = ":50051"
)

// authenticateClient check the client credentials
func authenticateClient(ctx context.Context) (string, error) {
    if md, ok := metadata.FromIncomingContext(ctx); ok {
        clientUsername := strings.Join(md["username"], "")
        clientPassword := strings.Join(md["password"], "")
        if clientUsername != "valineliu" {
            return "", fmt.Errorf("unknown user %s", clientUsername)
        }
        if clientPassword != "root" {
            return "", fmt.Errorf("wrong password %s", clientPassword)
        }
        log.Printf("authenticated client: %s", clientUsername)
        return "9527", nil
    }
    return "", fmt.Errorf("missing credentials")
}

// unaryInterceptor calls authenticateClient with current context
func unaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    clientID, err := authenticateClient(ctx)
    if err != nil {
        return nil, err
    }
    ctx = context.WithValue(ctx, "clientID", clientID)
    return handler(ctx, req)
}

func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    creds, err := credentials.NewServerTLSFromFile("../cert/server.crt", "../cert/server.key")
    if err != nil {
        log.Fatalf("could not load TLS keys: %s", err)
    }
    // Create an array of gRPC options with the credentials
    opts := []grpc.ServerOption{grpc.Creds(creds), grpc.UnaryInterceptor(unaryInterceptor)}
    s := grpc.NewServer(opts...)
    pb.RegisterSimpleMathServer(s, &rpcimpl.SimpleMathServer{})
    reflection.Register(s)
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

以上就是全部的修改,其它的地方没有变化。

4. Break it down again

下面我们看看我们究竟做了什么变化。

首先,和在客户端中加入DialOption对应,我们在服务器端加入一个新的grpc.ServerOption到之前的数组里(opts):grpc.UnaryInterceptor。为了构造这个参数,我们将之前定义的函数unaryInterceptor传进去,这样每一次调用服务器都可以调用这个unaryInterceptor函数(叙述不准确,只有调用我们的GreatCommonDivisor函数才有效果,原因后面再说);

在我们定义的unaryInterceptor函数中,我们通过authenticateClient函数进行验证,并得到一个clientID,随后将这个ID插入到context中;

在我们定义的authenticateClient函数中,我们从metadata中获取验证所需要的数据,然后做一个简单的验证。验证成功了,就返回一个ID。

这就是服务器中主要的修改,关于metadata和interceptor的具体含义,会在以后的文章中进行具体的介绍,这里仅仅了解大概的含义即可。

5. Let them talk

最后,我们编译客户端和服务器的代码,启动服务器后,执行客户端命令:

$ ./client gcd 12 15

结果如下:

2018/10/08 09:47:38 The Greatest Common Divisor of 12 and 15 is 3

说明验证通过了。而在我们的服务器端有如下输出:

2018/10/08 09:47:38 authenticated client: valineliu

之后,我们修改我们的客户端中验证的信息:

auth := AuthItem{
        Username: "valineliu",
        Password: "badroot",
}

编译后重新运行,结果如下:

2018/10/08 09:50:07 could not compute: rpc error: code = Unknown desc = wrong password badroot

在描述的信息中,指出了密码错误。而在服务器端,没有输出,说明验证不通过。

在上面的描述中,我们通过一个简单的例子做了演示,而在实际场景中,这样的验证逻辑可以更加复杂与智能。

To Be Continued~

6. 系列文章

版权声明:本文来源简书,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://www.jianshu.com/p/fa43c54df957
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-02-02 14:50:58
  • 阅读 ( 1355 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢