feat: 接入消息加密与实名认证能力

This commit is contained in:
richjohnrambo
2026-09-21 09:53:39 +08:00
parent 841c0c77cd
commit e3b93113f1
33 changed files with 1747 additions and 99 deletions
+112 -8
View File
@@ -26,6 +26,7 @@ import (
"github.com/tinode/chat/server/logs"
"github.com/tinode/chat/server/media"
"github.com/tinode/chat/server/moderation"
realnamealiyun "github.com/tinode/chat/server/realname/aliyun"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/types"
"golang.org/x/crypto/bcrypt"
@@ -1691,17 +1692,20 @@ func serveIMRealNameSubmissions(wrt http.ResponseWriter, req *http.Request) {
return
}
submissionID := hashIMSecret(principal.TenantID, principal.Uid.String(), idempotencyKey, "realname")[:32]
status, reviewReason, verifiedAt := initialRealNameSubmissionState(req, principal, name, idType, idNumber)
rv := &types.RealNameVerification{
TenantID: principal.TenantID,
UserID: principal.Uid,
SubmissionID: submissionID,
Status: "pending",
Status: status,
NameCipher: protectIMSensitive(principal.TenantID, name),
IDType: idType,
IDNumberCipher: protectIMSensitive(principal.TenantID, idNumber),
IDNumberHash: hashIMSecret(principal.TenantID, idType, idNumber, "realname-id"),
MaskedName: maskIMName(name),
MaskedIDNumber: maskIMIDNumber(idNumber),
ReviewReason: reviewReason,
VerifiedAt: verifiedAt,
}
if err := store.Security.CreateRealNameSubmission(principal.TenantID, rv); err != nil {
if errors.Is(err, types.ErrDuplicate) {
@@ -1711,7 +1715,12 @@ func serveIMRealNameSubmissions(wrt http.ResponseWriter, req *http.Request) {
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
return
}
writeIMSuccess(wrt, req, map[string]any{"submission_id": submissionID, "status": "pending"})
response := map[string]any{"submission_id": submissionID, "status": status}
if status == "manual_review" {
response["status"] = "pending"
response["review_status"] = "manual_review"
}
writeIMSuccess(wrt, req, response)
}
func serveIMRealNameAppeals(wrt http.ResponseWriter, req *http.Request) {
@@ -2563,7 +2572,8 @@ func serveIMFavoriteForward(wrt http.ResponseWriter, req *http.Request) {
results = append(results, map[string]any{"target_topic": target, "favorite_id": favoriteID, "status": "failed", "reason": relationErrorReason(err)})
continue
}
head, content := favoriteForwardMessageHeadContent(item)
clientMid := forwardClientMid(req.Header.Get("Idempotency-Key"), "favorite", target, favoriteID)
head, content := favoriteForwardMessageHeadContent(item, clientMid)
msg := &types.Message{
TenantID: principal.TenantID,
Topic: target,
@@ -2581,6 +2591,7 @@ func serveIMFavoriteForward(wrt http.ResponseWriter, req *http.Request) {
if saved != nil && saved.Pending {
result := forwardResultUnknownPayload(target, saved.SeqId, relationErrorReason(err))
result["favorite_id"] = favoriteID
result["client_mid"] = clientMid
mergeForwardCommentResult(result, commentResult)
results = append(results, result)
continue
@@ -2588,6 +2599,7 @@ func serveIMFavoriteForward(wrt http.ResponseWriter, req *http.Request) {
if msg.SeqId > 0 && relationErrorReason(err) == imReasonInternal {
result := forwardResultUnknownPayload(target, msg.SeqId, relationErrorReason(err))
result["favorite_id"] = favoriteID
result["client_mid"] = clientMid
mergeForwardCommentResult(result, commentResult)
results = append(results, result)
continue
@@ -2597,11 +2609,12 @@ func serveIMFavoriteForward(wrt http.ResponseWriter, req *http.Request) {
}
if saved != nil && saved.Pending {
result := map[string]any{"target_topic": target, "favorite_id": favoriteID, "status": "pending", "seq": saved.SeqId, "reason": "MESSAGE_PERSIST_PENDING"}
result["client_mid"] = clientMid
mergeForwardCommentResult(result, commentResult)
results = append(results, result)
continue
}
result := map[string]any{"target_topic": target, "favorite_id": favoriteID, "status": "sent", "seq": saved.SeqId}
result := map[string]any{"target_topic": target, "favorite_id": favoriteID, "status": "sent", "seq": saved.SeqId, "client_mid": clientMid}
mergeForwardCommentResult(result, commentResult)
results = append(results, result)
}
@@ -6571,13 +6584,20 @@ func hashIMActionToken(tenantID types.TenantID, uid types.Uid, action, target, t
func realNamePayload(rv *types.RealNameVerification) map[string]any {
canSubmit := rv.Status == "unverified" || rv.Status == "rejected"
status := rv.Status
if status == "manual_review" {
status = "pending"
}
data := map[string]any{
"status": rv.Status,
"status": status,
"submission_id": rv.SubmissionID,
"masked_name": rv.MaskedName,
"masked_id_number": rv.MaskedIDNumber,
"can_submit": canSubmit,
}
if rv.Status == "manual_review" {
data["review_status"] = rv.Status
}
if rv.VerifiedAt != nil {
data["verified_at"] = rv.VerifiedAt.Format(time.RFC3339Nano)
}
@@ -6604,6 +6624,89 @@ func realNameAppealPayload(appeal *types.RealNameAppeal) map[string]any {
}
}
func initialRealNameSubmissionState(req *http.Request, principal *imPrincipal, name, idType, idNumber string) (string, *string, *time.Time) {
status := "pending"
policy, err := store.RealNameConfig.Policy(principal.TenantID)
if err != nil || policy == nil {
logs.Warn.Printf("realname policy lookup failed: tenant=%d uid=%s err=%v", principal.TenantID, principal.Uid, err)
return status, nil, nil
}
if policy.ReviewPolicy != types.RealNameReviewPolicyAutoManual {
return status, nil, nil
}
result := reviewIMRealNameAutomatically(req, principal, policy, name, idType, idNumber)
switch result.status {
case "verified":
now := time.Now().UTC().Round(time.Millisecond)
return "verified", nil, &now
default:
reason := strings.TrimSpace(result.reason)
if reason == "" {
reason = "自动审核未通过,已转人工审核"
}
return "manual_review", &reason, nil
}
}
type imRealNameAutoReviewResult struct {
status string
reason string
}
func reviewIMRealNameAutomatically(req *http.Request, principal *imPrincipal, policy *types.RealNamePolicy, name, idType, idNumber string) imRealNameAutoReviewResult {
values, err := store.ThirdPartyConfig.Get("im_realname",
"realname_provider",
"aliyun_region_id",
"aliyun_endpoint",
"aliyun_access_key_id",
"aliyun_access_key_secret",
"aliyun_timeout_ms",
"mock_enabled",
"mock_result")
if err != nil {
logs.Warn.Printf("realname auto review config lookup failed: tenant=%d uid=%s err=%v", principal.TenantID, principal.Uid, err)
return imRealNameAutoReviewResult{status: "manual_review", reason: "自动审核配置读取失败,已转人工审核"}
}
if parseIMBool(values["mock_enabled"]) {
switch strings.ToLower(strings.TrimSpace(values["mock_result"])) {
case "verified", "pass", "passed", "success":
return imRealNameAutoReviewResult{status: "verified"}
default:
return imRealNameAutoReviewResult{status: "manual_review", reason: "自动审核未通过,已转人工审核"}
}
}
if strings.TrimSpace(values["realname_provider"]) == "" ||
strings.TrimSpace(values["aliyun_access_key_id"]) == "" ||
strings.TrimSpace(values["aliyun_access_key_secret"]) == "" {
return imRealNameAutoReviewResult{status: "manual_review", reason: "自动审核配置未完成,已转人工审核"}
}
if !strings.EqualFold(strings.TrimSpace(values["realname_provider"]), "aliyun_cloudauth") {
return imRealNameAutoReviewResult{status: "manual_review", reason: "自动审核供应商暂不支持,已转人工审核"}
}
client, err := realnamealiyun.NewID2MetaClient(realnamealiyun.Config{
AccessKeyID: values["aliyun_access_key_id"],
AccessKeySecret: values["aliyun_access_key_secret"],
RegionID: values["aliyun_region_id"],
Endpoint: values["aliyun_endpoint"],
TimeoutMS: realnamealiyun.ParseTimeoutMS(values["aliyun_timeout_ms"], 1500),
})
if err != nil {
logs.Warn.Printf("realname auto review client init failed: tenant=%d uid=%s err=%v", principal.TenantID, principal.Uid, err)
return imRealNameAutoReviewResult{status: "manual_review", reason: "自动审核客户端初始化失败,已转人工审核"}
}
result, err := client.Verify(name, idNumber)
if err != nil {
logs.Warn.Printf("realname auto review call failed: tenant=%d uid=%s trace=%s err=%v", principal.TenantID, principal.Uid, req.Header.Get("X-Request-ID"), err)
return imRealNameAutoReviewResult{status: "manual_review", reason: "自动审核调用失败,已转人工审核"}
}
if result.Passed {
logs.Info.Printf("realname auto review passed: tenant=%d uid=%s request_id=%s latency=%s", principal.TenantID, principal.Uid, result.RequestID, result.Latency)
return imRealNameAutoReviewResult{status: "verified"}
}
logs.Info.Printf("realname auto review mismatch: tenant=%d uid=%s request_id=%s biz_code=%s latency=%s", principal.TenantID, principal.Uid, result.RequestID, result.BizCode, result.Latency)
return imRealNameAutoReviewResult{status: "manual_review", reason: "自动审核未通过,已转人工审核"}
}
func groupInvitationPayload(inv *types.GroupInvitation) map[string]any {
if inv == nil {
return map[string]any{}
@@ -6770,7 +6873,7 @@ func parseIMNonNegativeCursor(raw string) (int, error) {
func parseIMBool(raw string) bool {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "1", "true", "yes", "y":
case "1", "true", "yes", "y", "on", "enabled":
return true
default:
return false
@@ -9038,7 +9141,7 @@ func favoriteForwardContent(item *types.FavoriteItem, comment string) map[string
return payload
}
func favoriteForwardMessageHeadContent(item *types.FavoriteItem) (types.KVMap, any) {
func favoriteForwardMessageHeadContent(item *types.FavoriteItem, clientMid string) (types.KVMap, any) {
itemType := "text"
if item != nil && strings.TrimSpace(item.ItemType) != "" {
itemType = strings.TrimSpace(item.ItemType)
@@ -9059,12 +9162,13 @@ func favoriteForwardMessageHeadContent(item *types.FavoriteItem) (types.KVMap, a
if _, ok := head["type"]; !ok {
head["type"] = itemType
}
head["x-im-client-mid"] = clientMid
if content, ok := snapshot["content"]; ok {
return head, content
}
}
}
return forwardBundleMessageHead(""), favoriteForwardContent(item, "")
return forwardBundleMessageHead(clientMid), favoriteForwardContent(item, "")
}
func forwardFailurePayload(topic string, seq int64, target, reason string) map[string]any {