添加全局黑名单功能

pull/3727/head
hawklin2017 2 months ago
parent 9bb769a2eb
commit 706c6000cb

@ -30,9 +30,10 @@ import (
)
type Config struct {
API config.API
Share config.Share
Discovery config.Discovery
API config.API
Share config.Share
Discovery config.Discovery
MongodbConfig config.Mongo
}
func Start(ctx context.Context, index int, cfg *Config) error {

@ -23,8 +23,11 @@ import (
"github.com/go-playground/validator/v10"
"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/database/mgo"
"github.com/openimsdk/protocol/constant"
"github.com/openimsdk/tools/apiresp"
"github.com/openimsdk/tools/db/mongoutil"
"github.com/openimsdk/tools/discovery"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/tools/mw"
@ -53,6 +56,20 @@ func prommetricsGin() gin.HandlerFunc {
}
func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, config *Config) (*gin.Engine, error) {
mgocli, err := mongoutil.NewMongoDB(ctx, config.MongodbConfig.Build())
if err != nil {
return nil, err
}
userGlobalBlackDB, err := mgo.NewUserGlobalBlackMongo(mgocli.GetDB())
if err != nil {
return nil, err
}
userDB, err := mgo.NewUserMongo(mgocli.GetDB())
if err != nil {
return nil, err
}
blacklistCtrl := controller.NewUserGlobalBlackDatabase(userGlobalBlackDB)
authConn, err := client.GetConn(ctx, config.Share.RpcRegisterName.Auth)
if err != nil {
return nil, err
@ -98,6 +115,7 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co
r.Use(prommetricsGin(), gin.RecoveryWithWriter(gin.DefaultErrorWriter, mw.GinPanicErr), mw.CorsHandler(), mw.GinParseOperationID(), GinParseToken(rpcli.NewAuthClient(authConn)))
u := NewUserApi(user.NewUserClient(userConn), client, config.Share.RpcRegisterName)
m := NewMessageApi(msg.NewMsgClient(msgConn), rpcli.NewUserClient(userConn), config.Share.IMAdminUserID)
bl := NewUserGlobalBlackApi(blacklistCtrl, userDB, config.Share.IMAdminUserID, rpcli.NewAuthClient(authConn))
userRouterGroup := r.Group("/user")
{
userRouterGroup.POST("/user_register", u.UserRegister)
@ -123,6 +141,11 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co
userRouterGroup.POST("/add_notification_account", u.AddNotificationAccount)
userRouterGroup.POST("/update_notification_account", u.UpdateNotificationAccountInfo)
userRouterGroup.POST("/search_notification_account", u.SearchNotificationAccount)
// 全局黑名单管理(仅管理员)
userRouterGroup.POST("/add_global_blacklist", bl.AddGlobalBlacklist)
userRouterGroup.POST("/remove_global_blacklist", bl.RemoveGlobalBlacklist)
userRouterGroup.POST("/get_global_blacklist", bl.GetGlobalBlacklist)
}
// friend routing group
{

@ -0,0 +1,173 @@
package api
import (
"github.com/gin-gonic/gin"
"github.com/openimsdk/open-im-server/v3/pkg/authverify"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
"github.com/openimsdk/open-im-server/v3/pkg/rpcli"
"github.com/openimsdk/protocol/constant"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/apiresp"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/tools/mcontext"
)
type UserGlobalBlackApi struct {
blacklistDB controller.UserGlobalBlackDatabase
userDB database.User
imAdminUserIDs []string
authClient *rpcli.AuthClient
}
func NewUserGlobalBlackApi(blacklistDB controller.UserGlobalBlackDatabase, userDB database.User, imAdminUserIDs []string, authClient *rpcli.AuthClient) UserGlobalBlackApi {
return UserGlobalBlackApi{blacklistDB: blacklistDB, userDB: userDB, imAdminUserIDs: imAdminUserIDs, authClient: authClient}
}
type addGlobalBlacklistReq struct {
Nicknames []string `json:"nicknames" binding:"required,min=1"`
Reason string `json:"reason"`
}
type removeGlobalBlacklistReq struct {
Nicknames []string `json:"nicknames" binding:"required,min=1"`
}
type getGlobalBlacklistReq struct {
Pagination *sdkws.RequestPagination `json:"pagination" binding:"required"`
}
type globalBlackItem struct {
UserID string `json:"userID"`
Nickname string `json:"nickname"`
OperatorID string `json:"operatorID"`
Reason string `json:"reason"`
CreateTime int64 `json:"createTime"`
}
type getGlobalBlacklistResp struct {
Total int64 `json:"total"`
Blacks []globalBlackItem `json:"blacks"`
}
// AddGlobalBlacklist 管理员将用户加入全局黑名单,并立即踢下线(所有平台 token 标记 KickedToken
func (b *UserGlobalBlackApi) AddGlobalBlacklist(c *gin.Context) {
var req addGlobalBlacklistReq
if err := c.ShouldBindJSON(&req); err != nil {
apiresp.GinError(c, errs.ErrArgs.WrapMsg(err.Error()))
return
}
if err := authverify.CheckAdmin(c, b.imAdminUserIDs); err != nil {
apiresp.GinError(c, err)
return
}
operatorID := mcontext.GetOpUserID(c)
blacks := make([]*model.UserGlobalBlack, 0, len(req.Nicknames))
for _, nickname := range req.Nicknames {
users, err := b.userDB.TakeByNickname(c, nickname)
if err != nil {
apiresp.GinError(c, err)
return
}
if len(users) == 0 {
apiresp.GinError(c, errs.ErrRecordNotFound.WrapMsg("nickname not found", "nickname", nickname))
return
}
if len(users) > 1 {
apiresp.GinError(c, errs.ErrArgs.WrapMsg("nickname matched multiple users", "nickname", nickname))
return
}
blacks = append(blacks, &model.UserGlobalBlack{
UserID: users[0].UserID,
Nickname: users[0].Nickname,
OperatorID: operatorID,
Reason: req.Reason,
})
}
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)
}
}
}
apiresp.GinSuccess(c, nil)
}
// RemoveGlobalBlacklist 管理员从全局黑名单移除用户
func (b *UserGlobalBlackApi) RemoveGlobalBlacklist(c *gin.Context) {
var req removeGlobalBlacklistReq
if err := c.ShouldBindJSON(&req); err != nil {
apiresp.GinError(c, errs.ErrArgs.WrapMsg(err.Error()))
return
}
if err := authverify.CheckAdmin(c, b.imAdminUserIDs); err != nil {
apiresp.GinError(c, err)
return
}
userIDs := make([]string, 0, len(req.Nicknames))
for _, nickname := range req.Nicknames {
users, err := b.userDB.TakeByNickname(c, nickname)
if err != nil {
apiresp.GinError(c, err)
return
}
if len(users) == 0 {
apiresp.GinError(c, errs.ErrRecordNotFound.WrapMsg("nickname not found", "nickname", nickname))
return
}
if len(users) > 1 {
apiresp.GinError(c, errs.ErrArgs.WrapMsg("nickname matched multiple users", "nickname", nickname))
return
}
userIDs = append(userIDs, users[0].UserID)
}
if err := b.blacklistDB.RemoveBlack(c, userIDs); err != nil {
apiresp.GinError(c, err)
return
}
apiresp.GinSuccess(c, nil)
}
// GetGlobalBlacklist 管理员分页查询全局黑名单
func (b *UserGlobalBlackApi) GetGlobalBlacklist(c *gin.Context) {
var req getGlobalBlacklistReq
if err := c.ShouldBindJSON(&req); err != nil {
apiresp.GinError(c, errs.ErrArgs.WrapMsg(err.Error()))
return
}
if err := authverify.CheckAdmin(c, b.imAdminUserIDs); err != nil {
apiresp.GinError(c, err)
return
}
total, blacks, err := b.blacklistDB.GetBlackList(c, req.Pagination)
if err != nil {
apiresp.GinError(c, err)
return
}
items := make([]globalBlackItem, 0, len(blacks))
for _, blk := range blacks {
items = append(items, globalBlackItem{
UserID: blk.UserID,
Nickname: blk.Nickname,
OperatorID: blk.OperatorID,
Reason: blk.Reason,
CreateTime: blk.CreateTime.UnixMilli(),
})
}
apiresp.GinSuccess(c, getGlobalBlacklistResp{Total: total, Blacks: items})
}

