美文网首页
Dive into gRPC(5):验证客户端

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

作者: 起名难倒英雄汉 | 来源:发表于2018-10-08 09:56 被阅读0次

    安全通信中,我们在我们的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. 系列文章

    相关文章

      网友评论

          本文标题:Dive into gRPC(5):验证客户端

          本文链接:https://www.haomeiwen.com/subject/resdaftx.html