0.11.2: 旗舰版第三轮完善(通用聊天/企微钉钉飞书扫码登录/个人安全策略)
- 门户通用聊天:选择已批准模型直接对话,审批通过后自动开通用户级运行时 API Key(加密落库,限额取批准值),聊天经受管网关统一认证/限流/配额/审计; 会话哈希链完整性 + busy 租约防并发,失败不落库。 - 扫码登录:identity_providers 扩展 wecom/dingtalk/feishu,管理端配置 (AppID/AppSecret/AgentID/回调/自动开户/默认部门),登录页自动展示; one-time state 防 CSRF,provider_uid 全局唯一防多账号绑定,平台端点 固定公网 URL 复用 public-only 拨号。 - 个人安全策略:账号安全页(登录设备管理/吊销非当前会话/登录提醒开关/ 扫码绑定解绑),登录成功发布 security.login_detected 事件按偏好落站内信 (新增 security 类别),会话索引只存令牌摘要并惰性清理。 - 迁移 000038-000041;修复 social update 参数越界/凭据回读/路由挂载缺失; 全量测试 25 包通过,前端 admin/portal 构建通过,端到端验证完成。
This commit is contained in:
@@ -2,6 +2,7 @@ package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -96,7 +97,17 @@ func (s *Service) ClientIP(r *http.Request) string {
|
||||
return s.limiter.ClientIP(r)
|
||||
}
|
||||
|
||||
// SessionMeta 携带登录环境信息(IP/UA),用于设备管理与登录提醒。
|
||||
type SessionMeta struct {
|
||||
IP string
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
func (s *Service) Login(ctx context.Context, kind Kind, login, password string) (LoginResult, error) {
|
||||
return s.LoginWithMeta(ctx, kind, login, password, SessionMeta{})
|
||||
}
|
||||
|
||||
func (s *Service) LoginWithMeta(ctx context.Context, kind Kind, login, password string, meta SessionMeta) (LoginResult, error) {
|
||||
account, err := s.findByLogin(ctx, kind, login)
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
_ = s.hasher.Verify(password, dummyPasswordHash)
|
||||
@@ -139,7 +150,7 @@ func (s *Service) Login(ctx context.Context, kind Kind, login, password string)
|
||||
upgradedHash = &hash
|
||||
}
|
||||
principal := principalFor(account)
|
||||
token, err := s.sessions.Create(ctx, principal)
|
||||
token, err := s.sessions.CreateWithMeta(ctx, principal, meta.IP, meta.UserAgent)
|
||||
if err != nil {
|
||||
return LoginResult{}, err
|
||||
}
|
||||
@@ -147,10 +158,17 @@ func (s *Service) Login(ctx context.Context, kind Kind, login, password string)
|
||||
_ = s.sessions.Delete(ctx, "Bearer "+token)
|
||||
return LoginResult{}, err
|
||||
}
|
||||
if kind == KindPortal {
|
||||
s.NotifyLogin(ctx, account.ID, meta)
|
||||
}
|
||||
return LoginResult{Token: token, Account: account}, nil
|
||||
}
|
||||
|
||||
func (s *Service) CompleteTOTPLogin(ctx context.Context, kind Kind, tempToken, code, backupCode string) (LoginResult, error) {
|
||||
return s.CompleteTOTPLoginWithMeta(ctx, kind, tempToken, code, backupCode, SessionMeta{})
|
||||
}
|
||||
|
||||
func (s *Service) CompleteTOTPLoginWithMeta(ctx context.Context, kind Kind, tempToken, code, backupCode string, meta SessionMeta) (LoginResult, error) {
|
||||
// 先只读取(不消费)挑战令牌:验证码输错时令牌保留,用户可用同一
|
||||
// 令牌重试,而不是每个笔误都强制重新走完整登录。
|
||||
principal, err := s.sessions.AuthenticatePending(ctx, tempToken, kind)
|
||||
@@ -189,7 +207,7 @@ func (s *Service) CompleteTOTPLogin(ctx context.Context, kind Kind, tempToken, c
|
||||
if _, err := s.sessions.ConsumePending(ctx, tempToken, kind); err != nil {
|
||||
return LoginResult{}, err
|
||||
}
|
||||
token, err := s.sessions.Create(ctx, principalFor(account))
|
||||
token, err := s.sessions.CreateWithMeta(ctx, principalFor(account), meta.IP, meta.UserAgent)
|
||||
if err != nil {
|
||||
return LoginResult{}, err
|
||||
}
|
||||
@@ -198,9 +216,42 @@ func (s *Service) CompleteTOTPLogin(ctx context.Context, kind Kind, tempToken, c
|
||||
return LoginResult{}, err
|
||||
}
|
||||
_ = s.sessions.DeleteToken(ctx, tempToken)
|
||||
if kind == KindPortal {
|
||||
s.NotifyLogin(ctx, account.ID, meta)
|
||||
}
|
||||
return LoginResult{Token: token, Account: account}, nil
|
||||
}
|
||||
|
||||
// NotifyLogin 发布"新设备登录"事件(尽力而为,失败不影响登录)。站内信是否
|
||||
// 落盘由通知 worker 按用户的安全偏好决定。
|
||||
func (s *Service) NotifyLogin(ctx context.Context, portalUserID string, meta SessionMeta) {
|
||||
if s.repository == nil || portalUserID == "" {
|
||||
return
|
||||
}
|
||||
payload, _ := json.Marshal(map[string]any{"portal_user_id": portalUserID, "ip": meta.IP, "user_agent": meta.UserAgent})
|
||||
_ = s.repository.InsertOutboxEvent(ctx, "security.login_detected", "identity", portalUserID, payload)
|
||||
}
|
||||
|
||||
// ListSessions 返回账号的有效会话(我的登录设备)。
|
||||
func (s *Service) ListSessions(ctx context.Context, kind Kind, subjectID, authorization string) ([]SessionView, error) {
|
||||
return s.sessions.ListSessions(ctx, kind, subjectID, authorization)
|
||||
}
|
||||
|
||||
// RevokeSession 吊销指定会话(当前会话除外)。
|
||||
func (s *Service) RevokeSession(ctx context.Context, kind Kind, subjectID, sessionID, authorization string) error {
|
||||
return s.sessions.RevokeSession(ctx, kind, subjectID, sessionID, authorization)
|
||||
}
|
||||
|
||||
// SecurityPrefs 返回门户账号的安全偏好(登录通知开关,默认开启)。
|
||||
func (s *Service) SecurityPrefs(ctx context.Context, portalUserID string) (bool, error) {
|
||||
return s.repository.SecurityPrefs(ctx, portalUserID)
|
||||
}
|
||||
|
||||
// SetSecurityPrefs 更新门户账号的安全偏好。
|
||||
func (s *Service) SetSecurityPrefs(ctx context.Context, portalUserID string, loginNotify bool) error {
|
||||
return s.repository.SetSecurityPrefs(ctx, portalUserID, loginNotify)
|
||||
}
|
||||
|
||||
func (s *Service) SetupTOTP(ctx context.Context, account Account, password string) (TOTPSetupResult, error) {
|
||||
if account.TOTPEnabled {
|
||||
return TOTPSetupResult{}, ErrTOTPAlreadyEnabled
|
||||
|
||||
Reference in New Issue
Block a user