فهرست منبع

feat: gRPC 接入模式新增 Logout 接口

BaiLuoYan 1 هفته پیش
والد
کامیت
5a44811afb
5فایلهای تغییر یافته به همراه208 افزوده شده و 13 حذف شده
  1. 38 0
      internal/server/permserver.go
  2. 101 12
      pb/perm.pb.go
  3. 8 0
      pb/perm.proto
  4. 55 1
      pb/perm_grpc.pb.go
  5. 6 0
      permclient/permclient.go

+ 38 - 0
internal/server/permserver.go

@@ -10,6 +10,7 @@ import (
 	"perms-system-server/internal/consts"
 	authHelper "perms-system-server/internal/logic/auth"
 	pub "perms-system-server/internal/logic/pub"
+	"perms-system-server/internal/loaders"
 	"perms-system-server/internal/middleware"
 	userModel "perms-system-server/internal/model/user"
 	"perms-system-server/internal/svc"
@@ -358,3 +359,40 @@ func (s *PermServer) GetUserPerms(ctx context.Context, req *pb.GetUserPermsReq)
 		Perms:      ud.Perms,
 	}, nil
 }
+
+// Logout 用户登出。解析 accessToken 获取用户身份,递增 tokenVersion 使所有令牌立即失效并清除缓存。
+func (s *PermServer) Logout(ctx context.Context, req *pb.LogoutReq) (*pb.LogoutResp, error) {
+	if req.AccessToken == "" {
+		return nil, status.Error(codes.InvalidArgument, "accessToken不能为空")
+	}
+
+	token, err := middleware.ParseWithHMAC(req.AccessToken, s.svcCtx.Config.Auth.AccessSecret, &middleware.Claims{})
+	if err != nil || !token.Valid {
+		return nil, status.Error(codes.Unauthenticated, "accessToken无效或已过期")
+	}
+
+	claims, ok := token.Claims.(*middleware.Claims)
+	if !ok || claims.TokenType != consts.TokenTypeAccess {
+		return nil, status.Error(codes.Unauthenticated, "accessToken无效")
+	}
+
+	if s.svcCtx.TokenOpLimiter != nil {
+		code, _ := s.svcCtx.TokenOpLimiter.Take(fmt.Sprintf("logout:%d", claims.UserId))
+		if code == limit.OverQuota {
+			return nil, status.Error(codes.ResourceExhausted, "操作过于频繁,请稍后再试")
+		}
+	}
+
+	if _, err := s.svcCtx.SysUserModel.IncrementTokenVersion(ctx, claims.UserId, claims.Username); err != nil {
+		if !errors.Is(err, userModel.ErrUpdateConflict) {
+			return nil, status.Error(codes.Internal, "登出失败")
+		}
+		logx.WithContext(ctx).Infof("grpc logout on already-deleted user userId=%d, treated as idempotent success", claims.UserId)
+	}
+
+	cleanCtx, cancel := loaders.DetachCacheCleanCtx(ctx)
+	defer cancel()
+	s.svcCtx.UserDetailsLoader.Clean(cleanCtx, claims.UserId)
+
+	return &pb.LogoutResp{}, nil
+}

+ 101 - 12
pb/perm.pb.go

@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.36.11
-// 	protoc        v6.33.4
+// 	protoc        v7.34.1
 // source: pb/perm.proto
 
 package pb
@@ -729,6 +729,86 @@ func (x *GetUserPermsResp) GetMemberType() string {
 	return ""
 }
 
