topfans/backend/services/socialService/provider/social_provider.go
zerosaturation b7022d2dc1 fix(backend): auth boundary — trusted identity from ctx (batch 2)
- 新增 pkg/authctx: 从 Dubbo attachment/gRPC metadata 提取可信 user_id/star_id,
  统一覆盖 req 同名字段, 缺身份返 Unauthenticated。
- 各 provider 接入(堵身份伪造/越权):
  * moderation SubmitReport 等 6 RPC(举报人伪造)
  * asset CheckAssetLike/GetAssetQrcode/TrackShare(点赞/分享归因伪造)
  * social CheckFriendship(修 starID=0 隐私预言机)
  * activity PurchaseItem/BatchPurchaseItem(水晶扣费伪造)等 5 RPC
  * gallery/aiChat/task/notification 迁移 authctx, 删散落 extractUserInfo*
- social 正确性: GetUserLikedAssets OR 显式分组(防御); GetRandomUsersByStar 真随机(去 rand.Seed)。
- gateway: /auth/validate 移入 AuthMiddleware 保护组(/refresh 保留,依赖注入身份)。
- 删 userService 已迁移死函数; notification 缺身份错误码统一为 Unauthenticated。
- 各 provider 单测(伪造身份被覆盖 + 缺身份拒绝)。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-23 01:12:35 +08:00

