内容简介:(金庆的专栏 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_; };
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- SpringBlade 2.3.3 发布,重构令牌逻辑,增强令牌功能
- SessionId /认证令牌生成的最佳做法
- 技术讨论 | 使用CredSniper获取双因素认证令牌
- 天辰的救赎(JS)第二章(神秘令牌)
- 限流算法之漏桶算法、令牌桶算法
- SpringBlade 2.2 发布,升级多终端令牌认证系统
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Java 8函数式编程
[英] Richard Warburton / 王群锋 / 人民邮电出版社 / 2015-3 / 39.00元
通过每一章的练习快速掌握Java 8中的Lambda表达式 分析流、高级集合和其他Java 8类库的改进 利用多核CPU提高数据并发的性能 将现有代码库和库代码Lambda化 学习Lambda表达式单元测试和调试的实践解决方案 用Lambda表达式实现面向对象编程的SOLID原则 编写能有效执行消息传送和非阻塞I/O的并发应用一起来看看 《Java 8函数式编程》 这本书的介绍吧!