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

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

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

    用户身份认证

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

    用户状态保持

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

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

    grpc-go 官方对于认证鉴权的介绍如下:

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

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

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

    我们先创建一个文件夹 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 文件

    生成证书

    生成私钥

    使用私钥生成证书

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

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

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

    使用证书进行 TLS 通信认证

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

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

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

    server

    1. package main
    2. import (
    3. "context"
    4. "log"
    5. "net"
    6. "google.golang.org/grpc"
    7. "google.golang.org/grpc/credentials"
    8. pb "google.golang.org/grpc/examples/helloworld/helloworld"
    9. )
    10. const (
    11. port = ":50051"
    12. // server is used to implement helloworld.GreeterServer.
    13. type server struct{}
    14. // SayHello implements helloworld.GreeterServer
    15. func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    16. log.Printf("Received: %v", in.Name)
    17. return &pb.HelloReply{Message: "Hello " + in.Name}, nil
    18. }
    19. c, err := credentials.NewServerTLSFromFile("../keys/server.pem", "../keys/server.key")
    20. if err != nil {
    21. log.Fatalf("credentials.NewServerTLSFromFile err: %v", err)
    22. }
    23. lis, err := net.Listen("tcp", port)
    24. if err != nil {
    25. log.Fatalf("failed to listen: %v", err)
    26. }
    27. s := grpc.NewServer(grpc.Creds(c))
    28. pb.RegisterGreeterServer(s, &server{})
    29. if err := s.Serve(lis); err != nil {
    30. log.Fatalf("failed to serve: %v", err)
    31. }
    32. }

    client

    这里的代码已经上传 github 了,详见:https://github.com/diubrother/helloauth

    server

    先来看 server 端,server 端根据 server 的公钥和私钥生成了一个 TransportCredentials ,如下:

    1. c, err := credentials.NewServerTLSFromFile("../keys/server.pem", "../keys/server.key")
    2. func NewServerTLSFromFile(certFile, keyFile string) (TransportCredentials, error) {
    3. cert, err := tls.LoadX509KeyPair(certFile, keyFile)
    4. if err != nil {
    5. return nil, err
    6. }
    7. return NewTLS(&tls.Config{Certificates: []tls.Certificate{cert}}), nil
    8. }

    看一下 NewTLS 这个方法,他其实就返回了一个 tlsCreds 的结构体,这个结构体实现了 TransportCredentials 这个接口,包括 ClientHandshake 和 ServerHandshake 。

    1. func NewTLS(c *tls.Config) TransportCredentials {
    2. tc := &tlsCreds{cloneTLSConfig(c)}
    3. tc.config.NextProtos = appendH2ToNextProtos(tc.config.NextProtos)
    4. return tc
    1. func (c *tlsCreds) ServerHandshake(rawConn net.Conn) (net.Conn, AuthInfo, error) {
    2. conn := tls.Server(rawConn, c.config)
    3. if err := conn.Handshake(); err != nil {
    4. return nil, nil, err
    5. }
    6. }

    client

    和 server 端类似,client 端也是通过公钥和服务名先创建一个 TransportCredentials

    1. cred, err := credentials.NewClientTLSFromFile("../keys/server.pem", "helloauth")

    看一下 NewClientTLSFromFile 这个方法,发现它也是调用了相同的 NewTLS 方法返回了一个 tlsCreds 结构体,跟 server 简直一模一样。

    接下来在创建客户端连接时,将 tlsCreds 这个结构体传了进去。

    1. conn, err := grpc.Dial(address, grpc.WithTransportCredentials(cred))

    Dial —— > DialContext 方法中有这么一段代码,将我们传入的 serverName 也就是 “helloauth” 赋值给了 clientConn 的 authority 这个字段。

    1. creds := cc.dopts.copts.TransportCredentials
    2. if creds != nil && creds.Info().ServerName != "" {
    3. cc.authority = creds.Info().ServerName
    4. } else if cc.dopts.insecure && cc.dopts.authority != "" {
    5. cc.authority = cc.dopts.authority
    6. } else {
    7. // Use endpoint from "scheme://authority/endpoint" as the default
    8. // authority for ClientConn.
    9. cc.authority = cc.parsedTarget.Endpoint
    10. }

    认证过程

    client

    那什么时候开始认证呢?先来说说 client。

    client 的认证其实是在调用 connect 方法的时候,在之前讲述负载均衡时降到了,在 acBalancerWrapper 里面有一个 UpdateAddresses 方法,调用 ac.connect() ——> ac.resetTransport() ——> ac.tryAllAddrs ——> ac.createTransport ——> transport.NewClientTransport ——> newHTTP2Client 方法时,有这么一段代码:

    transportCreds := opts.TransportCredentials perRPCCreds := opts.PerRPCCredentials

    1. if b := opts.CredsBundle; b != nil {
    2. if t := b.TransportCredentials(); t != nil {
    3. transportCreds = t
    4. }
    5. if t := b.PerRPCCredentials(); t != nil {
    6. perRPCCreds = append(perRPCCreds, t)
    7. }
    8. }
    9. if transportCreds != nil {
    10. scheme = "https"
    11. conn, authInfo, err = transportCreds.ClientHandshake(connectCtx, addr.Authority, conn)
    12. if err != nil {
    13. return nil, connectionErrorf(isTemporary(err), err, "transport: authentication handshake failed: %v", err)
    14. }
    15. isSecure = true
    16. }

    这里即调用了tlsCreds 的 ClientHandshake 方法进行握手,实现客户端的认证。

    server

    再来说说 server

    server 的认证其实是在调用 Serve ——> handleRawConn ——> useTransportAuthenticator 方法,调用了 s.opts.creds.ServerHandshake(rawConn) 方法,其底层也是调用 tlsCreds ServerHandshake 方法进行服务端握手。

    1. func (s *Server) useTransportAuthenticator(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
    2. if s.opts.creds == nil {
    3. return rawConn, nil, nil
    4. }