grpc加TLS加密和令牌认证

栏目: 服务器 · 发布时间: 5年前

内容简介:(金庆的专栏 2018.11)用 golang 创建 grpc 服务,开启 TLS 加密,并采用令牌认证。然后用 C++ 和 golang 分别创建客户端连接服务器。

grpc加TLS加密和令牌认证

(金庆的专栏 2018.11)

用 golang 创建 grpc 服务,开启 TLS 加密,并采用令牌认证。

然后用 C++ 和 golang 分别创建客户端连接服务器。

import (
	...
	grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
)

func main() {
	listen, err := net.Listen("tcp", ":12345")
	if err != nil {
		grpclog.Fatalf("failed to listen: %v", err)
	}
	
	// TLS认证
	creds, err := credentials.NewServerTLSFromFile("keys/server.crt", "keys/server.key")
	if err != nil {
		grpclog.Fatalf("Failed to generate credentials %v", err)
	}
	
	// 实例化grpc Server, 并开启TLS认证
	s := grpc.NewServer(grpc.Creds(creds),
		grpc_auth.UnaryServerInterceptor(auth.Authenticate),
		grpc_auth.StreamServerInterceptor(auth.Authenticate))
	
	// 注册HelloService
	pb.RegisterHelloServer(s, HelloService)
	grpclog.Println("Listen on " + Address + " with TLS")
	s.Serve(listen)
}

其中 server.key 是私钥,server.crt 是自签名证书,如下生成:

$ openssl genrsa -out server.key 2048
$ openssl req -new -x509 -sha256 -key server.key \
 -out server.crt -days 36500 \
 -subj /C=CN/ST=Shanghai/L=Songjiang/O=ztgame/OU=tech/CN=mydomain.ztgame.com/emailAddress=myname@ztgame.com

查看证书文件

$ openssl x509 -in server.crt -noout -text

auth.Authenticate 如下,作为 interceptor, 对每个请求进行令牌验证。

package auth

import (
	"context"
	"sync"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
)

// from token.yaml file
var tokenToAppName = &sync.Map{}

func init() {
	tokenToAppName.Store("test", "test")
}

// XXX load tokenToAppName from file

// Authenticate checks that a token exists and is valid.
// It removes the token from the context and
//  stores the app name of the token in the returned context
func Authenticate(ctx context.Context) (context.Context, error) {
	token, err := extractHeader(ctx, "authorization-token")
	if err != nil {
		return ctx, err
	}
	// Remove token from headers from here on
	ctx = purgeHeader(ctx, "authorization-token")

	valAppName, ok := tokenToAppName.Load(token)
	if !ok {
		return ctx, status.Errorf(codes.Unauthenticated, "no app for token '%s'", token)
	}
	appName := valAppName.(string)
	return context.WithValue(ctx, keyAppName{}, appName), nil
}

func extractHeader(ctx context.Context, header string) (string, error) {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return "", status.Error(codes.Unauthenticated, "no headers in request")
	}

	authHeaders, ok := md[header]
	if !ok {
		return "", status.Error(codes.Unauthenticated, "no header in request")
	}

	if len(authHeaders) != 1 {
		return "", status.Error(codes.Unauthenticated, "more than 1 header in request")
	}

	return authHeaders[0], nil
}

func purgeHeader(ctx context.Context, header string) context.Context {
	md, _ := metadata.FromIncomingContext(ctx)
	mdCopy := md.Copy()
	mdCopy[header] = nil
	return metadata.NewIncomingContext(ctx, mdCopy)
}

type keyAppName struct{}

// GetAppName can be used to extract app name stored in a context.
func GetAppName(ctx context.Context) string {
	// Authenticate()之后必然存在app name
	return ctx.Value(keyAppName{}).(string)
}

tokenToAppName 是一个map, 将合法的令牌映射为应用名。

每个应用(即用户)分配一个令牌,根据令牌可查到该用户是否合法,以及用户的其他信息。

这里只需要应用名。

每个请求将调用 Authenticate() , 该方法将从 http 头获取请求的令牌,查找对应的应用名,

ctx 中将删除令牌,替换成应用名。

GetAppName() 将从 ctx 中获取应用名。

服务方法实现如下:

func (s MailServer) Get(ctx context.Context, r *pb.GetRequest) (*pb.GetResponse, error) {
	app := auth.GetAppName(ctx)
	body, err := db.NewGetter(app).GetMailBody(r.MailIndex)
	return &pb.GetResponse{
		Result: getResult(err),
		Body:   body,
	}, nil
}

先获取应用名,然后根据应用名获取相应的数据返回。

