11c7fbd5cce3e378266e78afbac3e7bb
7 - grpc 认证鉴权 —— tls 认证

grpc 认证鉴权

在了解 grpc 认证鉴权之前,我们有必要先梳理一下认证鉴权方面的知识。

1、单体模式下的认证鉴权

在单体模式下,整个应用是一个进程,应用一般只需要一个统一的安全认证模块来实现用户认证鉴权。例如用户登陆时,安全模块验证用户名和密码的合法性。假如合法,为用户生成一个唯一的 Session。将 SessionId 返回给客户端,客户端一般将 SessionId 以 Cookie 的形式记录下来,并在后续请求中传递 Cookie 给服务端来验证身份。为了避免 Session Id被第三者截取和盗用,客户端和应用之前应使用 TLS 加密通信,session 也会设置有过期时间。

客户端访问服务端时,服务端一般会用一个拦截器拦截请求,取出 session id,假如 id 合法,则可判断客户端登陆。然后查询用户的权限表,判断用户是否具有执行某次操作的权限。

2、微服务模式下的认证鉴权

在微服务模式下,一个整体的应用可能被拆分为多个微服务,之前只有一个服务端,现在会存在多个服务端。对于客户端的单个请求,为保证安全,需要跟每个微服务都要重复上面的过程。这种模式每个微服务都要去实现相同的校验逻辑,肯定是非常冗余的。

用户身份认证

为了避免每个服务端都进行重复认证,采用一个服务进行统一认证。所以考虑一个单点登录的方案,用户只需要登录一次,就可以访问所有微服务。一般在 api 的 gateway 层提供对外服务的入口,所以可以在 api gateway 层提供统一的用户认证。

用户状态保持

由于 http 是一个无状态的协议,前面说到了单体模式下通过 cookie 保存用户状态, cookie 一般存储于浏览器中,用来保存用户的信息。但是 cookie 是有状态的。客户端和服务端在一次会话期间都需要维护 cookie 或者 sessionId,在微服务环境下,我们期望服务的认证是无状态的。所以我们一般采用 token 认证的方式,而非 cookie。

token 由服务端用自己的密钥加密生成,在客户端登录或者完成信息校验时返回给客户端,客户端认证成功后每次向服务端发送请求带上 token,服务端根据密钥进行解密,从而校验 token 的合法,假如合法则认证通过。token 这种方式的校验不需要服务端保存会话状态。方便服务扩展

3、grpc 认证鉴权

grpc-go 官方对于认证鉴权的介绍如下:https://github.com/grpc/grpc-go/blob/master/Documentation/grpc-auth-support.md

通过官方介绍可知, grpc-go 认证鉴权是通过 tls + oauth2 实现的。这里不对 tls 和 oauth2 进行详细介绍,假如有不清楚的可以参考阮一峰老师的教程,介绍得比较清楚

tls :http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html
oauth2 :http://www.ruanyifeng.com/blog/2019/04/oauth_design.html

下面我们就来具体看看 grpc-go 是如何实现认证鉴权的

grpc-go 官方 doc 说了这里关于 auth 的部分有 demo 放在 examples 目录下的 features 目录下。但是 demo 没有包括证书生成的步骤,这里我们自建一个 demo,从生成证书开始一步步进行 grpc 的认证讲解。

我们先创建一个文件夹 helloauth,然后把之前examples 目录下 helloworld demo 中的 client 和 server 的 go 文件全部 copy 过来,先执行 go mod init helloauth 来生成 go.mod 文件。由于 google.golang.org 被墙,所以执行 go mod edit -replace=google.golang.org/grpc=github.com/grpc/grpc-go@latest, 接着
注意把 替换成 pb "google.golang.org/grpc/examples/helloworld/helloworld" 替换成 pb "helloauth/helloworld" 来引用我们新生成的 pb 文件

生成证书

生成私钥

openssl ecparam -genkey -name secp384r1 -out server.key

自签公钥

openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650

填写信息(注意 Common Name 要填写服务名)

Country Name (2 letter code) []:
State or Province Name (full name) []:
Locality Name (eg, city) []:
Organization Name (eg, company) []:
Organizational Unit Name (eg, section) []:
Common Name (eg, fully qualified host name) []:helloauth
Email Address []:

生成完毕后,将证书文件放到 keys 目录下,整个项目目录结构如下:

使用证书进行 TLS 通信认证

我们之前的 helloworld demo 中,client 在创建 DialContext 指定非安全模式通信,如下:

    conn, err := grpc.Dial(address, grpc.WithInsecure())

这种模式下,client 和 server 都不会进行通信认证,其实是不安全的。下面我们来看看安全模式下应该如何通信

server

package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"

    pb "google.golang.org/grpc/examples/helloworld/helloworld"
)

const (
    port = ":50051"
)

// server is used to implement helloworld.GreeterServer.
type server struct{}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    log.Printf("Received: %v", in.Name)
    return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

func main() {
    c, err := credentials.NewServerTLSFromFile("../keys/server.pem", "../keys/server.key")
        if err != nil {
            log.Fatalf("credentials.NewServerTLSFromFile err: %v", err)
        }

    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer(grpc.Creds(c))
    pb.RegisterGreeterServer(s, &server{})
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

client

package main

import (
    "context"
    "log"
    "os"
    "time"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"

    pb "helloauth/helloworld"
)

const (
    address     = "localhost:50051"
    defaultName = "world"
)

func main() {
    cred, err := credentials.NewClientTLSFromFile("../keys/server.pem", "helloauth")
        if err != nil {
top Created with Sketch.