feat(auth): implement session-based token management with revocation support
All checks were successful
Build Backend / build (push) Successful in 2m19s
Build Backend / build-docker (push) Successful in 46s

Add Session model, SessionService, and SessionRepository to track user login
sessions and enable token revocation on auth-critical events.

- Introduce explicit TokenType (access/refresh) in JWT claims to prevent
  refresh token misuse via access token endpoints
- Add SessionID field to JWT claims, enabling stateless JWT validation
  against revoked sessions
- Replace legacy Auth middleware with RequireAuth/OptionalAuth pipeline
  that validates token type, account status, and session validity
- Implement session revocation on password change, reset, user ban/inactive,
  and explicit logout
- Add Principal cache with active invalidation for banned/role-changed users
- Fix IDOR vulnerability: GetMessagesByCursor now validates currentUserID
  is conversation participant via GetParticipantStrict
- Add group member visibility checks: announcements, group info, member list
  now require group membership
- Simplify Casbin policy: remove g grouping, use r.sub == p.sub matcher
  with globMatch; user_roles table is single source of truth for roles
- Add migration logic to clean legacy casbin g rules and migrate old p rules
  from path-style to admin/<domain> resource naming
This commit is contained in:
lafay
2026-07-05 18:28:08 +08:00
parent d240485d0e
commit eb931bf1c6
41 changed files with 2544 additions and 360 deletions

View File

@@ -91,9 +91,16 @@ type chatServiceImpl struct {
versionLogMaxGap int64
pushWorker *PushWorker
redisClient *redis.Client
// groupAbility 用于群消息发送前的群成员/禁言校验。
// 可选依赖:为 nil 时跳过群能力校验(向后兼容旧测试构造),生产环境应注入。
groupAbility GroupAbility
}
// NewChatService 创建聊天服务
//
// groupAbility 可选注入:群消息发送前的成员/禁言校验由此完成。
// 生产环境必须注入,否则群禁言规则无法生效。
func NewChatService(
repo repository.MessageRepository,
userRepo repository.UserRepository,
@@ -105,6 +112,7 @@ func NewChatService(
seqBufferMgr *cache.SeqBufferManager,
pushWorker *PushWorker,
redisClient *redis.Client,
groupAbility GroupAbility,
versionLogRepo ...repository.ConversationVersionLogRepository,
) ChatService {
// 创建适配器
@@ -131,6 +139,7 @@ func NewChatService(
uploadService: uploadService,
pushWorker: pushWorker,
redisClient: redisClient,
groupAbility: groupAbility,
}
if len(versionLogRepo) > 0 && versionLogRepo[0] != nil {
@@ -466,6 +475,14 @@ func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conv
return nil, fmt.Errorf("failed to get participant: %w", err)
}
// 群消息发送:校验发送者是否仍是群成员、是否被禁言、群是否全员禁言。
// 私聊会话不参与该校验。
if conv.Type == model.ConversationTypeGroup && conv.GroupID != nil && *conv.GroupID != "" && s.groupAbility != nil {
if err := s.groupAbility.CanSendGroupMessage(ctx, senderID, *conv.GroupID); err != nil {
return nil, err
}
}
if s.uploadService != nil {
if err := s.uploadService.ValidateChatMessageImageSegments(segments); err != nil {
return nil, err
@@ -1054,8 +1071,8 @@ func (s *chatServiceImpl) IsUserOnline(userID string) bool {
// SaveMessage 仅保存消息到数据库,不发送实时推送
// 适用于群聊等由调用方自行负责推送的场景
func (s *chatServiceImpl) SaveMessage(ctx context.Context, senderID string, conversationID string, segments model.MessageSegments, replyToID *string) (*model.Message, error) {
// 验证会话是否存在
_, err := s.getConversation(ctx, conversationID)
// 验证会话是否存在(保留 conv 用于群消息能力校验)
conv, err := s.getConversation(ctx, conversationID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("会话不存在,请重新创建会话")
@@ -1072,6 +1089,14 @@ func (s *chatServiceImpl) SaveMessage(ctx context.Context, senderID string, conv
return nil, fmt.Errorf("failed to get participant: %w", err)
}
// 群消息发送:校验发送者是否仍是群成员、是否被禁言、群是否全员禁言。
// 私聊会话不参与该校验。
if conv.Type == model.ConversationTypeGroup && conv.GroupID != nil && *conv.GroupID != "" && s.groupAbility != nil {
if err := s.groupAbility.CanSendGroupMessage(ctx, senderID, *conv.GroupID); err != nil {
return nil, err
}
}
if s.uploadService != nil {
if err := s.uploadService.ValidateChatMessageImageSegments(segments); err != nil {
return nil, err