// Create the client TLS credentials
	creds, err := credentials.NewClientTLSFromFile("key/server.crt", "mydomain.ztgame.com")
	if err != nil {
		panic(fmt.Errorf("could not load tls cert: %s", err))
	}

	// We don't need to error here, as this creates a pool and connections
	// will happen later
	conn, _ := grpc.Dial(
		serviceURL,
		grpc.WithTransportCredentials(creds),
		grpc.WithPerRPCCredentials(auth.TokenAuth{
			Token: "test",
		}))

	cli := pb.NewMailClient(conn)

客户端只需要 server.crt, 其中包含服务器的公钥。

NewClientTLSFromFile() 的第2个参数是个域名,是 server.crt 中的域名。

目前测试阶段还没有正式域名设置,所以输入一个指定域名用于验证 server.crt 中的域名。

生产环境运行时,应该不需要这个域名,可以直接查询 DNS 进行验证。

Dial() 输入一个 WithPerRPCCredentials 用于令牌验证。

auth.TokenAuth 需要实现 PerRPCCredentials 接口:

package auth

import (
	"context"
)

type TokenAuth struct {
	Token string
}

func (t TokenAuth) GetRequestMetadata(ctx context.Context, in ...string) (map[string]string, error) {
	return map[string]string{
		"authorization-token": t.Token,
	}, nil
}

func (TokenAuth) RequireTransportSecurity() bool {
	return true
}

“authorization-token” 是客户端和服务器约定好的http认证头字符串。

C++ 端的客户端代码比golang的稍复杂,因为 grpc C++ 库没有 grpc-go 成熟。

代码参照 grpc 示例 greeter_async_client2.cc:

int main(int argc, char** argv) {
    grpc::SslCredentialsOptions ssl_options;
    ssl_options.pem_root_certs = SERVER_CRT;
    // Create a default SSL ChannelCredentials object.
    auto channel_creds = grpc::SslCredentials(ssl_options);
    grpc::ChannelArguments cargs;
    cargs.SetSslTargetNameOverride("gamemail.ztgame.com");  // 如果加了 DNS 就不用这个了

    auto call_creds = grpc::MetadataCredentialsFromPlugin(
        std::unique_ptr<grpc::MetadataCredentialsPlugin>(new TokenAuthenticator(TOKEN)));

    auto compsited_creds = grpc::CompositeChannelCredentials(channel_creds, call_creds);

    // Create a channel using the credentials created in the previous step.
    auto channel = grpc::CreateCustomChannel("1.2.3.4:8000", compsited_creds, cargs);

    // Instantiate the client.
    MailClient tester(channel);
    ...

    return 0;
}

因为 C++ 没有提供从文件读取 server.crt 的接口,所以在此直接用了一个常量字符串:

ssl_options.pem_root_certs = SERVER_CRT;

SERVER_CRT 定义如下:

// server.crt 的内容
const char SERVER_CRT[] = R"(
-----BEGIN CERTIFICATE-----
TjERMA8GA1UECAwIU2hhbmdoYWkxEjAQBgNVBAcMCVNvbmdqaWFuZzEPMA0GA1UE
...
E6v50RCQgtWGmna+oy1I2UTVABdjBFnyKPEuz106mBfOhT6cg80hBHVgrV7sLHq8
76QolJm8yzZPL1qpiO4dKHHsCP6R
-----END CERTIFICATE-----
)";

TokenAuthenticator 定义如下,是个自定义认证插件:

// TokenAuthenticator 用来支持令牌认证
// https://grpc.io/docs/guides/auth.html
class TokenAuthenticator : public grpc::MetadataCredentialsPlugin {
public:
  TokenAuthenticator(const std::string& token) : token_(token) {}

  grpc::Status GetMetadata(
      grpc::string_ref service_url, grpc::string_ref method_name,
      const grpc::AuthContext& channel_auth_context,
      std::multimap<grpc::string, grpc::string>* metadata) override {
    metadata->insert(std::make_pair("authorization-token", token_));
    return grpc::Status::OK;
  }

private:
  std::string token_;
};

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

编程珠玑(第二版)

编程珠玑(第二版)

[美] Jon Bentley / 谢君英、石朝江 / 中国电力出版社 / 2004-4 / 28.00元

《编程珠玑(第2版)》是计算机科学方面的经典名著。书的内容围绕程序设计人员面对的一系列实际问题展开。作者Jon Bentley 以其独有的洞察力和创造力,引导读者理解这些问题并学会解决方法,而这些正是程序员实际编程生涯中至关重要的。一起来看看 《编程珠玑(第二版)》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试