+type LogoutReq struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	AccessToken   string                 `protobuf:"bytes,1,opt,name=accessToken,proto3" json:"accessToken,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *LogoutReq) Reset() {
+	*x = LogoutReq{}
+	mi := &file_pb_perm_proto_msgTypes[11]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *LogoutReq) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*LogoutReq) ProtoMessage() {}
+
+func (x *LogoutReq) ProtoReflect() protoreflect.Message {
+	mi := &file_pb_perm_proto_msgTypes[11]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use LogoutReq.ProtoReflect.Descriptor instead.
+func (*LogoutReq) Descriptor() ([]byte, []int) {
+	return file_pb_perm_proto_rawDescGZIP(), []int{11}
+}
+
+func (x *LogoutReq) GetAccessToken() string {
+	if x != nil {
+		return x.AccessToken
+	}
+	return ""
+}
+
+type LogoutResp struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *LogoutResp) Reset() {
+	*x = LogoutResp{}
+	mi := &file_pb_perm_proto_msgTypes[12]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *LogoutResp) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*LogoutResp) ProtoMessage() {}
+
+func (x *LogoutResp) ProtoReflect() protoreflect.Message {
+	mi := &file_pb_perm_proto_msgTypes[12]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use LogoutResp.ProtoReflect.Descriptor instead.
+func (*LogoutResp) Descriptor() ([]byte, []int) {
+	return file_pb_perm_proto_rawDescGZIP(), []int{12}
+}
+
 var File_pb_perm_proto protoreflect.FileDescriptor
 
 const file_pb_perm_proto_rawDesc = "" +
@@ -789,13 +869,18 @@ const file_pb_perm_proto_rawDesc = "" +
 	"\x05perms\x18\x01 \x03(\tR\x05perms\x12\x1e\n" +
 	"\n" +
 	"memberType\x18\x02 \x01(\tR\n" +
-	"memberType2\xa5\x02\n" +
+	"memberType\"-\n" +
+	"\tLogoutReq\x12 \n" +
+	"\vaccessToken\x18\x01 \x01(\tR\vaccessToken\"\f\n" +
+	"\n" +
+	"LogoutResp2\xce\x02\n" +
 	"\vPermService\x12B\n" +
 	"\x0fSyncPermissions\x12\x16.pb.SyncPermissionsReq\x1a\x17.pb.SyncPermissionsResp\x12$\n" +
 	"\x05Login\x12\f.pb.LoginReq\x1a\r.pb.LoginResp\x129\n" +
 	"\fRefreshToken\x12\x13.pb.RefreshTokenReq\x1a\x14.pb.RefreshTokenResp\x126\n" +
 	"\vVerifyToken\x12\x12.pb.VerifyTokenReq\x1a\x13.pb.VerifyTokenResp\x129\n" +
-	"\fGetUserPerms\x12\x13.pb.GetUserPermsReq\x1a\x14.pb.GetUserPermsRespB\x18Z\x16perms-system-server/pbb\x06proto3"
+	"\fGetUserPerms\x12\x13.pb.GetUserPermsReq\x1a\x14.pb.GetUserPermsResp\x12'\n" +
+	"\x06Logout\x12\r.pb.LogoutReq\x1a\x0e.pb.LogoutRespB\x18Z\x16perms-system-server/pbb\x06proto3"
 
 var (
 	file_pb_perm_proto_rawDescOnce sync.Once
@@ -809,7 +894,7 @@ func file_pb_perm_proto_rawDescGZIP() []byte {
 	return file_pb_perm_proto_rawDescData
 }
 
-var file_pb_perm_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
+var file_pb_perm_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
 var file_pb_perm_proto_goTypes = []any{
 	(*PermItem)(nil),            // 0: pb.PermItem
 	(*SyncPermissionsReq)(nil),  // 1: pb.SyncPermissionsReq
@@ -822,6 +907,8 @@ var file_pb_perm_proto_goTypes = []any{
 	(*VerifyTokenResp)(nil),     // 8: pb.VerifyTokenResp
 	(*GetUserPermsReq)(nil),     // 9: pb.GetUserPermsReq
 	(*GetUserPermsResp)(nil),    // 10: pb.GetUserPermsResp
+	(*LogoutReq)(nil),           // 11: pb.LogoutReq
+	(*LogoutResp)(nil),          // 12: pb.LogoutResp
 }
 var file_pb_perm_proto_depIdxs = []int32{
 	0,  // 0: pb.SyncPermissionsReq.perms:type_name -> pb.PermItem
@@ -830,13 +917,15 @@ var file_pb_perm_proto_depIdxs = []int32{
 	5,  // 3: pb.PermService.RefreshToken:input_type -> pb.RefreshTokenReq
 	7,  // 4: pb.PermService.VerifyToken:input_type -> pb.VerifyTokenReq
 	9,  // 5: pb.PermService.GetUserPerms:input_type -> pb.GetUserPermsReq
-	2,  // 6: pb.PermService.SyncPermissions:output_type -> pb.SyncPermissionsResp
-	4,  // 7: pb.PermService.Login:output_type -> pb.LoginResp
-	6,  // 8: pb.PermService.RefreshToken:output_type -> pb.RefreshTokenResp
-	8,  // 9: pb.PermService.VerifyToken:output_type -> pb.VerifyTokenResp
-	10, // 10: pb.PermService.GetUserPerms:output_type -> pb.GetUserPermsResp
-	6,  // [6:11] is the sub-list for method output_type
-	1,  // [1:6] is the sub-list for method input_type
+	11, // 6: pb.PermService.Logout:input_type -> pb.LogoutReq
+	2,  // 7: pb.PermService.SyncPermissions:output_type -> pb.SyncPermissionsResp
+	4,  // 8: pb.PermService.Login:output_type -> pb.LoginResp
+	6,  // 9: pb.PermService.RefreshToken:output_type -> pb.RefreshTokenResp
+	8,  // 10: pb.PermService.VerifyToken:output_type -> pb.VerifyTokenResp
+	10, // 11: pb.PermService.GetUserPerms:output_type -> pb.GetUserPermsResp
+	12, // 12: pb.PermService.Logout:output_type -> pb.LogoutResp
+	7,  // [7:13] is the sub-list for method output_type
+	1,  // [1:7] is the sub-list for method input_type
 	1,  // [1:1] is the sub-list for extension type_name
 	1,  // [1:1] is the sub-list for extension extendee
 	0,  // [0:1] is the sub-list for field type_name
@@ -853,7 +942,7 @@ func file_pb_perm_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: unsafe.Slice(unsafe.StringData(file_pb_perm_proto_rawDesc), len(file_pb_perm_proto_rawDesc)),
 			NumEnums:      0,
-			NumMessages:   11,
+			NumMessages:   13,
 			NumExtensions: 0,
 			NumServices:   1,
 		},

+ 8 - 0
pb/perm.proto

@@ -16,6 +16,8 @@ service PermService {
   rpc VerifyToken(VerifyTokenReq) returns (VerifyTokenResp);
   // GetUserPerms 查询用户权限。产品服务端通过 appKey/appSecret 认证后查询指定用户在该产品下的成员类型和权限列表。
   rpc GetUserPerms(GetUserPermsReq) returns (GetUserPermsResp);
+  // Logout 用户登出。使当前 accessToken 对应用户的所有令牌立即失效(递增 tokenVersion)。
+  rpc Logout(LogoutReq) returns (LogoutResp);
 }
 
 message PermItem {
@@ -89,3 +91,9 @@ message GetUserPermsResp {
   repeated string perms = 1;
   string memberType = 2;
 }
+
+message LogoutReq {
+  string accessToken = 1;
+}
+
+message LogoutResp {}

+ 55 - 1
pb/perm_grpc.pb.go

@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
 // versions:
 // - protoc-gen-go-grpc v1.6.1
-// - protoc             v6.33.4
+// - protoc             v7.34.1
 // source: pb/perm.proto
 
 package pb
@@ -24,17 +24,27 @@ const (
 	PermService_RefreshToken_FullMethodName    = "/pb.PermService/RefreshToken"
 	PermService_VerifyToken_FullMethodName     = "/pb.PermService/VerifyToken"
 	PermService_GetUserPerms_FullMethodName    = "/pb.PermService/GetUserPerms"
+	PermService_Logout_FullMethodName          = "/pb.PermService/Logout"
 )
 
 // PermServiceClient is the client API for PermService service.
 //
 // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+//
+// PermService 权限管理系统 gRPC 服务,供接入产品的服务端调用。
 type PermServiceClient interface {
+	// SyncPermissions 同步权限声明。产品服务端通过 appKey/appSecret 认证后批量同步权限定义(新增/更新/禁用不在列表中的权限)。
 	SyncPermissions(ctx context.Context, in *SyncPermissionsReq, opts ...grpc.CallOption) (*SyncPermissionsResp, error)
+	// Login 产品端登录。产品成员通过用户名密码 + productCode 登录,返回 JWT 令牌对及用户权限信息。
 	Login(ctx context.Context, in *LoginReq, opts ...grpc.CallOption) (*LoginResp, error)
+	// RefreshToken 刷新令牌。使用有效的 refreshToken 换取新的令牌对,旧令牌即时失效(单会话轮转)。
 	RefreshToken(ctx context.Context, in *RefreshTokenReq, opts ...grpc.CallOption) (*RefreshTokenResp, error)
+	// VerifyToken 验证令牌。校验 accessToken 的有效性(签名、过期、用户状态、产品状态、成员资格、tokenVersion),返回用户权限信息。
 	VerifyToken(ctx context.Context, in *VerifyTokenReq, opts ...grpc.CallOption) (*VerifyTokenResp, error)
+	// GetUserPerms 查询用户权限。产品服务端通过 appKey/appSecret 认证后查询指定用户在该产品下的成员类型和权限列表。
 	GetUserPerms(ctx context.Context, in *GetUserPermsReq, opts ...grpc.CallOption) (*GetUserPermsResp, error)
+	// Logout 用户登出。使当前 accessToken 对应用户的所有令牌立即失效(递增 tokenVersion)。
+	Logout(ctx context.Context, in *LogoutReq, opts ...grpc.CallOption) (*LogoutResp, error)
 }
 
 type permServiceClient struct {
@@ -95,15 +105,34 @@ func (c *permServiceClient) GetUserPerms(ctx context.Context, in *GetUserPermsRe
 	return out, nil
 }
 
+func (c *permServiceClient) Logout(ctx context.Context, in *LogoutReq, opts ...grpc.CallOption) (*LogoutResp, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(LogoutResp)
+	err := c.cc.Invoke(ctx, PermService_Logout_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
 // PermServiceServer is the server API for PermService service.
 // All implementations must embed UnimplementedPermServiceServer
 // for forward compatibility.
+//
+// PermService 权限管理系统 gRPC 服务,供接入产品的服务端调用。
 type PermServiceServer interface {
+	// SyncPermissions 同步权限声明。产品服务端通过 appKey/appSecret 认证后批量同步权限定义(新增/更新/禁用不在列表中的权限)。
 	SyncPermissions(context.Context, *SyncPermissionsReq) (*SyncPermissionsResp, error)
+	// Login 产品端登录。产品成员通过用户名密码 + productCode 登录,返回 JWT 令牌对及用户权限信息。
 	Login(context.Context, *LoginReq) (*LoginResp, error)
+	// RefreshToken 刷新令牌。使用有效的 refreshToken 换取新的令牌对,旧令牌即时失效(单会话轮转)。
 	RefreshToken(context.Context, *RefreshTokenReq) (*RefreshTokenResp, error)
+	// VerifyToken 验证令牌。校验 accessToken 的有效性(签名、过期、用户状态、产品状态、成员资格、tokenVersion),返回用户权限信息。
 	VerifyToken(context.Context, *VerifyTokenReq) (*VerifyTokenResp, error)
+	// GetUserPerms 查询用户权限。产品服务端通过 appKey/appSecret 认证后查询指定用户在该产品下的成员类型和权限列表。
 	GetUserPerms(context.Context, *GetUserPermsReq) (*GetUserPermsResp, error)
+	// Logout 用户登出。使当前 accessToken 对应用户的所有令牌立即失效(递增 tokenVersion)。
+	Logout(context.Context, *LogoutReq) (*LogoutResp, error)
 	mustEmbedUnimplementedPermServiceServer()
 }
 
@@ -129,6 +158,9 @@ func (UnimplementedPermServiceServer) VerifyToken(context.Context, *VerifyTokenR
 func (UnimplementedPermServiceServer) GetUserPerms(context.Context, *GetUserPermsReq) (*GetUserPermsResp, error) {
 	return nil, status.Error(codes.Unimplemented, "method GetUserPerms not implemented")
 }
+func (UnimplementedPermServiceServer) Logout(context.Context, *LogoutReq) (*LogoutResp, error) {
+	return nil, status.Error(codes.Unimplemented, "method Logout not implemented")
+}
 func (UnimplementedPermServiceServer) mustEmbedUnimplementedPermServiceServer() {}
 func (UnimplementedPermServiceServer) testEmbeddedByValue()                     {}
 
@@ -240,6 +272,24 @@ func _PermService_GetUserPerms_Handler(srv interface{}, ctx context.Context, dec
 	return interceptor(ctx, in, info, handler)
 }
 
+func _PermService_Logout_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(LogoutReq)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(PermServiceServer).Logout(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: PermService_Logout_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(PermServiceServer).Logout(ctx, req.(*LogoutReq))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
 // PermService_ServiceDesc is the grpc.ServiceDesc for PermService service.
 // It's only intended for direct use with grpc.RegisterService,
 // and not to be introspected or modified (even as a copy)
@@ -267,6 +317,10 @@ var PermService_ServiceDesc = grpc.ServiceDesc{
 			MethodName: "GetUserPerms",
 			Handler:    _PermService_GetUserPerms_Handler,
 		},
+		{
+			MethodName: "Logout",
+			Handler:    _PermService_Logout_Handler,
+		},
 	},
 	Streams:  []grpc.StreamDesc{},
 	Metadata: "pb/perm.proto",

+ 6 - 0
permclient/permclient.go

@@ -64,3 +64,9 @@ func (c *PermClient) GetUserPerms(ctx context.Context, appKey, appSecret string,
 		ProductCode: productCode,
 	})
 }
+
+func (c *PermClient) Logout(ctx context.Context, accessToken string) (*pb.LogoutResp, error) {
+	return c.cli.Logout(ctx, &pb.LogoutReq{
+		AccessToken: accessToken,
+	})
+}