Go gRPC服务端基于持久连接的连接时身份认证实现问询
Go gRPC服务端基于持久TCP连接的连接阶段认证方案
核心需求与疑问
需求是在TCP连接建立阶段完成用户身份认证,无需在每次gRPC请求中附带凭证,后续调用GetMyLoginTime时直接返回当前连接用户的登录时间。核心疑问:
- 在哪验证用户凭证并决定是否允许建立连接?
- 如何在gRPC方法处理器中关联当前连接的用户数据?
实现思路
通过自定义TCP监听器拦截连接建立过程,在TCP握手完成后、gRPC的HTTP/2握手前验证用户凭证;验证通过后记录用户登录时间,并通过连接的远程地址作为标识,在后续请求中关联用户数据。
具体实现步骤
1. 自定义认证监听器
实现net.Listener接口,在Accept方法中处理凭证验证:
package main import ( "errors" "log" "net" "strings" "sync" "time" "google.golang.org/grpc" "google.golang.org/grpc/peer" "google.golang.org/grpc/status" "google.golang.org/grpc/codes" test "your/proto/package/path" emptypb "google.golang.org/protobuf/types/known/emptypb" timestamppb "google.golang.org/protobuf/types/known/timestamppb" ) type AuthListener struct { net.Listener userStore *sync.Map // key: 远程地址字符串, value: 用户登录时间time.Time } // Accept 拦截TCP连接,先验证凭证再交给gRPC服务器 func (l *AuthListener) Accept() (net.Conn, error) { conn, err := l.Listener.Accept() if err != nil { return nil, err } // 读取客户端发送的凭证(约定格式:username:password\n) buf := make([]byte, 1024) n, err := conn.Read(buf) if err != nil { conn.Close() return nil, err } credStr := strings.TrimSpace(string(buf[:n])) credParts := strings.Split(credStr, ":") if len(credParts) != 2 || credParts[0] != "validUser" || credParts[1] != "validPass" { conn.Close() return nil, errors.New("invalid credentials") } // 验证通过,记录登录时间 loginTime := time.Now() l.userStore.Store(conn.RemoteAddr().String(), loginTime) return conn, nil }
2. 实现gRPC服务端逻辑
创建gRPC服务器时使用自定义监听器,并实现GetMyLoginTime方法:
type testServer struct { test.UnimplementedTestServer userStore *sync.Map } // GetMyLoginTime 返回当前连接用户的登录时间 func (s *testServer) GetMyLoginTime(ctx context.Context, _ *emptypb.Empty) (*timestamppb.Timestamp, error) { // 从请求上下文获取连接的远程地址 p, ok := peer.FromContext(ctx) if !ok { return nil, status.Errorf(codes.Unauthenticated, "无法获取连接信息") } remoteAddr := p.Addr.String() // 从存储中读取登录时间 val, exists := s.userStore.Load(remoteAddr) if !exists { return nil, status.Errorf(codes.Unauthenticated, "当前连接未完成认证") } loginTime, ok := val.(time.Time) if !ok { return nil, status.Errorf(codes.Internal, "用户数据格式错误") } return timestamppb.New(loginTime), nil } func main() { userStore := &sync.Map{} // 创建基础TCP监听器 lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("监听失败: %v", err) } // 包装为认证监听器 authLis := &AuthListener{Listener: lis, userStore: userStore} // 创建gRPC服务器,可选:监听连接状态变更,清理过期用户数据 s := grpc.NewServer(grpc.WithConnStateChange(func(conn *grpc.ClientConn, state grpc.ConnState) { if state == grpc.Idle || state == grpc.Shutdown { userStore.Delete(conn.RemoteAddr().String()) } })) test.RegisterTestServer(s, &testServer{userStore: userStore}) log.Println("gRPC服务启动,监听端口50051") if err := s.Serve(authLis); err != nil { log.Fatalf("服务启动失败: %v", err) } }
3. 客户端实现(配合认证流程)
客户端需要先发送凭证,再初始化gRPC连接:
package main import ( "log" "net" test "your/proto/package/path" emptypb "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) func main() { // 先建立TCP连接 tcpConn, err := net.Dial("tcp", ":50051") if err != nil { log.Fatalf("TCP连接失败: %v", err) } defer tcpConn.Close() // 发送认证凭证 _, err = tcpConn.Write([]byte("validUser:validPass\n")) if err != nil { log.Fatalf("发送凭证失败: %v", err) } // 将已认证的TCP连接包装为gRPC连接 grpcConn, err := grpc.NewClient("", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(func(_ context.Context, _ string) (net.Conn, error) { return tcpConn, nil }), ) if err != nil { log.Fatalf("创建gRPC客户端失败: %v", err) } defer grpcConn.Close() client := test.NewTestClient(grpcConn) resp, err := client.GetMyLoginTime(nil, &emptypb.Empty{}) if err != nil { log.Fatalf("调用GetMyLoginTime失败: %v", err) } log.Printf("登录时间: %v", resp.AsTime()) }
疑问解答
如何获取关联的底层TCP连接?
无需直接操作TCP连接,通过peer.FromContext(ctx)可获取连接的远程地址,以此作为标识关联用户数据;若确实需要原始连接,可在自定义AuthListener的Accept方法中拿到net.Conn实例。在哪验证用户凭证并判断连接是否可建立?
在自定义AuthListener的Accept方法中完成:TCP连接建立后,先读取客户端发送的凭证,验证不通过则直接关闭连接;验证通过后才将连接交给gRPC服务器处理后续的HTTP/2握手和gRPC请求。
内容的提问来源于stack exchange,提问作者xakepp35
相关产品推荐
相关产品推荐

