diff --git a/api/proto/user/v1/auth.pb.go b/api/proto/user/v1/auth.pb.go index 4b1d133d..43cd3b9a 100644 --- a/api/proto/user/v1/auth.pb.go +++ b/api/proto/user/v1/auth.pb.go @@ -1042,6 +1042,106 @@ func (x *RecordLoginBlockedResponse) GetRecorded() bool { return false } +// CheckLoginRiskIPWhitelistRequest 查询入口 IP 是否命中后台白名单。 +type CheckLoginRiskIPWhitelistRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + ClientIp string `protobuf:"bytes,2,opt,name=client_ip,json=clientIp,proto3" json:"client_ip,omitempty"` +} + +func (x *CheckLoginRiskIPWhitelistRequest) Reset() { + *x = CheckLoginRiskIPWhitelistRequest{} + mi := &file_proto_user_v1_auth_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckLoginRiskIPWhitelistRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckLoginRiskIPWhitelistRequest) ProtoMessage() {} + +func (x *CheckLoginRiskIPWhitelistRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_user_v1_auth_proto_msgTypes[13] + 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 CheckLoginRiskIPWhitelistRequest.ProtoReflect.Descriptor instead. +func (*CheckLoginRiskIPWhitelistRequest) Descriptor() ([]byte, []int) { + return file_proto_user_v1_auth_proto_rawDescGZIP(), []int{13} +} + +func (x *CheckLoginRiskIPWhitelistRequest) GetMeta() *RequestMeta { + if x != nil { + return x.Meta + } + return nil +} + +func (x *CheckLoginRiskIPWhitelistRequest) GetClientIp() string { + if x != nil { + return x.ClientIp + } + return "" +} + +// CheckLoginRiskIPWhitelistResponse 返回入口 IP 是否跳过登录地区屏蔽。 +type CheckLoginRiskIPWhitelistResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Whitelisted bool `protobuf:"varint,1,opt,name=whitelisted,proto3" json:"whitelisted,omitempty"` +} + +func (x *CheckLoginRiskIPWhitelistResponse) Reset() { + *x = CheckLoginRiskIPWhitelistResponse{} + mi := &file_proto_user_v1_auth_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckLoginRiskIPWhitelistResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckLoginRiskIPWhitelistResponse) ProtoMessage() {} + +func (x *CheckLoginRiskIPWhitelistResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_user_v1_auth_proto_msgTypes[14] + 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 CheckLoginRiskIPWhitelistResponse.ProtoReflect.Descriptor instead. +func (*CheckLoginRiskIPWhitelistResponse) Descriptor() ([]byte, []int) { + return file_proto_user_v1_auth_proto_rawDescGZIP(), []int{14} +} + +func (x *CheckLoginRiskIPWhitelistResponse) GetWhitelisted() bool { + if x != nil { + return x.Whitelisted + } + return false +} + // AppHeartbeatRequest 刷新当前登录会话的 App 在线心跳。 type AppHeartbeatRequest struct { state protoimpl.MessageState @@ -1055,7 +1155,7 @@ type AppHeartbeatRequest struct { func (x *AppHeartbeatRequest) Reset() { *x = AppHeartbeatRequest{} - mi := &file_proto_user_v1_auth_proto_msgTypes[13] + mi := &file_proto_user_v1_auth_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1067,7 +1167,7 @@ func (x *AppHeartbeatRequest) String() string { func (*AppHeartbeatRequest) ProtoMessage() {} func (x *AppHeartbeatRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_auth_proto_msgTypes[13] + mi := &file_proto_user_v1_auth_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1080,7 +1180,7 @@ func (x *AppHeartbeatRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AppHeartbeatRequest.ProtoReflect.Descriptor instead. func (*AppHeartbeatRequest) Descriptor() ([]byte, []int) { - return file_proto_user_v1_auth_proto_rawDescGZIP(), []int{13} + return file_proto_user_v1_auth_proto_rawDescGZIP(), []int{15} } func (x *AppHeartbeatRequest) GetMeta() *RequestMeta { @@ -1118,7 +1218,7 @@ type AppHeartbeatResponse struct { func (x *AppHeartbeatResponse) Reset() { *x = AppHeartbeatResponse{} - mi := &file_proto_user_v1_auth_proto_msgTypes[14] + mi := &file_proto_user_v1_auth_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1130,7 +1230,7 @@ func (x *AppHeartbeatResponse) String() string { func (*AppHeartbeatResponse) ProtoMessage() {} func (x *AppHeartbeatResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_auth_proto_msgTypes[14] + mi := &file_proto_user_v1_auth_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1143,7 +1243,7 @@ func (x *AppHeartbeatResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AppHeartbeatResponse.ProtoReflect.Descriptor instead. func (*AppHeartbeatResponse) Descriptor() ([]byte, []int) { - return file_proto_user_v1_auth_proto_rawDescGZIP(), []int{14} + return file_proto_user_v1_auth_proto_rawDescGZIP(), []int{16} } func (x *AppHeartbeatResponse) GetAccepted() bool { @@ -1335,75 +1435,95 @@ var file_proto_user_v1_auth_proto_rawDesc = []byte{ 0x73, 0x6f, 0x6e, 0x22, 0x38, 0x0a, 0x1a, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x65, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x65, 0x64, 0x22, 0x7d, 0x0a, - 0x13, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, - 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, - 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, - 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0xb0, 0x01, 0x0a, - 0x14, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, - 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x5f, 0x61, - 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x68, 0x65, 0x61, 0x72, - 0x74, 0x62, 0x65, 0x61, 0x74, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x69, 0x6d, 0x65, 0x4d, 0x73, 0x12, - 0x2e, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, - 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, - 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x32, - 0xdc, 0x05, 0x0a, 0x0b, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, - 0x51, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, - 0x12, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, - 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, - 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x68, 0x69, 0x72, 0x64, - 0x50, 0x61, 0x72, 0x74, 0x79, 0x12, 0x25, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, - 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x68, 0x69, 0x72, 0x64, - 0x50, 0x61, 0x72, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x68, - 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, - 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0b, 0x53, 0x65, 0x74, - 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x21, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, - 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, - 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x79, - 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x50, - 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x69, 0x0a, 0x12, 0x51, 0x75, 0x69, 0x63, 0x6b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, - 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x69, 0x63, 0x6b, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, - 0x51, 0x75, 0x69, 0x63, 0x6b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c, 0x52, 0x65, - 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x22, 0x2e, 0x68, 0x79, 0x61, - 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, - 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, - 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, - 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x1c, 0x2e, + 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x65, 0x64, 0x22, 0x6f, 0x0a, + 0x20, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x69, 0x73, 0x6b, 0x49, + 0x50, 0x57, 0x68, 0x69, 0x74, 0x65, 0x6c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, + 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x70, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x22, 0x45, + 0x0a, 0x21, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x69, 0x73, 0x6b, + 0x49, 0x50, 0x57, 0x68, 0x69, 0x74, 0x65, 0x6c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x77, 0x68, 0x69, 0x74, 0x65, 0x6c, 0x69, 0x73, 0x74, + 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x77, 0x68, 0x69, 0x74, 0x65, 0x6c, + 0x69, 0x73, 0x74, 0x65, 0x64, 0x22, 0x7d, 0x0a, 0x13, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x72, + 0x74, 0x62, 0x65, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, + 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, + 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, + 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, + 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x49, 0x64, 0x22, 0xb0, 0x01, 0x0a, 0x14, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x72, + 0x74, 0x62, 0x65, 0x61, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, + 0x08, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x08, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x68, 0x65, 0x61, + 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0d, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x41, 0x74, 0x4d, + 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x74, 0x69, 0x6d, 0x65, + 0x5f, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x54, 0x69, 0x6d, 0x65, 0x4d, 0x73, 0x12, 0x2e, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, + 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x32, 0xdc, 0x06, 0x0a, 0x0b, 0x41, 0x75, 0x74, 0x68, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x51, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, + 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, + 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61, + 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, + 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, + 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0f, 0x4c, 0x6f, + 0x67, 0x69, 0x6e, 0x54, 0x68, 0x69, 0x72, 0x64, 0x50, 0x61, 0x72, 0x74, 0x79, 0x12, 0x25, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, - 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x68, 0x79, - 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x6f, - 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x69, 0x0a, 0x12, 0x52, 0x65, - 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, - 0x12, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, - 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63, - 0x6b, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x68, 0x79, 0x61, - 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, - 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x72, - 0x74, 0x62, 0x65, 0x61, 0x74, 0x12, 0x22, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, - 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, - 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, - 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, - 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, - 0x5a, 0x24, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, - 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, - 0x75, 0x73, 0x65, 0x72, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x67, 0x69, 0x6e, 0x54, 0x68, 0x69, 0x72, 0x64, 0x50, 0x61, 0x72, 0x74, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, + 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x54, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, + 0x12, 0x21, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, + 0x2e, 0x53, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x69, 0x0a, 0x12, 0x51, 0x75, 0x69, 0x63, 0x6b, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x28, 0x2e, + 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, + 0x69, 0x63, 0x6b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, + 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x69, 0x63, 0x6b, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x12, 0x22, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, + 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x06, 0x4c, + 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x1c, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, + 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x69, 0x0a, 0x12, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, + 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, + 0x6f, 0x67, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x42, 0x6c, + 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7e, 0x0a, + 0x19, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x69, 0x73, 0x6b, 0x49, + 0x50, 0x57, 0x68, 0x69, 0x74, 0x65, 0x6c, 0x69, 0x73, 0x74, 0x12, 0x2f, 0x2e, 0x68, 0x79, 0x61, + 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x69, 0x73, 0x6b, 0x49, 0x50, 0x57, 0x68, 0x69, 0x74, 0x65, + 0x6c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x69, 0x73, 0x6b, 0x49, 0x50, 0x57, 0x68, 0x69, 0x74, + 0x65, 0x6c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, + 0x0c, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x12, 0x22, 0x2e, + 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x70, + 0x70, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, + 0x31, 0x2e, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, + 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, + 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x73, 0x65, 0x72, 0x76, 0x31, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1418,60 +1538,65 @@ func file_proto_user_v1_auth_proto_rawDescGZIP() []byte { return file_proto_user_v1_auth_proto_rawDescData } -var file_proto_user_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 15) +var file_proto_user_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 17) var file_proto_user_v1_auth_proto_goTypes = []any{ - (*LoginPasswordRequest)(nil), // 0: hyapp.user.v1.LoginPasswordRequest - (*LoginThirdPartyRequest)(nil), // 1: hyapp.user.v1.LoginThirdPartyRequest - (*AuthResponse)(nil), // 2: hyapp.user.v1.AuthResponse - (*SetPasswordRequest)(nil), // 3: hyapp.user.v1.SetPasswordRequest - (*SetPasswordResponse)(nil), // 4: hyapp.user.v1.SetPasswordResponse - (*QuickCreateAccountRequest)(nil), // 5: hyapp.user.v1.QuickCreateAccountRequest - (*QuickCreateAccountResponse)(nil), // 6: hyapp.user.v1.QuickCreateAccountResponse - (*RefreshTokenRequest)(nil), // 7: hyapp.user.v1.RefreshTokenRequest - (*RefreshTokenResponse)(nil), // 8: hyapp.user.v1.RefreshTokenResponse - (*LogoutRequest)(nil), // 9: hyapp.user.v1.LogoutRequest - (*LogoutResponse)(nil), // 10: hyapp.user.v1.LogoutResponse - (*RecordLoginBlockedRequest)(nil), // 11: hyapp.user.v1.RecordLoginBlockedRequest - (*RecordLoginBlockedResponse)(nil), // 12: hyapp.user.v1.RecordLoginBlockedResponse - (*AppHeartbeatRequest)(nil), // 13: hyapp.user.v1.AppHeartbeatRequest - (*AppHeartbeatResponse)(nil), // 14: hyapp.user.v1.AppHeartbeatResponse - (*RequestMeta)(nil), // 15: hyapp.user.v1.RequestMeta - (*AuthToken)(nil), // 16: hyapp.user.v1.AuthToken + (*LoginPasswordRequest)(nil), // 0: hyapp.user.v1.LoginPasswordRequest + (*LoginThirdPartyRequest)(nil), // 1: hyapp.user.v1.LoginThirdPartyRequest + (*AuthResponse)(nil), // 2: hyapp.user.v1.AuthResponse + (*SetPasswordRequest)(nil), // 3: hyapp.user.v1.SetPasswordRequest + (*SetPasswordResponse)(nil), // 4: hyapp.user.v1.SetPasswordResponse + (*QuickCreateAccountRequest)(nil), // 5: hyapp.user.v1.QuickCreateAccountRequest + (*QuickCreateAccountResponse)(nil), // 6: hyapp.user.v1.QuickCreateAccountResponse + (*RefreshTokenRequest)(nil), // 7: hyapp.user.v1.RefreshTokenRequest + (*RefreshTokenResponse)(nil), // 8: hyapp.user.v1.RefreshTokenResponse + (*LogoutRequest)(nil), // 9: hyapp.user.v1.LogoutRequest + (*LogoutResponse)(nil), // 10: hyapp.user.v1.LogoutResponse + (*RecordLoginBlockedRequest)(nil), // 11: hyapp.user.v1.RecordLoginBlockedRequest + (*RecordLoginBlockedResponse)(nil), // 12: hyapp.user.v1.RecordLoginBlockedResponse + (*CheckLoginRiskIPWhitelistRequest)(nil), // 13: hyapp.user.v1.CheckLoginRiskIPWhitelistRequest + (*CheckLoginRiskIPWhitelistResponse)(nil), // 14: hyapp.user.v1.CheckLoginRiskIPWhitelistResponse + (*AppHeartbeatRequest)(nil), // 15: hyapp.user.v1.AppHeartbeatRequest + (*AppHeartbeatResponse)(nil), // 16: hyapp.user.v1.AppHeartbeatResponse + (*RequestMeta)(nil), // 17: hyapp.user.v1.RequestMeta + (*AuthToken)(nil), // 18: hyapp.user.v1.AuthToken } var file_proto_user_v1_auth_proto_depIdxs = []int32{ - 15, // 0: hyapp.user.v1.LoginPasswordRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 15, // 1: hyapp.user.v1.LoginThirdPartyRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 16, // 2: hyapp.user.v1.AuthResponse.token:type_name -> hyapp.user.v1.AuthToken - 15, // 3: hyapp.user.v1.SetPasswordRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 15, // 4: hyapp.user.v1.QuickCreateAccountRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 16, // 5: hyapp.user.v1.QuickCreateAccountResponse.token:type_name -> hyapp.user.v1.AuthToken - 15, // 6: hyapp.user.v1.RefreshTokenRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 16, // 7: hyapp.user.v1.RefreshTokenResponse.token:type_name -> hyapp.user.v1.AuthToken - 15, // 8: hyapp.user.v1.LogoutRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 15, // 9: hyapp.user.v1.RecordLoginBlockedRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 15, // 10: hyapp.user.v1.AppHeartbeatRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 16, // 11: hyapp.user.v1.AppHeartbeatResponse.token:type_name -> hyapp.user.v1.AuthToken - 0, // 12: hyapp.user.v1.AuthService.LoginPassword:input_type -> hyapp.user.v1.LoginPasswordRequest - 1, // 13: hyapp.user.v1.AuthService.LoginThirdParty:input_type -> hyapp.user.v1.LoginThirdPartyRequest - 3, // 14: hyapp.user.v1.AuthService.SetPassword:input_type -> hyapp.user.v1.SetPasswordRequest - 5, // 15: hyapp.user.v1.AuthService.QuickCreateAccount:input_type -> hyapp.user.v1.QuickCreateAccountRequest - 7, // 16: hyapp.user.v1.AuthService.RefreshToken:input_type -> hyapp.user.v1.RefreshTokenRequest - 9, // 17: hyapp.user.v1.AuthService.Logout:input_type -> hyapp.user.v1.LogoutRequest - 11, // 18: hyapp.user.v1.AuthService.RecordLoginBlocked:input_type -> hyapp.user.v1.RecordLoginBlockedRequest - 13, // 19: hyapp.user.v1.AuthService.AppHeartbeat:input_type -> hyapp.user.v1.AppHeartbeatRequest - 2, // 20: hyapp.user.v1.AuthService.LoginPassword:output_type -> hyapp.user.v1.AuthResponse - 2, // 21: hyapp.user.v1.AuthService.LoginThirdParty:output_type -> hyapp.user.v1.AuthResponse - 4, // 22: hyapp.user.v1.AuthService.SetPassword:output_type -> hyapp.user.v1.SetPasswordResponse - 6, // 23: hyapp.user.v1.AuthService.QuickCreateAccount:output_type -> hyapp.user.v1.QuickCreateAccountResponse - 8, // 24: hyapp.user.v1.AuthService.RefreshToken:output_type -> hyapp.user.v1.RefreshTokenResponse - 10, // 25: hyapp.user.v1.AuthService.Logout:output_type -> hyapp.user.v1.LogoutResponse - 12, // 26: hyapp.user.v1.AuthService.RecordLoginBlocked:output_type -> hyapp.user.v1.RecordLoginBlockedResponse - 14, // 27: hyapp.user.v1.AuthService.AppHeartbeat:output_type -> hyapp.user.v1.AppHeartbeatResponse - 20, // [20:28] is the sub-list for method output_type - 12, // [12:20] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 17, // 0: hyapp.user.v1.LoginPasswordRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 17, // 1: hyapp.user.v1.LoginThirdPartyRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 18, // 2: hyapp.user.v1.AuthResponse.token:type_name -> hyapp.user.v1.AuthToken + 17, // 3: hyapp.user.v1.SetPasswordRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 17, // 4: hyapp.user.v1.QuickCreateAccountRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 18, // 5: hyapp.user.v1.QuickCreateAccountResponse.token:type_name -> hyapp.user.v1.AuthToken + 17, // 6: hyapp.user.v1.RefreshTokenRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 18, // 7: hyapp.user.v1.RefreshTokenResponse.token:type_name -> hyapp.user.v1.AuthToken + 17, // 8: hyapp.user.v1.LogoutRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 17, // 9: hyapp.user.v1.RecordLoginBlockedRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 17, // 10: hyapp.user.v1.CheckLoginRiskIPWhitelistRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 17, // 11: hyapp.user.v1.AppHeartbeatRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 18, // 12: hyapp.user.v1.AppHeartbeatResponse.token:type_name -> hyapp.user.v1.AuthToken + 0, // 13: hyapp.user.v1.AuthService.LoginPassword:input_type -> hyapp.user.v1.LoginPasswordRequest + 1, // 14: hyapp.user.v1.AuthService.LoginThirdParty:input_type -> hyapp.user.v1.LoginThirdPartyRequest + 3, // 15: hyapp.user.v1.AuthService.SetPassword:input_type -> hyapp.user.v1.SetPasswordRequest + 5, // 16: hyapp.user.v1.AuthService.QuickCreateAccount:input_type -> hyapp.user.v1.QuickCreateAccountRequest + 7, // 17: hyapp.user.v1.AuthService.RefreshToken:input_type -> hyapp.user.v1.RefreshTokenRequest + 9, // 18: hyapp.user.v1.AuthService.Logout:input_type -> hyapp.user.v1.LogoutRequest + 11, // 19: hyapp.user.v1.AuthService.RecordLoginBlocked:input_type -> hyapp.user.v1.RecordLoginBlockedRequest + 13, // 20: hyapp.user.v1.AuthService.CheckLoginRiskIPWhitelist:input_type -> hyapp.user.v1.CheckLoginRiskIPWhitelistRequest + 15, // 21: hyapp.user.v1.AuthService.AppHeartbeat:input_type -> hyapp.user.v1.AppHeartbeatRequest + 2, // 22: hyapp.user.v1.AuthService.LoginPassword:output_type -> hyapp.user.v1.AuthResponse + 2, // 23: hyapp.user.v1.AuthService.LoginThirdParty:output_type -> hyapp.user.v1.AuthResponse + 4, // 24: hyapp.user.v1.AuthService.SetPassword:output_type -> hyapp.user.v1.SetPasswordResponse + 6, // 25: hyapp.user.v1.AuthService.QuickCreateAccount:output_type -> hyapp.user.v1.QuickCreateAccountResponse + 8, // 26: hyapp.user.v1.AuthService.RefreshToken:output_type -> hyapp.user.v1.RefreshTokenResponse + 10, // 27: hyapp.user.v1.AuthService.Logout:output_type -> hyapp.user.v1.LogoutResponse + 12, // 28: hyapp.user.v1.AuthService.RecordLoginBlocked:output_type -> hyapp.user.v1.RecordLoginBlockedResponse + 14, // 29: hyapp.user.v1.AuthService.CheckLoginRiskIPWhitelist:output_type -> hyapp.user.v1.CheckLoginRiskIPWhitelistResponse + 16, // 30: hyapp.user.v1.AuthService.AppHeartbeat:output_type -> hyapp.user.v1.AppHeartbeatResponse + 22, // [22:31] is the sub-list for method output_type + 13, // [13:22] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_proto_user_v1_auth_proto_init() } @@ -1486,7 +1611,7 @@ func file_proto_user_v1_auth_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_proto_user_v1_auth_proto_rawDesc, NumEnums: 0, - NumMessages: 15, + NumMessages: 17, NumExtensions: 0, NumServices: 1, }, diff --git a/api/proto/user/v1/auth.proto b/api/proto/user/v1/auth.proto index f189400c..9a708177 100644 --- a/api/proto/user/v1/auth.proto +++ b/api/proto/user/v1/auth.proto @@ -124,6 +124,17 @@ message RecordLoginBlockedResponse { bool recorded = 1; } +// CheckLoginRiskIPWhitelistRequest 查询入口 IP 是否命中后台白名单。 +message CheckLoginRiskIPWhitelistRequest { + RequestMeta meta = 1; + string client_ip = 2; +} + +// CheckLoginRiskIPWhitelistResponse 返回入口 IP 是否跳过登录地区屏蔽。 +message CheckLoginRiskIPWhitelistResponse { + bool whitelisted = 1; +} + // AppHeartbeatRequest 刷新当前登录会话的 App 在线心跳。 message AppHeartbeatRequest { RequestMeta meta = 1; @@ -148,5 +159,6 @@ service AuthService { rpc RefreshToken(RefreshTokenRequest) returns (RefreshTokenResponse); rpc Logout(LogoutRequest) returns (LogoutResponse); rpc RecordLoginBlocked(RecordLoginBlockedRequest) returns (RecordLoginBlockedResponse); + rpc CheckLoginRiskIPWhitelist(CheckLoginRiskIPWhitelistRequest) returns (CheckLoginRiskIPWhitelistResponse); rpc AppHeartbeat(AppHeartbeatRequest) returns (AppHeartbeatResponse); } diff --git a/api/proto/user/v1/auth_grpc.pb.go b/api/proto/user/v1/auth_grpc.pb.go index 88916c26..e8c60546 100644 --- a/api/proto/user/v1/auth_grpc.pb.go +++ b/api/proto/user/v1/auth_grpc.pb.go @@ -19,14 +19,15 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - AuthService_LoginPassword_FullMethodName = "/hyapp.user.v1.AuthService/LoginPassword" - AuthService_LoginThirdParty_FullMethodName = "/hyapp.user.v1.AuthService/LoginThirdParty" - AuthService_SetPassword_FullMethodName = "/hyapp.user.v1.AuthService/SetPassword" - AuthService_QuickCreateAccount_FullMethodName = "/hyapp.user.v1.AuthService/QuickCreateAccount" - AuthService_RefreshToken_FullMethodName = "/hyapp.user.v1.AuthService/RefreshToken" - AuthService_Logout_FullMethodName = "/hyapp.user.v1.AuthService/Logout" - AuthService_RecordLoginBlocked_FullMethodName = "/hyapp.user.v1.AuthService/RecordLoginBlocked" - AuthService_AppHeartbeat_FullMethodName = "/hyapp.user.v1.AuthService/AppHeartbeat" + AuthService_LoginPassword_FullMethodName = "/hyapp.user.v1.AuthService/LoginPassword" + AuthService_LoginThirdParty_FullMethodName = "/hyapp.user.v1.AuthService/LoginThirdParty" + AuthService_SetPassword_FullMethodName = "/hyapp.user.v1.AuthService/SetPassword" + AuthService_QuickCreateAccount_FullMethodName = "/hyapp.user.v1.AuthService/QuickCreateAccount" + AuthService_RefreshToken_FullMethodName = "/hyapp.user.v1.AuthService/RefreshToken" + AuthService_Logout_FullMethodName = "/hyapp.user.v1.AuthService/Logout" + AuthService_RecordLoginBlocked_FullMethodName = "/hyapp.user.v1.AuthService/RecordLoginBlocked" + AuthService_CheckLoginRiskIPWhitelist_FullMethodName = "/hyapp.user.v1.AuthService/CheckLoginRiskIPWhitelist" + AuthService_AppHeartbeat_FullMethodName = "/hyapp.user.v1.AuthService/AppHeartbeat" ) // AuthServiceClient is the client API for AuthService service. @@ -42,6 +43,7 @@ type AuthServiceClient interface { RefreshToken(ctx context.Context, in *RefreshTokenRequest, opts ...grpc.CallOption) (*RefreshTokenResponse, error) Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*LogoutResponse, error) RecordLoginBlocked(ctx context.Context, in *RecordLoginBlockedRequest, opts ...grpc.CallOption) (*RecordLoginBlockedResponse, error) + CheckLoginRiskIPWhitelist(ctx context.Context, in *CheckLoginRiskIPWhitelistRequest, opts ...grpc.CallOption) (*CheckLoginRiskIPWhitelistResponse, error) AppHeartbeat(ctx context.Context, in *AppHeartbeatRequest, opts ...grpc.CallOption) (*AppHeartbeatResponse, error) } @@ -123,6 +125,16 @@ func (c *authServiceClient) RecordLoginBlocked(ctx context.Context, in *RecordLo return out, nil } +func (c *authServiceClient) CheckLoginRiskIPWhitelist(ctx context.Context, in *CheckLoginRiskIPWhitelistRequest, opts ...grpc.CallOption) (*CheckLoginRiskIPWhitelistResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CheckLoginRiskIPWhitelistResponse) + err := c.cc.Invoke(ctx, AuthService_CheckLoginRiskIPWhitelist_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *authServiceClient) AppHeartbeat(ctx context.Context, in *AppHeartbeatRequest, opts ...grpc.CallOption) (*AppHeartbeatResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(AppHeartbeatResponse) @@ -146,6 +158,7 @@ type AuthServiceServer interface { RefreshToken(context.Context, *RefreshTokenRequest) (*RefreshTokenResponse, error) Logout(context.Context, *LogoutRequest) (*LogoutResponse, error) RecordLoginBlocked(context.Context, *RecordLoginBlockedRequest) (*RecordLoginBlockedResponse, error) + CheckLoginRiskIPWhitelist(context.Context, *CheckLoginRiskIPWhitelistRequest) (*CheckLoginRiskIPWhitelistResponse, error) AppHeartbeat(context.Context, *AppHeartbeatRequest) (*AppHeartbeatResponse, error) mustEmbedUnimplementedAuthServiceServer() } @@ -178,6 +191,9 @@ func (UnimplementedAuthServiceServer) Logout(context.Context, *LogoutRequest) (* func (UnimplementedAuthServiceServer) RecordLoginBlocked(context.Context, *RecordLoginBlockedRequest) (*RecordLoginBlockedResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method RecordLoginBlocked not implemented") } +func (UnimplementedAuthServiceServer) CheckLoginRiskIPWhitelist(context.Context, *CheckLoginRiskIPWhitelistRequest) (*CheckLoginRiskIPWhitelistResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CheckLoginRiskIPWhitelist not implemented") +} func (UnimplementedAuthServiceServer) AppHeartbeat(context.Context, *AppHeartbeatRequest) (*AppHeartbeatResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method AppHeartbeat not implemented") } @@ -328,6 +344,24 @@ func _AuthService_RecordLoginBlocked_Handler(srv interface{}, ctx context.Contex return interceptor(ctx, in, info, handler) } +func _AuthService_CheckLoginRiskIPWhitelist_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CheckLoginRiskIPWhitelistRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).CheckLoginRiskIPWhitelist(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_CheckLoginRiskIPWhitelist_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).CheckLoginRiskIPWhitelist(ctx, req.(*CheckLoginRiskIPWhitelistRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _AuthService_AppHeartbeat_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(AppHeartbeatRequest) if err := dec(in); err != nil { @@ -381,6 +415,10 @@ var AuthService_ServiceDesc = grpc.ServiceDesc{ MethodName: "RecordLoginBlocked", Handler: _AuthService_RecordLoginBlocked_Handler, }, + { + MethodName: "CheckLoginRiskIPWhitelist", + Handler: _AuthService_CheckLoginRiskIPWhitelist_Handler, + }, { MethodName: "AppHeartbeat", Handler: _AuthService_AppHeartbeat_Handler, diff --git a/scripts/mysql/050_login_risk_ip_whitelist.sql b/scripts/mysql/050_login_risk_ip_whitelist.sql new file mode 100644 index 00000000..ed83ab19 --- /dev/null +++ b/scripts/mysql/050_login_risk_ip_whitelist.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS login_risk_ip_whitelist ( + app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离', + whitelist_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '白名单 ID', + ip_address VARCHAR(64) NOT NULL COMMENT '允许跳过登录地区屏蔽的客户端 IP', + enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '启用', + created_by_user_id BIGINT NULL COMMENT '创建人用户 ID', + updated_by_user_id BIGINT NULL COMMENT '更新人用户 ID', + created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms', + updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms', + PRIMARY KEY (whitelist_id), + UNIQUE KEY uk_login_risk_ip_whitelist_ip (app_code, ip_address), + KEY idx_login_risk_ip_whitelist_enabled (app_code, enabled, ip_address), + KEY idx_login_risk_ip_whitelist_updated (app_code, updated_at_ms) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='登录风险 IP 白名单表'; diff --git a/server/admin/internal/modules/regionblock/handler.go b/server/admin/internal/modules/regionblock/handler.go index 860a92a6..137e01e1 100644 --- a/server/admin/internal/modules/regionblock/handler.go +++ b/server/admin/internal/modules/regionblock/handler.go @@ -18,7 +18,8 @@ type Handler struct { } type replaceRequest struct { - Keywords *[]string `json:"keywords"` + Keywords *[]string `json:"keywords"` + WhitelistIPs *[]string `json:"whitelist_ips"` } func New(userDB *sql.DB, audit shared.OperationLogger) *Handler { @@ -32,7 +33,7 @@ func (h *Handler) ListRegionBlocks(c *gin.Context) { response.ServerError(c, "获取地区屏蔽配置失败") return } - response.OK(c, gin.H{"items": items, "total": len(items)}) + response.OK(c, items) } // ReplaceRegionBlocks 保存整页地区屏蔽词配置;空数组表示清空屏蔽词。 @@ -42,7 +43,7 @@ func (h *Handler) ReplaceRegionBlocks(c *gin.Context) { response.BadRequest(c, "地区屏蔽词参数不正确") return } - items, err := h.service.Replace(c.Request.Context(), int64(shared.ActorFromContext(c).UserID), *req.Keywords) + items, err := h.service.Replace(c.Request.Context(), int64(shared.ActorFromContext(c).UserID), *req.Keywords, req.WhitelistIPs) if err != nil { if errors.Is(err, errInvalidPayload) { response.BadRequest(c, "地区屏蔽词参数不正确") @@ -51,6 +52,6 @@ func (h *Handler) ReplaceRegionBlocks(c *gin.Context) { response.ServerError(c, "保存地区屏蔽配置失败") return } - shared.OperationLogWithResourceID(c, h.audit, "replace-region-blocks", "login_risk_country_blocks", middleware.CurrentRequestID(c), "success", fmt.Sprintf("keywords=%d", len(items))) - response.OK(c, gin.H{"items": items, "total": len(items)}) + shared.OperationLogWithResourceID(c, h.audit, "replace-region-blocks", "login_risk_country_blocks", middleware.CurrentRequestID(c), "success", fmt.Sprintf("keywords=%d whitelist_ips=%d", len(items.Items), len(items.WhitelistItems))) + response.OK(c, items) } diff --git a/server/admin/internal/modules/regionblock/service.go b/server/admin/internal/modules/regionblock/service.go index ac6b1057..fb68b239 100644 --- a/server/admin/internal/modules/regionblock/service.go +++ b/server/admin/internal/modules/regionblock/service.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "net/netip" "regexp" "strings" "time" @@ -15,6 +16,7 @@ import ( const ( maxRegionBlockKeywords = 200 maxRegionBlockKeywordLength = 128 + maxIPWhitelistItems = 500 ) var ( @@ -35,6 +37,21 @@ type RegionBlock struct { UpdatedAtMS int64 `json:"updated_at_ms"` } +type IPWhitelistItem struct { + WhitelistID int64 `json:"whitelist_id"` + IPAddress string `json:"ip_address"` + Enabled bool `json:"enabled"` + CreatedAtMS int64 `json:"created_at_ms"` + UpdatedAtMS int64 `json:"updated_at_ms"` +} + +type Config struct { + Items []RegionBlock `json:"items"` + Total int `json:"total"` + WhitelistItems []IPWhitelistItem `json:"whitelist_items"` + WhitelistTotal int `json:"whitelist_total"` +} + type normalizedKeyword struct { Keyword string CountryCode string @@ -48,28 +65,44 @@ func NewService(userDB *sql.DB) *Service { return &Service{userDB: userDB} } -// List 返回当前 App 已启用的登录地区屏蔽词。 +// List 返回当前 App 已启用的登录地区屏蔽词和 IP 白名单。 // 表属于 user-service 登录风控事实,admin-server 只作为后台维护入口直接写 user DB。 -func (s *Service) List(ctx context.Context) ([]RegionBlock, error) { +func (s *Service) List(ctx context.Context) (Config, error) { if s.userDB == nil { - return nil, errors.New("user db is not configured") + return Config{}, errors.New("user db is not configured") } - return queryEnabledBlocks(ctx, s.userDB, appctx.FromContext(ctx)) + appCode := appctx.FromContext(ctx) + blocks, err := queryEnabledBlocks(ctx, s.userDB, appCode) + if err != nil { + return Config{}, err + } + whitelist, err := queryEnabledIPWhitelist(ctx, s.userDB, appCode) + if err != nil { + return Config{}, err + } + return Config{Items: blocks, Total: len(blocks), WhitelistItems: whitelist, WhitelistTotal: len(whitelist)}, nil } // Replace 用页面提交的词表整体替换当前 App 的生效配置。 // 整体替换比逐条增删更适合配置页保存语义,也能避免客户端和服务端出现半旧半新的屏蔽词集合。 -func (s *Service) Replace(ctx context.Context, actorID int64, keywords []string) ([]RegionBlock, error) { +func (s *Service) Replace(ctx context.Context, actorID int64, keywords []string, whitelistIPs *[]string) (Config, error) { if s.userDB == nil { - return nil, errors.New("user db is not configured") + return Config{}, errors.New("user db is not configured") } normalized, err := normalizeKeywords(ctx, s.userDB, appctx.FromContext(ctx), keywords) if err != nil { - return nil, err + return Config{}, err + } + var normalizedIPs []string + if whitelistIPs != nil { + normalizedIPs, err = normalizeIPWhitelist(*whitelistIPs) + if err != nil { + return Config{}, err + } } tx, err := s.userDB.BeginTx(ctx, nil) if err != nil { - return nil, err + return Config{}, err } defer tx.Rollback() @@ -80,7 +113,7 @@ func (s *Service) Replace(ctx context.Context, actorID int64, keywords []string) SET enabled = 0, updated_by_user_id = ?, updated_at_ms = ? WHERE app_code = ? AND enabled = 1 `, nullableActor(actorID), nowMS, appCode); err != nil { - return nil, err + return Config{}, err } for _, keyword := range normalized { if _, err := tx.ExecContext(ctx, ` @@ -95,11 +128,35 @@ func (s *Service) Replace(ctx context.Context, actorID int64, keywords []string) updated_by_user_id = VALUES(updated_by_user_id), updated_at_ms = VALUES(updated_at_ms) `, appCode, keyword.Keyword, keyword.CountryCode, nullableActor(actorID), nullableActor(actorID), nowMS, nowMS); err != nil { - return nil, err + return Config{}, err + } + } + if whitelistIPs != nil { + if _, err := tx.ExecContext(ctx, ` + UPDATE login_risk_ip_whitelist + SET enabled = 0, updated_by_user_id = ?, updated_at_ms = ? + WHERE app_code = ? AND enabled = 1 + `, nullableActor(actorID), nowMS, appCode); err != nil { + return Config{}, err + } + for _, ip := range normalizedIPs { + if _, err := tx.ExecContext(ctx, ` + INSERT INTO login_risk_ip_whitelist ( + app_code, ip_address, enabled, + created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms + ) + VALUES (?, ?, 1, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + enabled = 1, + updated_by_user_id = VALUES(updated_by_user_id), + updated_at_ms = VALUES(updated_at_ms) + `, appCode, ip, nullableActor(actorID), nullableActor(actorID), nowMS, nowMS); err != nil { + return Config{}, err + } } } if err := tx.Commit(); err != nil { - return nil, err + return Config{}, err } return s.List(ctx) } @@ -132,6 +189,27 @@ func normalizeKeywords(ctx context.Context, querier countryCodeQuerier, appCode return result, nil } +func normalizeIPWhitelist(raw []string) ([]string, error) { + if len(raw) > maxIPWhitelistItems { + return nil, errInvalidPayload + } + result := make([]string, 0, len(raw)) + seen := map[string]struct{}{} + for _, item := range raw { + addr, err := netip.ParseAddr(strings.TrimSpace(item)) + if err != nil { + return nil, errInvalidPayload + } + ip := addr.String() + if _, ok := seen[ip]; ok { + continue + } + seen[ip] = struct{}{} + result = append(result, ip) + } + return result, nil +} + func resolveCountryCode(ctx context.Context, querier countryCodeQuerier, appCode string, keyword string) (string, error) { if countryCodePattern.MatchString(keyword) { return strings.ToUpper(keyword), nil @@ -186,6 +264,32 @@ func queryEnabledBlocks(ctx context.Context, db *sql.DB, appCode string) ([]Regi return items, nil } +func queryEnabledIPWhitelist(ctx context.Context, db *sql.DB, appCode string) ([]IPWhitelistItem, error) { + rows, err := db.QueryContext(ctx, ` + SELECT whitelist_id, ip_address, enabled, created_at_ms, updated_at_ms + FROM login_risk_ip_whitelist + WHERE app_code = ? AND enabled = 1 + ORDER BY ip_address ASC, whitelist_id ASC + `, appCode) + if err != nil { + return nil, err + } + defer rows.Close() + + items := make([]IPWhitelistItem, 0) + for rows.Next() { + var item IPWhitelistItem + if err := rows.Scan(&item.WhitelistID, &item.IPAddress, &item.Enabled, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil { + return nil, err + } + items = append(items, item) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + func nullableActor(actorID int64) any { if actorID <= 0 { return nil diff --git a/services/gateway-service/internal/client/user_client.go b/services/gateway-service/internal/client/user_client.go index 91ea07cc..f49bf736 100644 --- a/services/gateway-service/internal/client/user_client.go +++ b/services/gateway-service/internal/client/user_client.go @@ -17,6 +17,7 @@ type UserAuthClient interface { RefreshToken(ctx context.Context, req *userv1.RefreshTokenRequest) (*userv1.RefreshTokenResponse, error) Logout(ctx context.Context, req *userv1.LogoutRequest) (*userv1.LogoutResponse, error) RecordLoginBlocked(ctx context.Context, req *userv1.RecordLoginBlockedRequest) (*userv1.RecordLoginBlockedResponse, error) + CheckLoginRiskIPWhitelist(ctx context.Context, req *userv1.CheckLoginRiskIPWhitelistRequest) (*userv1.CheckLoginRiskIPWhitelistResponse, error) AppHeartbeat(ctx context.Context, req *userv1.AppHeartbeatRequest) (*userv1.AppHeartbeatResponse, error) } @@ -273,6 +274,10 @@ func (c *grpcUserAuthClient) RecordLoginBlocked(ctx context.Context, req *userv1 return c.client.RecordLoginBlocked(ctx, req) } +func (c *grpcUserAuthClient) CheckLoginRiskIPWhitelist(ctx context.Context, req *userv1.CheckLoginRiskIPWhitelistRequest) (*userv1.CheckLoginRiskIPWhitelistResponse, error) { + return c.client.CheckLoginRiskIPWhitelist(ctx, req) +} + func (c *grpcUserAuthClient) AppHeartbeat(ctx context.Context, req *userv1.AppHeartbeatRequest) (*userv1.AppHeartbeatResponse, error) { return c.client.AppHeartbeat(ctx, req) } diff --git a/services/gateway-service/internal/transport/http/login_risk.go b/services/gateway-service/internal/transport/http/login_risk.go index a019d0c1..9b29185e 100644 --- a/services/gateway-service/internal/transport/http/login_risk.go +++ b/services/gateway-service/internal/transport/http/login_risk.go @@ -189,6 +189,9 @@ func (h *Handler) allowLoginRisk(writer http.ResponseWriter, request *http.Reque if !config.Enabled { return true } + if h.loginRiskIPWhitelisted(request, input) { + return true + } if reason := config.FastGuard.blockReason(input.Language, input.Timezone); reason != "" { h.recordLoginBlocked(request, input, reason) httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodeAuthLoginBlocked, "login blocked") @@ -212,6 +215,28 @@ func (h *Handler) allowLoginRisk(writer http.ResponseWriter, request *http.Reque return true } +func (h *Handler) loginRiskIPWhitelisted(request *http.Request, input loginRiskInput) bool { + if h.userClient == nil { + return false + } + ip := strings.TrimSpace(clientIP(request)) + if ip == "" { + return false + } + ctx, cancel := context.WithTimeout(request.Context(), 500*time.Millisecond) + defer cancel() + resp, err := h.userClient.CheckLoginRiskIPWhitelist(ctx, &userv1.CheckLoginRiskIPWhitelistRequest{ + Meta: authRequestMetaWithLoginContext(request, "", input.Platform, input.Language, input.Timezone), + ClientIp: ip, + }) + if err != nil { + // 白名单读取失败不能制造额外放行;继续执行原地区屏蔽链路,并保留日志用于排查 Redis/DB 回源异常。 + logx.Warn(request.Context(), "login_ip_whitelist_check_failed", slog.String("component", "gateway_login_risk"), slog.String("client_ip_hash", hashLoginRiskIP(ip)), slog.String("error", err.Error())) + return false + } + return resp.GetWhitelisted() +} + func (h *Handler) recordLoginBlocked(request *http.Request, input loginRiskInput, reason string) { if h.userClient == nil { return diff --git a/services/gateway-service/internal/transport/http/response_test.go b/services/gateway-service/internal/transport/http/response_test.go index 16f5c642..ebd17d00 100644 --- a/services/gateway-service/internal/transport/http/response_test.go +++ b/services/gateway-service/internal/transport/http/response_test.go @@ -293,12 +293,14 @@ type fakeUserAuthClient struct { lastRefresh *userv1.RefreshTokenRequest lastLogout *userv1.LogoutRequest lastBlocked *userv1.RecordLoginBlockedRequest + lastWhitelist *userv1.CheckLoginRiskIPWhitelistRequest lastAppHeartbeat *userv1.AppHeartbeatRequest loginPasswordCount int loginThirdCount int refreshCount int logoutCount int blockedCount int + whitelisted bool loginErr error } @@ -681,6 +683,11 @@ func (f *fakeUserAuthClient) RecordLoginBlocked(_ context.Context, req *userv1.R return &userv1.RecordLoginBlockedResponse{Recorded: true}, nil } +func (f *fakeUserAuthClient) CheckLoginRiskIPWhitelist(_ context.Context, req *userv1.CheckLoginRiskIPWhitelistRequest) (*userv1.CheckLoginRiskIPWhitelistResponse, error) { + f.lastWhitelist = req + return &userv1.CheckLoginRiskIPWhitelistResponse{Whitelisted: f.whitelisted}, nil +} + func (f *fakeUserAuthClient) AppHeartbeat(_ context.Context, req *userv1.AppHeartbeatRequest) (*userv1.AppHeartbeatResponse, error) { f.lastAppHeartbeat = req return &userv1.AppHeartbeatResponse{Accepted: true, HeartbeatAtMs: 1_778_000_005_000, ServerTimeMs: 1_778_000_005_000, Token: &userv1.AuthToken{ @@ -5366,6 +5373,33 @@ func TestLoginRiskFastGuardBlocksLanguage(t *testing.T) { } } +func TestLoginRiskIPWhitelistBypassesFastGuard(t *testing.T) { + userClient := &fakeUserAuthClient{whitelisted: true} + handler := NewHandler(&fakeRoomClient{}, userClient) + handler.SetLoginRisk(LoginRiskConfig{ + Enabled: true, + FastGuard: LoginRiskFastGuardConfig{ + BlockedLanguagePrimaryTags: []string{"zh"}, + }, + }) + router := handler.Routes(auth.NewVerifier("secret")) + body := []byte(`{"account":"123456","password":"1234567","device_id":"ios","platform":"ios","language":"zh-CN","timezone":"America/Los_Angeles"}`) + request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/account/login", bytes.NewReader(body)) + request.Header.Set("X-Request-ID", "req-login-risk-whitelist") + request.Header.Set("X-Forwarded-For", "203.0.113.52") + recorder := httptest.NewRecorder() + + router.ServeHTTP(recorder, request) + + assertEnvelope(t, recorder, http.StatusOK, httpkit.CodeOK, "req-login-risk-whitelist") + if userClient.loginPasswordCount != 1 || userClient.blockedCount != 0 { + t.Fatalf("whitelisted IP must bypass fast guard: login=%d blocked=%d", userClient.loginPasswordCount, userClient.blockedCount) + } + if userClient.lastWhitelist == nil || userClient.lastWhitelist.GetClientIp() != "203.0.113.52" { + t.Fatalf("whitelist check request mismatch: %+v", userClient.lastWhitelist) + } +} + func TestLoginRiskIPCacheBlocksLogin(t *testing.T) { userClient := &fakeUserAuthClient{} cache := newMemoryLoginRiskCache() diff --git a/services/user-service/deploy/mysql/initdb/001_user_service.sql b/services/user-service/deploy/mysql/initdb/001_user_service.sql index c238446a..59c557b8 100644 --- a/services/user-service/deploy/mysql/initdb/001_user_service.sql +++ b/services/user-service/deploy/mysql/initdb/001_user_service.sql @@ -1236,3 +1236,18 @@ CREATE TABLE IF NOT EXISTS login_risk_country_blocks ( KEY idx_login_risk_country_blocks_enabled (app_code, enabled, country_code), KEY idx_login_risk_country_blocks_updated (app_code, updated_at_ms) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='登录风险国家拦截表'; + +CREATE TABLE IF NOT EXISTS login_risk_ip_whitelist ( + app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离', + whitelist_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '白名单 ID', + ip_address VARCHAR(64) NOT NULL COMMENT '允许跳过登录地区屏蔽的客户端 IP', + enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '启用', + created_by_user_id BIGINT NULL COMMENT '创建人用户 ID', + updated_by_user_id BIGINT NULL COMMENT '更新人用户 ID', + created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms', + updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms', + PRIMARY KEY (whitelist_id), + UNIQUE KEY uk_login_risk_ip_whitelist_ip (app_code, ip_address), + KEY idx_login_risk_ip_whitelist_enabled (app_code, enabled, ip_address), + KEY idx_login_risk_ip_whitelist_updated (app_code, updated_at_ms) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='登录风险 IP 白名单表'; diff --git a/services/user-service/internal/domain/auth/risk.go b/services/user-service/internal/domain/auth/risk.go index 539dcac3..e5b4b7eb 100644 --- a/services/user-service/internal/domain/auth/risk.go +++ b/services/user-service/internal/domain/auth/risk.go @@ -71,3 +71,14 @@ type LoginRiskCountryBlock struct { CreatedAtMs int64 UpdatedAtMs int64 } + +// LoginRiskIPWhitelist 是后台维护的登录地区屏蔽 IP 白名单事实。 +// IPAddress 使用 netip 标准化后的明文地址,便于运营审计和登录入口精确匹配。 +type LoginRiskIPWhitelist struct { + AppCode string + WhitelistID int64 + IPAddress string + Enabled bool + CreatedAtMs int64 + UpdatedAtMs int64 +} diff --git a/services/user-service/internal/service/auth/risk_cache.go b/services/user-service/internal/service/auth/risk_cache.go index 7ca54dc5..b2a3ab9c 100644 --- a/services/user-service/internal/service/auth/risk_cache.go +++ b/services/user-service/internal/service/auth/risk_cache.go @@ -59,10 +59,37 @@ func (c *RedisIPDecisionCache) SetRevokedSession(ctx context.Context, appCode st return c.client.Set(ctx, revokedSessionKey(appCode, sessionID), strings.TrimSpace(reason), ttl).Err() } +func (c *RedisIPDecisionCache) GetIPWhitelist(ctx context.Context, appCode string) ([]string, bool, error) { + raw, err := c.client.Get(ctx, loginRiskIPWhitelistKey(appCode)).Result() + if errors.Is(err, redis.Nil) { + return nil, false, nil + } + if err != nil { + return nil, false, err + } + var ips []string + if err := json.Unmarshal([]byte(raw), &ips); err != nil { + return nil, false, err + } + return ips, true, nil +} + +func (c *RedisIPDecisionCache) SetIPWhitelist(ctx context.Context, appCode string, ips []string, ttl time.Duration) error { + raw, err := json.Marshal(ips) + if err != nil { + return err + } + return c.client.Set(ctx, loginRiskIPWhitelistKey(appCode), raw, ttl).Err() +} + func loginRiskIPDecisionKey(appCode string, clientIPHash string) string { return "login_risk:ip:" + appcode.Normalize(appCode) + ":" + strings.TrimSpace(clientIPHash) } +func loginRiskIPWhitelistKey(appCode string) string { + return "login_risk:ip_whitelist:" + appcode.Normalize(appCode) +} + func revokedSessionKey(appCode string, sessionID string) string { return "auth:revoked_session:" + appcode.Normalize(appCode) + ":" + strings.TrimSpace(sessionID) } diff --git a/services/user-service/internal/service/auth/risk_policy.go b/services/user-service/internal/service/auth/risk_policy.go index c4f80bbe..ee46ba1a 100644 --- a/services/user-service/internal/service/auth/risk_policy.go +++ b/services/user-service/internal/service/auth/risk_policy.go @@ -2,13 +2,17 @@ package auth import ( "context" + "net/netip" "sort" "strings" + "time" "hyapp/pkg/appcode" authdomain "hyapp/services/user-service/internal/domain/auth" ) +const loginRiskIPWhitelistCacheTTL = 30 * time.Second + // ListLoginRiskBlockedCountries 返回当前 App 实际生效的登录地区风控词表。 // 静态配置和后台动态配置都下发给 App;App 可异步做本地风险提示,最终封禁仍以服务端 worker 为准。 func (s *Service) ListLoginRiskBlockedCountries(ctx context.Context) ([]authdomain.LoginRiskCountryBlock, error) { @@ -56,6 +60,54 @@ func (s *Service) ListLoginRiskBlockedCountries(ctx context.Context) ([]authdoma return blocks, nil } +// CheckLoginRiskIPWhitelisted 判断入口 IP 是否命中后台白名单。 +// 白名单是“跳过登录地区屏蔽”的明确运营例外,只做标准化后的单 IP 精确匹配,不扩展为网段或地区推断。 +func (s *Service) CheckLoginRiskIPWhitelisted(ctx context.Context, clientIP string) (bool, error) { + ip := normalizeLoginRiskIP(clientIP) + if ip == "" { + return false, nil + } + ips, err := s.listLoginRiskIPWhitelist(ctx) + if err != nil { + return false, err + } + for _, item := range ips { + if item == ip { + return true, nil + } + } + return false, nil +} + +func (s *Service) listLoginRiskIPWhitelist(ctx context.Context) ([]string, error) { + appCode := appcode.FromContext(ctx) + if s.ipDecisionCache != nil { + ips, ok, err := s.ipDecisionCache.GetIPWhitelist(ctx, appCode) + if err == nil && ok { + return normalizeLoginRiskIPList(ips), nil + } + } + if s.authRepository == nil { + return nil, nil + } + items, err := s.authRepository.ListEnabledLoginRiskIPWhitelist(ctx) + if err != nil { + return nil, err + } + ips := make([]string, 0, len(items)) + for _, item := range items { + if ip := normalizeLoginRiskIP(item.IPAddress); ip != "" { + ips = append(ips, ip) + } + } + ips = normalizeLoginRiskIPList(ips) + if s.ipDecisionCache != nil { + // 白名单读取结果按 App 整体缓存 30 秒;空列表也缓存,避免无配置时每次登录都回源 DB。 + _ = s.ipDecisionCache.SetIPWhitelist(ctx, appCode, ips, loginRiskIPWhitelistCacheTTL) + } + return ips, nil +} + func appendUniqueCountryBlock(blocks []authdomain.LoginRiskCountryBlock, seen map[string]struct{}, block authdomain.LoginRiskCountryBlock) []authdomain.LoginRiskCountryBlock { key := strings.ToLower(block.CountryCode + "\x00" + block.Keyword) if _, ok := seen[key]; ok { @@ -89,3 +141,29 @@ func (s *Service) countryBlockedByDynamicPolicy(ctx context.Context, countryCode } return false, nil } + +func normalizeLoginRiskIP(raw string) string { + addr, err := netip.ParseAddr(strings.TrimSpace(raw)) + if err != nil { + return "" + } + return addr.String() +} + +func normalizeLoginRiskIPList(raw []string) []string { + seen := make(map[string]struct{}, len(raw)) + ips := make([]string, 0, len(raw)) + for _, item := range raw { + ip := normalizeLoginRiskIP(item) + if ip == "" { + continue + } + if _, ok := seen[ip]; ok { + continue + } + seen[ip] = struct{}{} + ips = append(ips, ip) + } + sort.Strings(ips) + return ips +} diff --git a/services/user-service/internal/service/auth/risk_worker.go b/services/user-service/internal/service/auth/risk_worker.go index 5961deae..d7de306f 100644 --- a/services/user-service/internal/service/auth/risk_worker.go +++ b/services/user-service/internal/service/auth/risk_worker.go @@ -71,6 +71,19 @@ func normalizeLoginIPRiskWorkerOptions(options LoginIPRiskWorkerOptions) LoginIP func (s *Service) processLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error { policy := s.loginRiskPolicy nowMs := s.now().UnixMilli() + whitelisted, err := s.CheckLoginRiskIPWhitelisted(ctx, job.ClientIP) + if err != nil { + logx.Warn(ctx, "login_ip_whitelist_read_failed", slog.String("component", "user_auth_risk"), slog.String("job_id", job.JobID), slog.String("client_ip_hash", job.ClientIPHash), slog.String("error", err.Error())) + } + if whitelisted { + // 白名单是运营显式放行,命中后不再访问 GeoIP provider,也不写 IP blocked 决策,避免后续登录被旧地区缓存误拦。 + return s.applyLoginIPRiskDecision(ctx, job, IPDecision{ + Decision: authdomain.LoginIPRiskDecisionAllowed, + Source: "ip_whitelist", + CheckedAtMs: nowMs, + ExpiresAtMs: nowMs + loginRiskIPWhitelistCacheTTL.Milliseconds(), + }, nil, authdomain.LoginIPRiskStatusCompleted) + } if cached, ok := s.freshCachedIPDecision(ctx, job); ok { return s.applyLoginIPRiskDecision(ctx, job, cached, nil, authdomain.LoginIPRiskStatusSkipped) } diff --git a/services/user-service/internal/service/auth/service.go b/services/user-service/internal/service/auth/service.go index 23537705..20a21b95 100644 --- a/services/user-service/internal/service/auth/service.go +++ b/services/user-service/internal/service/auth/service.go @@ -78,6 +78,8 @@ type AuthRepository interface { CreateLoginIPRiskDecision(ctx context.Context, decision authdomain.LoginIPRiskDecision) error // ListEnabledLoginRiskCountryBlocks 返回当前 App 的动态屏蔽国家词表。 ListEnabledLoginRiskCountryBlocks(ctx context.Context) ([]authdomain.LoginRiskCountryBlock, error) + // ListEnabledLoginRiskIPWhitelist 返回当前 App 的登录地区屏蔽 IP 白名单。 + ListEnabledLoginRiskIPWhitelist(ctx context.Context) ([]authdomain.LoginRiskIPWhitelist, error) // MarkLoginAuditBlocked 把登录成功后被异步风控撤销的审计行更新成阻断态。 MarkLoginAuditBlocked(ctx context.Context, requestID string, userID int64, countryCode string, blockReason string) error } @@ -165,6 +167,8 @@ type IPDecisionCache interface { GetIPDecision(ctx context.Context, appCode string, clientIPHash string) (IPDecision, bool, error) SetIPDecision(ctx context.Context, appCode string, clientIPHash string, decision IPDecision, ttl time.Duration) error SetRevokedSession(ctx context.Context, appCode string, sessionID string, reason string, ttl time.Duration) error + GetIPWhitelist(ctx context.Context, appCode string) ([]string, bool, error) + SetIPWhitelist(ctx context.Context, appCode string, ips []string, ttl time.Duration) error } // IPDecision 是 Redis 中 IP 决策 JSON 的领域投影。 diff --git a/services/user-service/internal/service/auth/service_test.go b/services/user-service/internal/service/auth/service_test.go index 60a18a8d..aff44905 100644 --- a/services/user-service/internal/service/auth/service_test.go +++ b/services/user-service/internal/service/auth/service_test.go @@ -23,10 +23,11 @@ type memoryDecisionCache struct { mu sync.Mutex ip map[string]authservice.IPDecision revoked map[string]string + ips map[string][]string } func newMemoryDecisionCache() *memoryDecisionCache { - return &memoryDecisionCache{ip: make(map[string]authservice.IPDecision), revoked: make(map[string]string)} + return &memoryDecisionCache{ip: make(map[string]authservice.IPDecision), revoked: make(map[string]string), ips: make(map[string][]string)} } func (c *memoryDecisionCache) GetIPDecision(_ context.Context, appCode string, clientIPHash string) (authservice.IPDecision, bool, error) { @@ -50,6 +51,20 @@ func (c *memoryDecisionCache) SetRevokedSession(_ context.Context, appCode strin return nil } +func (c *memoryDecisionCache) GetIPWhitelist(_ context.Context, appCode string) ([]string, bool, error) { + c.mu.Lock() + defer c.mu.Unlock() + ips, ok := c.ips[appCode] + return append([]string(nil), ips...), ok, nil +} + +func (c *memoryDecisionCache) SetIPWhitelist(_ context.Context, appCode string, ips []string, _ time.Duration) error { + c.mu.Lock() + defer c.mu.Unlock() + c.ips[appCode] = append([]string(nil), ips...) + return nil +} + type sequenceIDGenerator struct { // values 是测试预设 user_id 序列。 values []int64 @@ -552,6 +567,73 @@ func TestLoginIPRiskWorkerRevokesBlockedCountrySession(t *testing.T) { } } +func TestLoginIPRiskIPWhitelistBypassesBlockedCountry(t *testing.T) { + ctx := context.Background() + repository := mysqltest.NewRepository(t) + seedCountry(t, repository, "CN") + now := time.UnixMilli(1000) + if _, err := repository.RawDB().ExecContext(ctx, ` + INSERT INTO login_risk_ip_whitelist ( + app_code, ip_address, enabled, created_at_ms, updated_at_ms + ) + VALUES ('lalu', '203.0.113.80', 1, 1000, 1000) + `); err != nil { + t.Fatalf("seed ip whitelist failed: %v", err) + } + cache := newMemoryDecisionCache() + authSvc := newAuthService(repository, &now, []int64{900024}, []string{"100024"}, + authservice.WithLoginRiskPolicy(authservice.LoginRiskPolicy{ + Enabled: true, + UnsupportedCountries: []string{"CN"}, + BlockedTTL: time.Hour, + AllowedTTL: time.Hour, + UnknownTTL: time.Minute, + ProviderTimeout: 50 * time.Millisecond, + ProviderNames: []string{"edge_country"}, + DenylistTTL: time.Hour, + }), + authservice.WithIPDecisionCache(cache), + authservice.WithIPGeoProviders(authservice.BuildIPGeoProviders([]string{"edge_country"})), + ) + + token, _, err := authSvc.LoginThirdParty(ctx, "wechat", "openid-risk-whitelist", thirdPartyRegistration("ios"), authservice.Meta{ + AppCode: "lalu", + RequestID: "req-risk-whitelist", + ClientIP: "203.0.113.80", + CountryByIP: "CN", + Platform: "ios", + }) + if err != nil { + t.Fatalf("LoginThirdParty failed: %v", err) + } + + now = time.UnixMilli(2000) + processed, err := authSvc.ProcessLoginIPRiskBatch(ctx, authservice.LoginIPRiskWorkerOptions{WorkerID: "risk-whitelist-test", LockTTL: time.Second, BatchSize: 10}) + if err != nil || processed != 1 { + t.Fatalf("risk worker mismatch: processed=%d err=%v", processed, err) + } + if _, err := authSvc.RefreshToken(ctx, token.RefreshToken, "dev-ios", authservice.Meta{AppCode: "lalu", RequestID: "req-refresh-whitelist"}); err != nil { + t.Fatalf("whitelisted IP must keep refresh session active: %v", err) + } + if _, ok := cache.ip["lalu:"+authservice.HashLoginRiskIP("203.0.113.80")]; ok { + t.Fatalf("whitelisted IP must not write ordinary IP decision cache") + } + if got := cache.ips["lalu"]; len(got) != 1 || got[0] != "203.0.113.80" { + t.Fatalf("ip whitelist cache mismatch: %+v", got) + } + var decision string + if err := repository.RawDB().QueryRowContext(ctx, ` + SELECT decision + FROM login_ip_risk_jobs + WHERE app_code = 'lalu' AND request_id = 'req-risk-whitelist' + `).Scan(&decision); err != nil { + t.Fatalf("query risk job failed: %v", err) + } + if decision != "allowed" { + t.Fatalf("whitelisted risk job decision mismatch: %s", decision) + } +} + func TestLoginIPRiskWorkerRevokesRotatedSameDeviceSessions(t *testing.T) { // 复现线上竞态:登录后风控任务尚未完成,客户端连续 refresh 轮换 session。 // blocked 决策落地时必须沿源 session 的同设备登录链路清掉后续 session,否则最新 access/refresh 仍能继续使用。 diff --git a/services/user-service/internal/storage/mysql/auth/risk.go b/services/user-service/internal/storage/mysql/auth/risk.go index 803fe818..abf7986d 100644 --- a/services/user-service/internal/storage/mysql/auth/risk.go +++ b/services/user-service/internal/storage/mysql/auth/risk.go @@ -131,6 +131,36 @@ func (r *Repository) ListEnabledLoginRiskCountryBlocks(ctx context.Context) ([]a return blocks, nil } +func (r *Repository) ListEnabledLoginRiskIPWhitelist(ctx context.Context) ([]authdomain.LoginRiskIPWhitelist, error) { + rows, err := r.db.QueryContext(ctx, ` + SELECT app_code, whitelist_id, ip_address, enabled, created_at_ms, updated_at_ms + FROM login_risk_ip_whitelist + WHERE app_code = ? AND enabled = 1 + ORDER BY ip_address ASC, whitelist_id ASC + `, appcode.FromContext(ctx)) + if err != nil { + return nil, err + } + defer rows.Close() + + items := make([]authdomain.LoginRiskIPWhitelist, 0) + for rows.Next() { + var item authdomain.LoginRiskIPWhitelist + if err := rows.Scan(&item.AppCode, &item.WhitelistID, &item.IPAddress, &item.Enabled, &item.CreatedAtMs, &item.UpdatedAtMs); err != nil { + return nil, err + } + item.IPAddress = strings.TrimSpace(item.IPAddress) + if item.IPAddress == "" { + continue + } + items = append(items, item) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + type loginIPRiskJobScanner interface { Scan(dest ...any) error } diff --git a/services/user-service/internal/testutil/mysqltest/mysqltest.go b/services/user-service/internal/testutil/mysqltest/mysqltest.go index bddca121..ea97a2b7 100644 --- a/services/user-service/internal/testutil/mysqltest/mysqltest.go +++ b/services/user-service/internal/testutil/mysqltest/mysqltest.go @@ -394,6 +394,11 @@ func (r *Repository) ListEnabledLoginRiskCountryBlocks(ctx context.Context) ([]a return r.Repository.AuthRepository().ListEnabledLoginRiskCountryBlocks(ctx) } +// ListEnabledLoginRiskIPWhitelist 让测试 wrapper 继续满足认证风控接口。 +func (r *Repository) ListEnabledLoginRiskIPWhitelist(ctx context.Context) ([]authdomain.LoginRiskIPWhitelist, error) { + return r.Repository.AuthRepository().ListEnabledLoginRiskIPWhitelist(ctx) +} + // MarkLoginAuditBlocked 让测试 wrapper 继续满足认证接口。 func (r *Repository) MarkLoginAuditBlocked(ctx context.Context, requestID string, userID int64, countryCode string, blockReason string) error { return r.Repository.AuthRepository().MarkLoginAuditBlocked(ctx, requestID, userID, countryCode, blockReason) diff --git a/services/user-service/internal/transport/grpc/server.go b/services/user-service/internal/transport/grpc/server.go index bbdcaab8..9e8d3045 100644 --- a/services/user-service/internal/transport/grpc/server.go +++ b/services/user-service/internal/transport/grpc/server.go @@ -212,6 +212,16 @@ func (s *Server) RecordLoginBlocked(ctx context.Context, req *userv1.RecordLogin return &userv1.RecordLoginBlockedResponse{Recorded: recorded}, nil } +// CheckLoginRiskIPWhitelist 判断登录入口 IP 是否命中后台白名单;gateway 用它在快拦截前保留运营例外。 +func (s *Server) CheckLoginRiskIPWhitelist(ctx context.Context, req *userv1.CheckLoginRiskIPWhitelistRequest) (*userv1.CheckLoginRiskIPWhitelistResponse, error) { + ctx = contextWithApp(ctx, req.GetMeta()) + whitelisted, err := s.authSvc.CheckLoginRiskIPWhitelisted(ctx, req.GetClientIp()) + if err != nil { + return nil, xerr.ToGRPCError(err) + } + return &userv1.CheckLoginRiskIPWhitelistResponse{Whitelisted: whitelisted}, nil +} + // AppHeartbeat 刷新当前 App 登录会话最近在线时间,不创建房间或麦位 presence。 func (s *Server) AppHeartbeat(ctx context.Context, req *userv1.AppHeartbeatRequest) (*userv1.AppHeartbeatResponse, error) { ctx = contextWithApp(ctx, req.GetMeta())