增加黑名单状态

pull/3727/head
hawklin2017 1 month ago
parent ac8062b7c6
commit 1d68284787

@ -29,10 +29,14 @@ func NewUserGlobalBlackApi(blacklistDB controller.UserGlobalBlackDatabase, userD
type addGlobalBlacklistReq struct {
UserIDs []string `json:"userIDs" binding:"required,min=1"`
Reason string `json:"reason"`
// Status 限制类型1=冻结可登录不能收发消息2=黑名单(不可登录,自动踢下线)
Status int32 `json:"status" binding:"required,oneof=1 2"`
}
type removeGlobalBlacklistReq struct {
UserIDs []string `json:"userIDs" binding:"required,min=1"`
// Status 目标状态0=恢复正常(同步从 blacklistDB 删除记录1=冻结2=黑名单
Status int32 `json:"status" binding:"oneof=0 1 2"`
}
type getGlobalBlacklistReq struct {
@ -45,6 +49,8 @@ type globalBlackItem struct {
OperatorID string `json:"operatorID"`
Reason string `json:"reason"`
CreateTime int64 `json:"createTime"`
// Status 限制类型1=冻结2=黑名单
Status int32 `json:"status"`
}
type getGlobalBlacklistResp struct {
@ -52,7 +58,8 @@ type getGlobalBlacklistResp struct {
Blacks []globalBlackItem `json:"blacks"`
}
// AddGlobalBlacklist 管理员将用户加入全局黑名单,并立即踢下线(所有平台 token 标记 KickedToken
// AddGlobalBlacklist 管理员设置用户限制状态。
// Status=1冻结可登录但不能收发消息Status=2黑名单不可登录自动踢下线不能收发消息。
func (b *UserGlobalBlackApi) AddGlobalBlacklist(c *gin.Context) {
var req addGlobalBlacklistReq
if err := c.ShouldBindJSON(&req); err != nil {
@ -85,31 +92,44 @@ func (b *UserGlobalBlackApi) AddGlobalBlacklist(c *gin.Context) {
Nickname: u.Nickname,
OperatorID: operatorID,
Reason: req.Reason,
Status: req.Status,
})
}
if err := b.blacklistDB.AddBlack(c, blacks); err != nil {
apiresp.GinError(c, err)
return
}
// 黑名单写入成功后,对每个被封禁用户的所有非管理员平台执行 force_logout
// 1. 断开 WS 长连接msggateway.KickUserOffline
// 2. 将 Redis 中该平台的所有 token 标记为 KickedToken
for _, black := range blacks {
for platformID := range constant.PlatformID2Name {
if int32(platformID) == constant.AdminPlatformID {
continue
}
if err := b.authClient.ForceLogout(c, black.UserID, int32(platformID)); err != nil {
// 踢下线失败不阻断主流程,记录警告即可
log.ZWarn(c, "AddGlobalBlacklist: ForceLogout failed", err,
"userID", black.UserID, "platformID", platformID)
// 同步更新 user 集合中的状态字段
for _, userID := range req.UserIDs {
if err := b.userDB.UpdateByMap(c, userID, map[string]any{"status": req.Status}); err != nil {
log.ZWarn(c, "AddGlobalBlacklist: UpdateByMap status failed", err,
"userID", userID, "status", req.Status)
}
}
// 仅黑名单Status=2需要踢下线断开 WS 长连接并将 token 标记为 KickedToken
if req.Status == model.UserStatusBlacklist {
for _, black := range blacks {
for platformID := range constant.PlatformID2Name {
if int32(platformID) == constant.AdminPlatformID {
continue
}
if err := b.authClient.ForceLogout(c, black.UserID, int32(platformID)); err != nil {
log.ZWarn(c, "AddGlobalBlacklist: ForceLogout failed", err,
"userID", black.UserID, "platformID", platformID)
}
}
}
}
apiresp.GinSuccess(c, nil)
}
// RemoveGlobalBlacklist 管理员从全局黑名单移除用户
// RemoveGlobalBlacklist 管理员更新用户账号状态。
// 执行顺序:
// 1. 将 user 集合中的 status 字段更新为请求值
// 2. 仅当 status == 0恢复正常才从 blacklistDB 删除该用户的限制记录
//
// 说明blacklistDB 是 auth/msg 层的拦截依据;状态先落 user 集合,
// 只有确认目标状态为"正常"时才清除黑名单记录,避免状态写入成功但记录未删导致仍被拦截。
func (b *UserGlobalBlackApi) RemoveGlobalBlacklist(c *gin.Context) {
var req removeGlobalBlacklistReq
if err := c.ShouldBindJSON(&req); err != nil {
@ -120,9 +140,19 @@ func (b *UserGlobalBlackApi) RemoveGlobalBlacklist(c *gin.Context) {
apiresp.GinError(c, err)
return
}
if err := b.blacklistDB.RemoveBlack(c, req.UserIDs); err != nil {
apiresp.GinError(c, err)
return
for _, userID := range req.UserIDs {
if err := b.userDB.UpdateByMap(c, userID, map[string]any{"status": req.Status}); err != nil {
log.ZError(c, "RemoveGlobalBlacklist: UpdateByMap status failed", err, "userID", userID, "status", req.Status)
apiresp.GinError(c, err)
return
}
}
// 只有目标状态为 0正常时才删除 blacklistDB 中的限制记录
if req.Status == model.UserStatusNormal {
if err := b.blacklistDB.RemoveBlack(c, req.UserIDs); err != nil {
apiresp.GinError(c, err)
return
}
}
apiresp.GinSuccess(c, nil)
}
@ -151,6 +181,7 @@ func (b *UserGlobalBlackApi) GetGlobalBlacklist(c *gin.Context) {
OperatorID: blk.OperatorID,
Reason: blk.Reason,
CreateTime: blk.CreateTime.UnixMilli(),
Status: blk.Status,
})
}
apiresp.GinSuccess(c, getGlobalBlacklistResp{Total: total, Blacks: items})

@ -32,6 +32,7 @@ import (
"github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics"
"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
pbauth "github.com/openimsdk/protocol/auth"
"github.com/openimsdk/protocol/constant"
"github.com/openimsdk/protocol/msggateway"
@ -140,13 +141,13 @@ func (s *authServer) GetUserToken(ctx context.Context, req *pbauth.GetUserTokenR
return nil, errs.ErrArgs.WrapMsg("app account can`t get token")
}
blocked, _ := s.blacklistDB.IsBlocked(ctx, req.UserID)
if blocked {
// Blacklisted users should be actively kicked to invalidate existing sessions.
// 仅黑名单status=2禁止登录冻结status=1允许获取 token仅在收发消息层面拦截
status, _ := s.blacklistDB.GetStatus(ctx, req.UserID)
if status == model.UserStatusBlacklist {
if kickErr := s.forceKickOffAllPlatforms(ctx, req.UserID); kickErr != nil {
log.ZWarn(ctx, "GetUserToken forceKickOffAllPlatforms failed", kickErr, "userID", req.UserID)
}
log.ZWarn(ctx, "GetUserToken is blocked", errors.New("user is in global blacklist, userID="+req.UserID), "userID", req.UserID, "blocked", blocked)
log.ZWarn(ctx, "GetUserToken is blocked", errors.New("user is in global blacklist, userID="+req.UserID), "userID", req.UserID, "status", status)
return nil, servererrs.ErrUserBlocked.WithDetail("user is in global blacklist, userID=" + req.UserID)
}
token, err := s.authDatabase.CreateToken(ctx, req.UserID, int(req.PlatformID))
@ -167,14 +168,13 @@ func (s *authServer) parseToken(ctx context.Context, tokensString string) (claim
if isAdmin {
return claims, nil
}
// 非管理员用户检查全局黑名单
blocked, _ := s.blacklistDB.IsBlocked(ctx, claims.UserID)
if blocked {
// Blacklisted users should be actively kicked to invalidate existing sessions.
// 非管理员用户检查全局黑名单:仅 status=2黑名单拦截status=1冻结允许通过 token 校验
status, _ := s.blacklistDB.GetStatus(ctx, claims.UserID)
if status == model.UserStatusBlacklist {
if kickErr := s.forceKickOffAllPlatforms(ctx, claims.UserID); kickErr != nil {
log.ZWarn(ctx, "parseToken forceKickOffAllPlatforms failed", kickErr, "userID", claims.UserID)
}
log.ZWarn(ctx, "parseToken is blocked", errors.New("user is in global blacklist, userID="+claims.UserID), "userID", claims.UserID, "blocked", blocked)
log.ZWarn(ctx, "parseToken is blocked", errors.New("user is in global blacklist, userID="+claims.UserID), "userID", claims.UserID, "status", status)
return nil, servererrs.ErrUserBlocked.WithDetail("user is in global blacklist, userID=" + claims.UserID)
}
m, err := s.authDatabase.GetTokensWithoutError(ctx, claims.UserID, claims.PlatformID)

@ -34,6 +34,10 @@ import (
func (m *msgServer) SendMsg(ctx context.Context, req *pbmsg.SendMsgReq) (*pbmsg.SendMsgResp, error) {
if req.MsgData != nil {
m.encapsulateMsgData(req.MsgData)
// 全局账号状态校验:冻结/黑名单用户不可收发消息
if err := m.verifyUserStatus(ctx, req); err != nil {
return nil, err
}
switch req.MsgData.SessionType {
case constant.SingleChatType:
return m.sendMsgSingleChat(ctx, req)

@ -71,6 +71,7 @@ type msgServer struct {
webhookClient *webhook.Client
conversationClient *rpcli.ConversationClient
spamReportDB database.SpamReport
globalBlackDB controller.UserGlobalBlackDatabase
}
func (m *msgServer) addInterceptorHandler(interceptorFunc ...MessageInterceptorFunc) {
@ -127,6 +128,10 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
if err != nil {
return err
}
globalBlackMgo, err := mgo.NewUserGlobalBlackMongo(mgocli.GetDB())
if err != nil {
return err
}
s := &msgServer{
MsgDatabase: msgDatabase,
RegisterCenter: client,
@ -138,6 +143,7 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
webhookClient: webhook.NewWebhookClient(config.WebhooksConfig.URL),
conversationClient: conversationClient,
spamReportDB: spamReportDB,
globalBlackDB: controller.NewUserGlobalBlackDatabase(globalBlackMgo),
}
s.notificationSender = notification.NewNotificationSender(&config.NotificationConfig, notification.WithLocalSendMsg(s.SendMsg))

@ -21,6 +21,7 @@ import (
"time"
"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
"github.com/openimsdk/protocol/constant"
"github.com/openimsdk/protocol/msg"
"github.com/openimsdk/protocol/sdkws"
@ -50,6 +51,43 @@ type MessageRevoked struct {
Seq uint32 `json:"seq"`
}
// verifyUserStatus 校验发送方/接收方的全局账号状态。
// 任意一方处于冻结(1)或黑名单(2)即拒绝消息发送/投递。
// 通知类消息NotificationBegin~NotificationEnd和管理员发送方放行。
func (m *msgServer) verifyUserStatus(ctx context.Context, data *msg.SendMsgReq) error {
if data == nil || data.MsgData == nil {
return nil
}
if data.MsgData.ContentType >= constant.NotificationBegin && data.MsgData.ContentType <= constant.NotificationEnd {
return nil
}
sendID := data.MsgData.SendID
if datautil.Contain(sendID, m.config.Share.IMAdminUserID...) {
return nil
}
if sendID != "" {
st, err := m.globalBlackDB.GetStatus(ctx, sendID)
if err != nil {
log.ZWarn(ctx, "verifyUserStatus: GetStatus(send) failed", err, "sendID", sendID)
} else if st == model.UserStatusFrozen || st == model.UserStatusBlacklist {
return servererrs.ErrUserBlocked.WithDetail("sender is restricted, status=" + strconv.Itoa(int(st)))
}
}
// 单聊:同时校验接收方状态;群聊接收方拦截在推送层处理
if data.MsgData.SessionType == constant.SingleChatType {
recvID := data.MsgData.RecvID
if recvID != "" && !datautil.Contain(recvID, m.config.Share.IMAdminUserID...) {
st, err := m.globalBlackDB.GetStatus(ctx, recvID)
if err != nil {
log.ZWarn(ctx, "verifyUserStatus: GetStatus(recv) failed", err, "recvID", recvID)
} else if st == model.UserStatusFrozen || st == model.UserStatusBlacklist {
return servererrs.ErrMsgReceiveNotAllowed.WrapMsg("receiver is restricted")
}
}
}
return nil
}
func (m *msgServer) messageVerification(ctx context.Context, data *msg.SendMsgReq) error {
switch data.MsgData.SessionType {
case constant.SingleChatType:

@ -14,8 +14,10 @@ type UserGlobalBlackDatabase interface {
AddBlack(ctx context.Context, blacks []*model.UserGlobalBlack) error
// RemoveBlack 按 userID 将用户从全局黑名单移除
RemoveBlack(ctx context.Context, userIDs []string) error
// IsBlocked 检查用户是否在全局黑名单
// IsBlocked 检查用户是否在全局黑名单(含冻结)
IsBlocked(ctx context.Context, userID string) (bool, error)
// GetStatus 返回用户限制状态0=正常1=冻结2=黑名单
GetStatus(ctx context.Context, userID string) (int32, error)
// FindBlocked 批量查询哪些 userID 在全局黑名单中,返回被封禁的记录
FindBlocked(ctx context.Context, userIDs []string) ([]*model.UserGlobalBlack, error)
// GetBlackList 分页获取黑名单列表
@ -42,6 +44,10 @@ func (u *userGlobalBlackDatabase) IsBlocked(ctx context.Context, userID string)
return u.db.IsBlocked(ctx, userID)
}
func (u *userGlobalBlackDatabase) GetStatus(ctx context.Context, userID string) (int32, error) {
return u.db.GetStatus(ctx, userID)
}
func (u *userGlobalBlackDatabase) GetBlackList(ctx context.Context, pagination pagination.Pagination) (int64, []*model.UserGlobalBlack, error) {
return u.db.Page(ctx, pagination)
}

@ -37,7 +37,7 @@ func (u *UserGlobalBlackMgo) Add(ctx context.Context, blacks []*model.UserGlobal
b.CreateTime = time.Now()
}
}
// 使用 upsert 避免重复插入报错
// 使用 upsert 避免重复插入报错status 也走 $set 以便升级/降级(冻结↔黑名单)时同步更新
for _, b := range blacks {
filter := bson.M{"user_id": b.UserID}
update := bson.M{
@ -45,6 +45,7 @@ func (u *UserGlobalBlackMgo) Add(ctx context.Context, blacks []*model.UserGlobal
"nickname": b.Nickname,
"operator_id": b.OperatorID,
"reason": b.Reason,
"status": b.Status,
},
"$setOnInsert": bson.M{
"user_id": b.UserID,
@ -59,6 +60,20 @@ func (u *UserGlobalBlackMgo) Add(ctx context.Context, blacks []*model.UserGlobal
return nil
}
// GetStatus 返回 userID 对应的限制状态:
// 0=正常无记录1=冻结2=黑名单
func (u *UserGlobalBlackMgo) GetStatus(ctx context.Context, userID string) (int32, error) {
var doc model.UserGlobalBlack
err := u.coll.FindOne(ctx, bson.M{"user_id": userID}, options.FindOne().SetProjection(bson.M{"status": 1})).Decode(&doc)
if err != nil {
if err == mongo.ErrNoDocuments {
return model.UserStatusNormal, nil
}
return model.UserStatusNormal, errs.Wrap(err)
}
return doc.Status, nil
}
func (u *UserGlobalBlackMgo) Remove(ctx context.Context, users []string) error {
if len(users) == 0 {
return nil

@ -17,6 +17,8 @@ type UserGlobalBlack interface {
Find(ctx context.Context, userIDs []string) ([]*model.UserGlobalBlack, error)
// IsBlocked 检查单个用户是否在黑名单
IsBlocked(ctx context.Context, userID string) (bool, error)
// GetStatus 返回用户限制状态0=正常1=冻结2=黑名单
GetStatus(ctx context.Context, userID string) (int32, error)
// Page 分页查询黑名单列表
Page(ctx context.Context, pagination pagination.Pagination) (count int64, blacks []*model.UserGlobalBlack, err error)
}

@ -42,6 +42,14 @@ const (
MsgReceiveSettingNobody int32 = 2
)
// UserStatus 用户账号状态枚举。
// 0=正常1=冻结可登录不能收发消息2=黑名单(不可登录,自动踢下线,不能收发消息)
const (
UserStatusNormal int32 = 0
UserStatusFrozen int32 = 1
UserStatusBlacklist int32 = 2
)
type User struct {
UserID string `bson:"user_id"`
Nickname string `bson:"nickname"`
@ -57,6 +65,8 @@ type User struct {
PhoneVisibility int32 `bson:"phone_visibility"`
CallAcceptSetting int32 `bson:"call_accept_setting"`
MsgReceiveSetting int32 `bson:"msg_receive_setting"`
// Status 账号状态0=正常1=冻结2=黑名单
Status int32 `bson:"status"`
}
func (u *User) GetNickname() string {

@ -2,11 +2,14 @@ package model
import "time"
// UserGlobalBlack 全局黑名单记录,被加入黑名单的用户无法登录
// UserGlobalBlack 全局黑名单/冻结记录。
// Status: 1=冻结可登录不能收发消息2=黑名单(不可登录,自动踢下线,不能收发消息)
type UserGlobalBlack struct {
UserID string `bson:"user_id"`
Nickname string `bson:"nickname"`
OperatorID string `bson:"operator_id"`
Reason string `bson:"reason"`
CreateTime time.Time `bson:"create_time"`
// Status 限制类型1=冻结2=黑名单
Status int32 `bson:"status"`
}

Loading…
Cancel
Save