596 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package provider
import (
"context"
"fmt"
"strconv"
"time"
"dubbo.apache.org/dubbo-go/v3/common/constant"
"github.com/topfans/backend/pkg/authctx"
"github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/pkg/proto/common"
pb "github.com/topfans/backend/pkg/proto/social"
"github.com/topfans/backend/services/socialService/service"
"go.uber.org/zap"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// SocialProvider 社交服务Provider实现
// 实现 Triple 协议生成的 SocialServiceHandler 接口
type SocialProvider struct {
friendService service.FriendService
assetLikeService *service.AssetLikeService
}
// 确保 SocialProvider 实现了 SocialServiceHandler 接口
var _ pb.SocialServiceHandler = (*SocialProvider)(nil)
// NewSocialProvider 创建社交服务Provider实例
func NewSocialProvider(friendService service.FriendService, assetLikeService *service.AssetLikeService) *SocialProvider {
return &SocialProvider{
friendService: friendService,
assetLikeService: assetLikeService,
}
}
// ========== 好友请求相关 ==========
// SendFriendRequest 发送好友请求
func (p *SocialProvider) SendFriendRequest(ctx context.Context, req *pb.SendFriendRequestRequest) (*pb.SendFriendRequestResponse, error) {
// 从上下文获取用户信息由middleware注入
userID, starID, err := extractUserInfo(ctx)
if err != nil {
logger.Logger.Warn("Failed to extract user info from context", zap.Error(err))
return nil, err
}
logger.Logger.Debug("SendFriendRequest called",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int64("friend_user_id", req.FriendUserId),
)
return p.friendService.SendFriendRequest(ctx, req, userID, starID)
}
// GetFriendRequests 获取好友请求列表
func (p *SocialProvider) GetFriendRequests(ctx context.Context, req *pb.GetFriendRequestsRequest) (*pb.GetFriendRequestsResponse, error) {
// 从上下文获取用户信息
userID, starID, err := extractUserInfo(ctx)
if err != nil {
logger.Logger.Warn("Failed to extract user info from context", zap.Error(err))
return nil, err
}
logger.Logger.Debug("GetFriendRequests called",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.String("type", req.Type),
zap.String("status", req.Status),
)
return p.friendService.GetFriendRequests(ctx, req, userID, starID)
}
// HandleFriendRequest 处理好友请求(接受/拒绝)
func (p *SocialProvider) HandleFriendRequest(ctx context.Context, req *pb.HandleFriendRequestRequest) (*pb.HandleFriendRequestResponse, error) {
// 从上下文获取用户信息
userID, starID, err := extractUserInfo(ctx)
if err != nil {
logger.Logger.Warn("Failed to extract user info from context", zap.Error(err))
return nil, err
}
logger.Logger.Debug("HandleFriendRequest called",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int64("request_id", req.RequestId),
zap.String("action", req.Action),
)
return p.friendService.HandleFriendRequest(ctx, req, userID, starID)
}
// ========== 好友关系相关 ==========
// GetFriendList 获取好友列表
func (p *SocialProvider) GetFriendList(ctx context.Context, req *pb.GetFriendListRequest) (*pb.GetFriendListResponse, error) {
// 从上下文获取用户信息
userID, starID, err := extractUserInfo(ctx)
if err != nil {
logger.Logger.Warn("Failed to extract user info from context", zap.Error(err))
return nil, err
}
logger.Logger.Debug("GetFriendList called",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.String("keyword", req.Keyword),
)
return p.friendService.GetFriendList(ctx, req, userID, starID)
}
// DeleteFriend 删除好友
func (p *SocialProvider) DeleteFriend(ctx context.Context, req *pb.DeleteFriendRequest) (*pb.DeleteFriendResponse, error) {
// 从上下文获取用户信息
userID, starID, err := extractUserInfo(ctx)
if err != nil {
logger.Logger.Warn("Failed to extract user info from context", zap.Error(err))
return nil, err
}
logger.Logger.Debug("DeleteFriend called",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int64("friend_user_id", req.FriendUserId),
)
return p.friendService.DeleteFriend(ctx, req, userID, starID)
}
// SetFriendRemark 设置好友备注
func (p *SocialProvider) SetFriendRemark(ctx context.Context, req *pb.SetFriendRemarkRequest) (*pb.SetFriendRemarkResponse, error) {
// 从上下文获取用户信息
userID, starID, err := extractUserInfo(ctx)
if err != nil {
logger.Logger.Warn("Failed to extract user info from context", zap.Error(err))
return nil, err
}
logger.Logger.Debug("SetFriendRemark called",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int64("friend_user_id", req.FriendUserId),
)
return p.friendService.SetFriendRemark(ctx, req, userID, starID)
}
// CheckFriendship 检查好友关系
//
// 安全语义调用者身份user_id, star_id一律从 ctx 里取可信值,覆盖
// req.UserId调用方任意可填不可信。旧实现直接用 req.UserId + 硬编码
// starID=0导致隐私预言机——攻击者可探测任意两人是否好友。新实现让调用者
// 只能查询"我与 req.FriendUserId 在当前明星下是否好友"。
// 缺身份 → Unauthenticated不静默放行。
func (p *SocialProvider) CheckFriendship(ctx context.Context, req *pb.CheckFriendshipRequest) (*pb.CheckFriendshipResponse, error) {
userID, starID, err := authctx.ExtractIdentity(ctx)
if err != nil {
logger.Logger.Warn("CheckFriendship: identity required", zap.Error(err))
return nil, status.Error(codes.Unauthenticated, "identity required")
}
if req.FriendUserId == 0 {
return nil, status.Error(codes.InvalidArgument, "friend_user_id required")
}
logger.Logger.Debug("CheckFriendship called",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int64("friend_user_id", req.FriendUserId),
)
return p.friendService.CheckFriendship(ctx, userID, starID, req.FriendUserId)
}
// GetFriendCount 获取好友数量
func (p *SocialProvider) GetFriendCount(ctx context.Context, req *pb.GetFriendCountRequest) (*pb.GetFriendCountResponse, error) {
// 从上下文获取用户信息
userID, starID, err := extractUserInfo(ctx)
if err != nil {
logger.Logger.Warn("Failed to extract user info from context", zap.Error(err))
return nil, err
}
logger.Logger.Debug("GetFriendCount called",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
return p.friendService.GetFriendCount(ctx, req, userID, starID)
}
// ========== 辅助方法 ==========
// SearchUserForFriend 查找用户信息(用于加好友)
func (p *SocialProvider) SearchUserForFriend(ctx context.Context, req *pb.SearchUserForFriendRequest) (*pb.SearchUserForFriendResponse, error) {
// 从上下文获取用户信息
userID, starID, err := extractUserInfo(ctx)
if err != nil {
logger.Logger.Warn("Failed to extract user info from context", zap.Error(err))
return nil, err
}
logger.Logger.Debug("SearchUserForFriend called",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int64("friend_fan_profile_id", req.GetFriendFanProfileId()),
)
return p.friendService.SearchUserForFriend(ctx, req, userID, starID)
}
// GetUsersPaged 获取分页用户列表(同一明星身份下)
func (p *SocialProvider) GetUsersPaged(ctx context.Context, req *pb.GetUsersPagedRequest) (*pb.GetUsersPagedResponse, error) {
userID, starID, err := extractUserInfo(ctx)
if err != nil {
logger.Logger.Warn("Failed to extract user info from context", zap.Error(err))
return nil, err
}
logger.Logger.Debug("GetUsersPaged called",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int32("page", req.Page),
zap.Int32("page_size", req.PageSize),
)
return p.friendService.GetUsersPaged(ctx, req, userID, starID)
}
// GetRandomUsers 获取随机用户(同一明星下)
func (p *SocialProvider) GetRandomUsers(ctx context.Context, req *pb.GetRandomUsersRequest) (*pb.GetRandomUsersResponse, error) {
// 从上下文获取用户信息
userID, starID, err := extractUserInfo(ctx)
if err != nil {
logger.Logger.Warn("Failed to extract user info from context", zap.Error(err))
return nil, err
}
logger.Logger.Debug("GetRandomUsers called",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int32("count", req.Count),
)
return p.friendService.GetRandomUsers(ctx, req, userID, starID)
}
// ========== 资产点赞相关 ==========
// LikeAsset 点赞资产
func (p *SocialProvider) LikeAsset(ctx context.Context, req *pb.LikeAssetRequest) (*pb.LikeAssetResponse, error) {
// 从上下文获取用户信息
userID, starID, err := extractUserInfo(ctx)
if err != nil {
logger.Logger.Warn("Failed to extract user info from context", zap.Error(err))
return &pb.LikeAssetResponse{
Base: &common.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "Unauthorized: " + err.Error(),
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
logger.Logger.Debug("LikeAsset called",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int64("asset_id", req.AssetId),
)
// 调用业务逻辑层
likeCount, err := p.assetLikeService.LikeAsset(ctx, req.AssetId, userID, starID)
if err != nil {
logger.Logger.Error("Failed to like asset",
zap.Error(err),
zap.Int64("asset_id", req.AssetId),
zap.Int64("user_id", userID),
)
return &pb.LikeAssetResponse{
Base: &common.BaseResponse{
Code: uint32(codes.Internal),
Message: err.Error(),
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
return &pb.LikeAssetResponse{
Base: &common.BaseResponse{
Code: uint32(codes.OK),
Message: "Successfully liked asset",
Timestamp: time.Now().UnixMilli(),
},
AssetId: req.AssetId,
NewLikeCount: likeCount,
IsLiked: true,
}, nil
}
// UnlikeAsset 取消点赞资产
func (p *SocialProvider) UnlikeAsset(ctx context.Context, req *pb.UnlikeAssetRequest) (*pb.UnlikeAssetResponse, error) {
// 从上下文获取用户信息
userID, starID, err := extractUserInfo(ctx)
if err != nil {
logger.Logger.Warn("Failed to extract user info from context", zap.Error(err))
return &pb.UnlikeAssetResponse{
Base: &common.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "Unauthorized: " + err.Error(),
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
logger.Logger.Debug("UnlikeAsset called",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int64("asset_id", req.AssetId),
)
// 调用业务逻辑层
if err := p.assetLikeService.UnlikeAsset(ctx, req.AssetId, userID, starID); err != nil {
logger.Logger.Error("Failed to unlike asset",
zap.Error(err),
zap.Int64("asset_id", req.AssetId),
zap.Int64("user_id", userID),
)
return &pb.UnlikeAssetResponse{
Base: &common.BaseResponse{
Code: uint32(codes.Internal),
Message: err.Error(),
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
return &pb.UnlikeAssetResponse{
Base: &common.BaseResponse{
Code: uint32(codes.OK),
Message: "Successfully unliked asset",
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
// CheckAssetLike 检查是否已点赞资产
func (p *SocialProvider) CheckAssetLike(ctx context.Context, req *pb.CheckAssetLikeRequest) (*pb.CheckAssetLikeResponse, error) {
// 从上下文获取用户信息
userID, starID, err := extractUserInfo(ctx)
if err != nil {
logger.Logger.Warn("Failed to extract user info from context", zap.Error(err))
return &pb.CheckAssetLikeResponse{
Base: &common.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "Unauthorized: " + err.Error(),
Timestamp: time.Now().UnixMilli(),
},
IsLiked: false,
}, nil
}
logger.Logger.Debug("CheckAssetLike called",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int64("asset_id", req.AssetId),
)
// 调用业务逻辑层
isLiked, err := p.assetLikeService.CheckAssetLike(ctx, req.AssetId, userID, starID)
if err != nil {
logger.Logger.Error("Failed to check asset like",
zap.Error(err),
zap.Int64("asset_id", req.AssetId),
zap.Int64("user_id", userID),
)
return &pb.CheckAssetLikeResponse{
Base: &common.BaseResponse{
Code: uint32(codes.Internal),
Message: err.Error(),
Timestamp: time.Now().UnixMilli(),
},
IsLiked: false,
}, nil
}
return &pb.CheckAssetLikeResponse{
Base: &common.BaseResponse{
Code: uint32(codes.OK),
Message: "Successfully checked asset like status",
Timestamp: time.Now().UnixMilli(),
},
IsLiked: isLiked,
}, nil
}
// GetAssetLikers 获取资产点赞用户列表
func (p *SocialProvider) GetAssetLikers(ctx context.Context, req *pb.GetAssetLikersRequest) (*pb.GetAssetLikersResponse, error) {
// 从上下文获取用户信息starID 可选,用于 JOIN fan_profiles
_, starID, err := extractUserInfo(ctx)
if err != nil {
// starID 获取失败不影响查询,使用 0 让 service 层自动获取
starID = 0
logger.Logger.Warn("Failed to extract star_id from context, will get from asset",
zap.Error(err),
)
}
logger.Logger.Debug("GetAssetLikers called",
zap.Int64("asset_id", req.AssetId),
zap.Int64("star_id", starID),
zap.Int64("cursor", req.Cursor),
zap.Int32("page_size", req.PageSize),
)
return p.assetLikeService.GetAssetLikers(ctx, req.AssetId, starID, req.Cursor, req.PageSize)
}
// ========== 我的作品相关 ==========
// GetMyLikedAssets 获取我点赞的作品列表
func (p *SocialProvider) GetMyLikedAssets(ctx context.Context, req *pb.GetMyLikedAssetsRequest) (*pb.GetMyLikedAssetsResponse, error) {
// 从上下文获取用户信息
userID, starID, err := extractUserInfo(ctx)
if err != nil {
logger.Logger.Warn("Failed to extract user info from context", zap.Error(err))
return &pb.GetMyLikedAssetsResponse{
Base: &common.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "Unauthorized: " + err.Error(),
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
logger.Logger.Debug("GetMyLikedAssets called",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int32("page", req.Page),
zap.Int32("page_size", req.PageSize),
)
return p.assetLikeService.GetMyLikedAssets(ctx, req, userID, starID)
}
// GetMyTodayLikedAssets 获取我今日点赞的作品列表
func (p *SocialProvider) GetMyTodayLikedAssets(ctx context.Context, req *pb.GetMyTodayLikedAssetsRequest) (*pb.GetMyTodayLikedAssetsResponse, error) {
userID, starID, err := extractUserInfo(ctx)
if err != nil {
logger.Logger.Warn("Failed to extract user info from context", zap.Error(err))
return &pb.GetMyTodayLikedAssetsResponse{
Base: &common.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "Unauthorized: " + err.Error(),
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
logger.Logger.Debug("GetMyTodayLikedAssets called",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int32("page", req.Page),
zap.Int32("page_size", req.PageSize),
)
return p.assetLikeService.GetMyTodayLikedAssets(ctx, req, userID, starID)
}
// GetMyWeekLikedAssets 获取我本周点赞的作品列表
func (p *SocialProvider) GetMyWeekLikedAssets(ctx context.Context, req *pb.GetMyWeekLikedAssetsRequest) (*pb.GetMyWeekLikedAssetsResponse, error) {
userID, starID, err := extractUserInfo(ctx)
if err != nil {
logger.Logger.Warn("Failed to extract user info from context", zap.Error(err))
return &pb.GetMyWeekLikedAssetsResponse{
Base: &common.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "Unauthorized: " + err.Error(),
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
logger.Logger.Debug("GetMyWeekLikedAssets called",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int32("page", req.Page),
zap.Int32("page_size", req.PageSize),
)
return p.assetLikeService.GetMyWeekLikedAssets(ctx, req, userID, starID)
}
// GetUserLikedAssets 获取他人点赞的作品列表
func (p *SocialProvider) GetUserLikedAssets(ctx context.Context, req *pb.GetUserLikedAssetsRequest) (*pb.GetUserLikedAssetsResponse, error) {
userID, starID, err := extractUserInfo(ctx)
if err != nil {
logger.Logger.Warn("Failed to extract user info from context", zap.Error(err))
return &pb.GetUserLikedAssetsResponse{
Base: &common.BaseResponse{
Code: uint32(codes.Unauthenticated),
Message: "Unauthorized: " + err.Error(),
Timestamp: time.Now().UnixMilli(),
},
}, nil
}
logger.Logger.Debug("GetUserLikedAssets called",
zap.Int64("target_user_id", req.UserId),
zap.Int64("caller_user_id", userID),
zap.Int64("star_id", starID),
zap.Int32("page", req.Page),
zap.Int32("page_size", req.PageSize),
)
return p.assetLikeService.GetUserLikedAssets(ctx, req, req.UserId, starID)
}
// extractUserInfo 从 Dubbo attachments 中提取用户信息
// 网关调用:网关已验证 Token 并将 user_id 和 star_id 通过 attachments 传递
func extractUserInfo(ctx context.Context) (int64, int64, error) {
logger.Logger.Debug("Extracting user info from Dubbo attachments")
// 使用正确的 constant.AttachmentKey 获取 Dubbo attachments
if attachments := ctx.Value(constant.AttachmentKey); attachments != nil {
logger.Logger.Debug("Found attachments in context", zap.Any("attachments", attachments))
if attMap, ok := attachments.(map[string]interface{}); ok {
userID, starID := extractUserInfoFromMap(attMap)
if userID > 0 && starID > 0 {
logger.Logger.Debug("Successfully extracted user info",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
return userID, starID, nil
}
logger.Logger.Warn("Extracted zero user_id or star_id",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
)
} else {
logger.Logger.Warn("Attachments is not a map[string]interface{}",
zap.String("type", fmt.Sprintf("%T", attachments)),
)
}
} else {
logger.Logger.Warn("No attachments found in context")
}
return 0, 0, fmt.Errorf("user info not found in Dubbo attachments (expected user_id and star_id from gateway)")
}
// extractUserInfoFromMap 从 map 中提取 user_id 和 star_id
// 支持多种类型int64, float64, string
func extractUserInfoFromMap(attMap map[string]interface{}) (int64, int64) {
var userID, starID int64
// 提取 user_id
if v, ok := attMap["user_id"]; ok {
userID = parseIntValue(v)
}
// 提取 star_id
if v, ok := attMap["star_id"]; ok {
starID = parseIntValue(v)
}
return userID, starID
}
// parseIntValue 解析不同类型的整数值
func parseIntValue(v interface{}) int64 {
switch val := v.(type) {
case int64:
return val
case int:
return int64(val)
case float64:
return int64(val)
case string:
if i, err := strconv.ParseInt(val, 10, 64); err == nil {
return i
}
case []string:
if len(val) > 0 {
if i, err := strconv.ParseInt(val[0], 10, 64); err == nil {
return i
}
}
}
return 0
}