026e5ac94eee35011b3ce7ffc3e203f6
8 - grpc 认证鉴权 —— oauth2 认证

grpc 认证鉴权 —— oauth2

前面我们说了 tls 认证,tls 保证了 client 和 server 通信的安全性,但是无法做到接口级别的权限控制。例如有 A、B、C、D 四个系统,存在下面两个场景:
1、我们希望 A 可以访问 B、C 系统,但是不能访问 D 系统
2、B 系统提供了 b1、b2、b3 三个接口,我们希望 A 系统可以访问 b1、b2 接口,但是不能访问 b3 接口。
此时 tls 认证肯定是无法实现上面两个诉求的,对于这两个场景,grpc 提供了 oauth2 的认证方式。对 oauth2 不了解的同学可以参考 http://www.ruanyifeng.com/blog/2019/04/oauth_design.html

oauth2 认证鉴权实现

grpc 官方提供了对 oauth2 认证鉴权的实现 demo,放在 examples 目录的 features 目录的 authentication 目录下,我们来看一下源码实现

server

server 端源码实现如下:

func main() {
    flag.Parse()
    fmt.Printf("server starting on port %d...\n", *port)

    cert, err := tls.LoadX509KeyPair(testdata.Path("server1.pem"), testdata.Path("server1.key"))
    if err != nil {
        log.Fatalf("failed to load key pair: %s", err)
    }
    opts := []grpc.ServerOption{
        // The following grpc.ServerOption adds an interceptor for all unary
        // RPCs. To configure an interceptor for streaming RPCs, see:
        // https://godoc.org/google.golang.org/grpc#StreamInterceptor
        grpc.UnaryInterceptor(ensureValidToken),
        // Enable TLS for all incoming connections.
        grpc.Creds(credentials.NewServerTLSFromCert(&cert)),
    }
    s := grpc.NewServer(opts...)
    ecpb.RegisterEchoServer(s, &ecServer{})
    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

server 端先调用了 tls 包下的 LoadX509KeyPair,通过 server 的公钥和私钥生成了一个 Certificate 结构体来保存证书信息。然后注册了一个校验 token 的方法到拦截器中,并将证书信息设置到 serverOption 中,构造 server 的时候层层透传进去,最终会被设置到 Server 里面 ServerOptions 结构中的 credentials.TransportCredentials 和 UnaryServerInterceptor 中。

我们来看看这两个结构什么时候会被调用,先梳理调用链路,在 s.Serve ——> s.handleRawConn ——> s.serveStreams ——> s.handleStream ——> s.processUnaryRPC 方法中有一行

reply, appErr := md.Handler(srv.server, ctx, df, s.opts.unaryInt)

可以看到调用了 md.Handler 方法,将 s.opts.unaryInt 这个结构传入了进去。s.opts.unaryInt 就是我们之前注册的 UnaryServerInterceptor 拦截器。md 是一个 MethodDesc 这个结构,包括了 MethodName 和 Handler

type MethodDesc struct {
    MethodName string
    Handler    methodHandler
}

这里会取出我们之前注册进去的结构,还记得我们介绍 helloworld 时 RegisterService 吗?至于如何取出 MethodName,源码中的设计非常复杂,经过了层层包装,这里不是本节重点就不赘述了。

func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) {
    s.RegisterService(&_Greeter_serviceDesc, srv)
}

var _Greeter_serviceDesc = grpc.ServiceDesc{
    ServiceName: "helloworld.Greeter",
    HandlerType: (*GreeterServer)(nil),
    Methods: []grpc.MethodDesc{
        {
            MethodName: "SayHello",
            Handler:    _Greeter_SayHello_Handler,
        },
    },
    Streams:  []grpc.StreamDesc{},
    Metadata: "helloworld.proto",
}

我们看到 md.Handler 其实是 _Greeter_SayHello_Handler 这个结构,它也是在 pb 文件中生成的。

func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
    in := new(HelloRequest)
    if err := dec(in); err != nil {
        return nil, err
    }
    if interceptor == nil {
        return srv.(GreeterServer).SayHello(ctx, in)
    }
    info := &grpc.UnaryServerInfo{
        Server:     srv,
        FullMethod: "/helloworld.Greeter/SayHello",
    }
    handler := func(ctx context.Context, req interface{}) (interface{}, error) {
        return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest))
    }
    return interceptor(ctx, in, info, handler)
}

这里调用了我们传入的 interceptor 方法。回到我们的调用:

reply, appErr := md.Handler(srv.server, ctx, df, s.opts.unaryInt)

所以其实是调用了 s.opts.unaryInt 这个拦截器。这个拦截器是我们之前在 创建 server 的时候赋值的。

    opts := []grpc.ServerOption{
        // The following grpc.ServerOption adds an interceptor for all unary
        // RPCs. To configure an interceptor for streaming RPCs, see:
        // https://godoc.org/google.golang.org/grpc#StreamInterceptor
        grpc.UnaryInterceptor(ensureValidToken),
        // Enable TLS for all incoming connections.
        grpc.Creds(credentials.NewServerTLSFromCert(&cert)),
    }
    s := grpc.NewServer(opts...)

看 grpc.UnaryInterceptor 这个方法,其实是将 ensureValidToken 这个函数赋值给了 s.opts.unaryInt

    func UnaryInterceptor(i UnaryServerInterceptor) ServerOption {
        return newFuncServerOption(func(o *serverOptions) {
            if o.unaryInt != nil {
                panic("The unary server interceptor was already set and may not be reset.")
            }
            o.unaryInt = i
        })
    }

所以之前我们执行的这一行

    return interceptor(ctx, in, info, handler)

其实是执行了 ensureValidToken 这个函数,这个函数就是我们在 server 端定义的 token 校验的函数。先取出我们传入的 metadata 数据,然后校验 token

    func ensureValidToken(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
        md, ok := metadata.FromIncomingContext(ctx)
        if !ok {
            return nil, errMissingMetadata
        }
        // The keys within metadata.MD are normalized to lowercase.
        // See: https://godoc.org/google.golang.org/grpc/metadata#New
        if !valid(md["authorization"]) {
            return nil, errInvalidToken
        }
        // Continue execution of handler after ensuring a valid token.
        return handler(ctx, req)
}

校验完 token 后,最终执行了 handler(ctx, req)

    handler := func(ctx context.Context, req interface{}) (interface{}, error) {
top Created with Sketch.