@ -22,6 +22,8 @@ import (
"github.com/openimsdk/open-im-server/v3/pkg/common/config"
redis2 "github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo"
"github.com/openimsdk/tools/db/mongoutil"
"github.com/openimsdk/tools/db/redisutil"
"github.com/openimsdk/tools/utils/datautil"
"github.com/redis/go-redis/v9"
@ -42,6 +44,7 @@ import (
type authServer struct {
pbauth.UnimplementedAuthServer
blacklistDB controller.UserGlobalBlackDatabase
authDatabase controller.AuthDatabase
RegisterCenter discovery.SvcDiscoveryRegistry
config *Config
@ -49,10 +52,11 @@ type authServer struct {
}
type Config struct {
RpcConfig config.Auth
RedisConfig config.Redis
Share config.Share
Discovery config.Discovery
RpcConfig config.Auth
RedisConfig config.Redis
MongodbConfig config.Mongo
Share config.Share
Discovery config.Discovery
}
func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server *grpc.Server) error {
@ -60,6 +64,14 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
if err != nil {
return err
}
mgocli, err := mongoutil.NewMongoDB(ctx, config.MongodbConfig.Build())
if err != nil {
return err
}
userGlobalBlackDB, err := mgo.NewUserGlobalBlackMongo(mgocli.GetDB())
if err != nil {
return err
}
userConn, err := client.GetConn(ctx, config.Share.RpcRegisterName.User)
if err != nil {
return err
@ -73,8 +85,9 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
config.Share.MultiLogin,
config.Share.IMAdminUserID,
),
config: config,
userClient: rpcli.NewUserClient(userConn),
config: config,
blacklistDB: controller.NewUserGlobalBlackDatabase(userGlobalBlackDB),
userClient: rpcli.NewUserClient(userConn),
})
return nil
}
@ -126,6 +139,16 @@ func (s *authServer) GetUserToken(ctx context.Context, req *pbauth.GetUserTokenR
if user.AppMangerLevel >= constant.AppNotificationAdmin {
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.
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)
return nil, servererrs.ErrUserBlocked.WithDetail("user is in global blacklist, userID=" + req.UserID)
}
token, err := s.authDatabase.CreateToken(ctx, req.UserID, int(req.PlatformID))
if err != nil {
return nil, err
@ -144,6 +167,16 @@ 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.
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)
return nil, servererrs.ErrUserBlocked.WithDetail("user is in global blacklist, userID=" + claims.UserID)
}
m, err := s.authDatabase.GetTokensWithoutError(ctx, claims.UserID, claims.PlatformID)
if err != nil {
return nil, err
@ -218,6 +251,18 @@ func (s *authServer) forceKickOff(ctx context.Context, userID string, platformID
return nil
}
func (s *authServer) forceKickOffAllPlatforms(ctx context.Context, userID string) error {
for platformID := range constant.PlatformID2Name {
if int32(platformID) == constant.AdminPlatformID {
continue
}
if err := s.forceKickOff(ctx, userID, int32(platformID)); err != nil {
return err
}
}
return nil
}
func (s *authServer) InvalidateToken(ctx context.Context, req *pbauth.InvalidateTokenReq) (*pbauth.InvalidateTokenResp, error) {
m, err := s.authDatabase.GetTokensWithoutError(ctx, req.UserID, int(req.PlatformID))
if err != nil && !errors.Is(err, redis.Nil) {

@ -37,6 +37,7 @@ func NewApiCmd() *ApiCmd {
OpenIMAPICfgFileName: &apiConfig.API,
ShareFileName: &apiConfig.Share,
DiscoveryConfigFilename: &apiConfig.Discovery,
MongodbConfigFileName: &apiConfig.MongodbConfig,
}
ret.RootCmd = NewRootCmd(program.GetProcessName(), WithConfigMap(ret.configMap))
ret.ctx = context.WithValue(context.Background(), "version", version.Version)

@ -38,6 +38,7 @@ func NewAuthRpcCmd() *AuthRpcCmd {
OpenIMRPCAuthCfgFileName: &authConfig.RpcConfig,
RedisConfigFileName: &authConfig.RedisConfig,
ShareFileName: &authConfig.Share,
MongodbConfigFileName: &authConfig.MongodbConfig,
DiscoveryConfigFilename: &authConfig.Discovery,
}
ret.RootCmd = NewRootCmd(program.GetProcessName(), WithConfigMap(ret.configMap))

@ -54,6 +54,7 @@ const (
// Account error codes.
UserIDNotFoundError = 1101 // UserID does not exist or is not registered
RegisteredAlreadyError = 1102 // user is already registered
UserBlockedError = 1103 // user is blocked (global blacklist)
// Group error codes.
GroupIDNotFoundError = 1201 // GroupID does not exist

@ -29,6 +29,7 @@ var (
ErrRecordNotFound = errs.NewCodeError(RecordNotFoundError, "RecordNotFoundError")
ErrUserIDNotFound = errs.NewCodeError(UserIDNotFoundError, "UserIDNotFoundError")
ErrUserBlocked = errs.NewCodeError(UserBlockedError, "UserBlockedError")
ErrGroupIDNotFound = errs.NewCodeError(GroupIDNotFoundError, "GroupIDNotFoundError")
ErrGroupIDExisted = errs.NewCodeError(GroupIDExisted, "GroupIDExisted")

@ -0,0 +1,45 @@
package controller
import (
"context"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
"github.com/openimsdk/tools/db/pagination"
)
// UserGlobalBlackDatabase 全局黑名单业务接口
type UserGlobalBlackDatabase interface {
// AddBlack 将用户加入全局黑名单
AddBlack(ctx context.Context, blacks []*model.UserGlobalBlack) error
// RemoveBlack 按昵称将用户从全局黑名单移除
RemoveBlack(ctx context.Context, nicknames []string) error
// IsBlocked 检查用户是否在全局黑名单
IsBlocked(ctx context.Context, userID string) (bool, error)
// GetBlackList 分页获取黑名单列表
GetBlackList(ctx context.Context, pagination pagination.Pagination) (count int64, blacks []*model.UserGlobalBlack, err error)
}
type userGlobalBlackDatabase struct {
db database.UserGlobalBlack
}
func NewUserGlobalBlackDatabase(db database.UserGlobalBlack) UserGlobalBlackDatabase {
return &userGlobalBlackDatabase{db: db}
}
func (u *userGlobalBlackDatabase) AddBlack(ctx context.Context, blacks []*model.UserGlobalBlack) error {
return u.db.Add(ctx, blacks)
}
func (u *userGlobalBlackDatabase) RemoveBlack(ctx context.Context, nicknames []string) error {
return u.db.Remove(ctx, nicknames)
}
func (u *userGlobalBlackDatabase) IsBlocked(ctx context.Context, userID string) (bool, error) {
return u.db.IsBlocked(ctx, userID)
}
func (u *userGlobalBlackDatabase) GetBlackList(ctx context.Context, pagination pagination.Pagination) (int64, []*model.UserGlobalBlack, error) {
return u.db.Page(ctx, pagination)
}

@ -0,0 +1,89 @@
package mgo
import (
"context"
"time"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
"github.com/openimsdk/tools/db/mongoutil"
"github.com/openimsdk/tools/db/pagination"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func NewUserGlobalBlackMongo(db *mongo.Database) (database.UserGlobalBlack, error) {
coll := db.Collection(database.UserGlobalBlackName)
_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{
Keys: bson.D{{Key: "user_id", Value: 1}},
Options: options.Index().SetUnique(true),
})
if err != nil {
return nil, errs.Wrap(err)
}
return &UserGlobalBlackMgo{coll: coll}, nil
}
type UserGlobalBlackMgo struct {
coll *mongo.Collection
}
func (u *UserGlobalBlackMgo) Add(ctx context.Context, blacks []*model.UserGlobalBlack) error {
for _, b := range blacks {
if b.CreateTime.IsZero() {
b.CreateTime = time.Now()
}
}
// 使用 upsert 避免重复插入报错
for _, b := range blacks {
filter := bson.M{"user_id": b.UserID}
update := bson.M{
"$set": bson.M{
"nickname": b.Nickname,
"operator_id": b.OperatorID,
"reason": b.Reason,
},
"$setOnInsert": bson.M{
"user_id": b.UserID,
"create_time": b.CreateTime,
},
}
opts := options.Update().SetUpsert(true)
if _, err := u.coll.UpdateOne(ctx, filter, update, opts); err != nil {
return errs.Wrap(err)
}
}
return nil
}
func (u *UserGlobalBlackMgo) Remove(ctx context.Context, users []string) error {
if len(users) == 0 {
return nil
}
_, err := u.coll.DeleteMany(ctx, bson.M{"user_id": bson.M{"$in": users}})
return errs.Wrap(err)
}
func (u *UserGlobalBlackMgo) Find(ctx context.Context, userIDs []string) ([]*model.UserGlobalBlack, error) {
if len(userIDs) == 0 {
return nil, nil
}
return mongoutil.Find[*model.UserGlobalBlack](ctx, u.coll, bson.M{"user_id": bson.M{"$in": userIDs}})
}
func (u *UserGlobalBlackMgo) IsBlocked(ctx context.Context, userID string) (bool, error) {
count, err := u.coll.CountDocuments(ctx, bson.M{"user_id": userID})
if err != nil {
log.ZWarn(ctx, "IsBlocked failed", err, "collection", database.UserGlobalBlackName, "userID", userID, "count", count)
return false, nil
}
return count > 0, nil
}
func (u *UserGlobalBlackMgo) Page(ctx context.Context, pagination pagination.Pagination) (int64, []*model.UserGlobalBlack, error) {
return mongoutil.FindPage[*model.UserGlobalBlack](ctx, u.coll, bson.M{}, pagination)
}

@ -17,4 +17,5 @@ const (
UserName = "user"
SeqConversationName = "seq"
SeqUserName = "seq_user"
UserGlobalBlackName = "user_global_black_list"
)

@ -0,0 +1,22 @@
package database
import (
"context"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
"github.com/openimsdk/tools/db/pagination"
)
// UserGlobalBlack 全局黑名单持久化接口
type UserGlobalBlack interface {
// Add 批量添加用户到全局黑名单
Add(ctx context.Context, blacks []*model.UserGlobalBlack) error
// Remove 按昵称从全局黑名单移除用户
Remove(ctx context.Context, nicknames []string) error
// Find 查询指定用户是否在黑名单(返回在黑名单中的记录)
Find(ctx context.Context, userIDs []string) ([]*model.UserGlobalBlack, error)
// IsBlocked 检查单个用户是否在黑名单
IsBlocked(ctx context.Context, userID string) (bool, error)
// Page 分页查询黑名单列表
Page(ctx context.Context, pagination pagination.Pagination) (count int64, blacks []*model.UserGlobalBlack, err error)
}

@ -0,0 +1,12 @@
package model
import "time"
// UserGlobalBlack 全局黑名单记录,被加入黑名单的用户无法登录
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"`
}

@ -2,6 +2,7 @@ package rpcli
import (
"context"
"github.com/openimsdk/protocol/auth"
"google.golang.org/grpc"
)
@ -28,3 +29,7 @@ func (x *AuthClient) InvalidateToken(ctx context.Context, req *auth.InvalidateTo
func (x *AuthClient) ParseToken(ctx context.Context, token string) (*auth.ParseTokenResp, error) {
return x.AuthClient.ParseToken(ctx, &auth.ParseTokenReq{Token: token})
}
func (x *AuthClient) ForceLogout(ctx context.Context, userID string, platformID int32) error {
return ignoreResp(x.AuthClient.ForceLogout(ctx, &auth.ForceLogoutReq{UserID: userID, PlatformID: platformID}))
}

@ -0,0 +1,171 @@
#!/usr/bin/env bash
set -euo pipefail
# 统一通过 API 新链路管理全局黑名单(按 nickname
#
# 用法:
# 1) 添加
# ./scripts/global_blacklist_api.sh add "alice,bob" [reason]
#
# 2) 删除
# ./scripts/global_blacklist_api.sh remove "alice,bob"
#
# 3) 查询
# ./scripts/global_blacklist_api.sh list [pageNumber] [showNumber]
#
# 环境变量(可覆盖):
# OPENIM_API_ADDR 默认: http://127.0.0.1:10002
# ADMIN_TOKEN 管理员 token如未提供则自动调用 /auth/get_admin_token 获取)
# OPENIM_SECRET 获取管理员 token 所需 secret默认: openIM123
# ADMIN_USER_ID 获取管理员 token 所需 userID默认: imAdmin
OPENIM_API_ADDR="${OPENIM_API_ADDR:-http://127.0.0.1:10002}"
ADMIN_TOKEN="${ADMIN_TOKEN:-}"
OPENIM_SECRET="${OPENIM_SECRET:-openIM123}"
ADMIN_USER_ID="${ADMIN_USER_ID:-imAdmin}"
OPERATION_ID="${OPERATION_ID:-gb_$(date +%s)_$RANDOM}"
ACTION="${1:-}"
NICKNAMES_RAW="${2:-}"
REASON="${3:-manual_by_api_script}"
PAGE_NUMBER="${2:-1}"
SHOW_NUMBER="${3:-20}"
die() {
echo "ERROR: $*" >&2
exit 1
}
trim() {
local s="$1"
s="${s#"${s%%[![:space:]]*}"}"
s="${s%"${s##*[![:space:]]}"}"
printf '%s' "$s"
}
nicknames_csv_to_json_array() {
local csv="$1"
local arr_json="["
local first=1
local item
IFS=',' read -r -a _items <<< "$csv"
for item in "${_items[@]}"; do
item="$(trim "$item")"
[[ -z "$item" ]] && continue
if [[ $first -eq 1 ]]; then
arr_json="${arr_json}\"${item}\""
first=0
else
arr_json="${arr_json},\"${item}\""
fi
done
arr_json="${arr_json}]"
if [[ "$arr_json" == "[]" ]]; then
die "nicknames 为空,请传入逗号分隔昵称,如 \"alice,bob\""
fi
printf '%s' "$arr_json"
}
get_admin_token() {
local uid body resp token last_resp
local -a candidates=("${ADMIN_USER_ID}" "openIM123456" "imAdmin")
last_resp=""
for uid in "${candidates[@]}"; do
body="{\"secret\":\"${OPENIM_SECRET}\",\"userID\":\"${uid}\"}"
resp="$(curl -sS -X POST "${OPENIM_API_ADDR}/auth/get_admin_token" \
-H "Content-Type: application/json" \
-H "operationID: ${OPERATION_ID}" \
-d "$body")"
last_resp="$resp"
token="$(python3 - <<'PY' "$resp"
import json
import sys
raw = sys.argv[1]
try:
obj = json.loads(raw)
except Exception:
print("")
raise SystemExit(0)
token = ""
if isinstance(obj, dict):
data = obj.get("data")
if isinstance(data, dict):
token = data.get("token") or data.get("Token") or ""
if not token:
token = obj.get("token") or obj.get("Token") or ""
print(token)
PY
)"
if [[ -n "$token" ]]; then
echo "自动获取管理员 token 成功userID=${uid}" >&2
printf '%s' "$token"
return 0
fi
done
echo "get_admin_token raw response: $last_resp" >&2
die "自动获取管理员 token 失败,请检查 OPENIM_API_ADDR/OPENIM_SECRET/ADMIN_USER_ID当前: ${ADMIN_USER_ID}),或直接设置 ADMIN_TOKEN"
}
call_api() {
local path="$1"
local body="$2"
local token="$3"
curl -sS -X POST "${OPENIM_API_ADDR}${path}" \
-H "Content-Type: application/json" \
-H "operationID: ${OPERATION_ID}" \
-H "token: ${token}" \
-d "$body"
}
if [[ -z "$ACTION" ]]; then
cat <<'EOF'
用法:
添加: ./scripts/global_blacklist_api.sh add "alice,bob" [reason]
删除: ./scripts/global_blacklist_api.sh remove "alice,bob"
查询: ./scripts/global_blacklist_api.sh list [pageNumber] [showNumber]
EOF
exit 1
fi
if [[ -z "$ADMIN_TOKEN" ]]; then
echo "ADMIN_TOKEN 未设置,尝试自动获取管理员 token..."
ADMIN_TOKEN="$(get_admin_token)"
fi
case "$ACTION" in
add)
[[ -z "$NICKNAMES_RAW" ]] && die "add 需要 nicknames 参数"
NICKNAMES_JSON="$(nicknames_csv_to_json_array "$NICKNAMES_RAW")"
BODY="{\"nicknames\":${NICKNAMES_JSON},\"reason\":\"${REASON}\"}"
echo ">>> POST /user/add_global_blacklist"
call_api "/user/add_global_blacklist" "$BODY" "$ADMIN_TOKEN"
;;
remove)
[[ -z "$NICKNAMES_RAW" ]] && die "remove 需要 nicknames 参数"
NICKNAMES_JSON="$(nicknames_csv_to_json_array "$NICKNAMES_RAW")"
BODY="{\"nicknames\":${NICKNAMES_JSON}}"
echo ">>> POST /user/remove_global_blacklist"
call_api "/user/remove_global_blacklist" "$BODY" "$ADMIN_TOKEN"
;;
list)
BODY="{\"pagination\":{\"pageNumber\":${PAGE_NUMBER},\"showNumber\":${SHOW_NUMBER}}}"
echo ">>> POST /user/get_global_blacklist"
call_api "/user/get_global_blacklist" "$BODY" "$ADMIN_TOKEN"
;;
*)
die "不支持的 action: ${ACTION}(仅支持 add/remove/list"
;;
esac
echo
Loading…
Cancel
Save