package auth import ( "context" "strings" "golang.org/x/crypto/bcrypt" "hyapp/pkg/appcode" "hyapp/pkg/xerr" authdomain "hyapp/services/user-service/internal/domain/auth" userdomain "hyapp/services/user-service/internal/domain/user" ) // LoginPassword 使用当前 display_user_id 和用户后续设置的密码创建新 session。 // 用户不存在、未设置密码和密码错误统一返回 AUTH_FAILED,避免暴露可枚举身份状态。 func (s *Service) LoginPassword(ctx context.Context, displayUserID string, password string, deviceID string, meta Meta) (authdomain.Token, error) { meta.AppCode = appcode.Normalize(meta.AppCode) displayUserID = strings.TrimSpace(displayUserID) if displayUserID == "" || password == "" || strings.TrimSpace(deviceID) == "" { // device_id 是 refresh token 后续轮换绑定条件,登录时必须提供。 return authdomain.Token{}, xerr.New(xerr.InvalidArgument, "display_user_id, password and device_id are required") } if s.authRepository == nil { // 没有认证 repository 时不能读取密码身份或创建 session。 return authdomain.Token{}, xerr.New(xerr.Unavailable, "auth repository is not configured") } if !userdomain.ValidResolvableDisplayUserID(displayUserID) { // 密码登录接受当前 active 展示号,也接受被 active 靓号覆盖的 held 默认短号;非法字符和不存在账号统一映射 AUTH_FAILED,避免账号枚举。 s.audit(ctx, meta, 0, loginPassword, "", resultFailed, string(xerr.AuthFailed)) return authdomain.Token{}, authFailed() } nowMs := s.now().UnixMilli() // repository 会按输入展示号定位用户,再返回该用户当前 active 展示号;held 默认短号只作为登录别名,不会覆盖 token 展示号。 accountRecord, err := s.authRepository.FindPasswordByDisplayUserID(ctx, displayUserID, nowMs) if err != nil { s.audit(ctx, meta, 0, loginPassword, "", resultFailed, string(xerr.AuthFailed)) return authdomain.Token{}, authFailed() } if err := verifyPassword(accountRecord.PasswordHash, password); err != nil { // 密码错误不暴露为独立 reason,和未设置密码保持同一认证失败语义。 s.audit(ctx, meta, accountRecord.UserID, loginPassword, "", resultFailed, string(xerr.AuthFailed)) return authdomain.Token{}, authFailed() } // 重新读取用户快照,保证禁用状态和过期靓号恢复都在签发前生效。 user, err := s.freshUser(ctx, accountRecord.UserID, nowMs, meta.RequestID) if err != nil { s.audit(ctx, meta, accountRecord.UserID, loginPassword, "", resultFailed, string(xerr.CodeOf(err))) return authdomain.Token{}, err } if !user.CanLogin() { // disabled/banned 用户不能通过已有密码继续拿新 token。 s.audit(ctx, meta, user.UserID, loginPassword, "", resultFailed, string(xerr.UserDisabled)) return authdomain.Token{}, xerr.New(xerr.UserDisabled, "user is disabled") } // 每次密码登录都会创建新的 refresh session。 session, refreshToken, err := s.newSession(meta.AppCode, user.UserID, deviceID) if err != nil { return authdomain.Token{}, err } if err := s.authRepository.CreateSession(ctx, session); err != nil { // session 写入失败时不能返回 token,避免客户端持有没有服务端状态的 refresh token。 s.audit(ctx, meta, user.UserID, loginPassword, "", resultFailed, string(xerr.CodeOf(err))) return authdomain.Token{}, err } s.audit(ctx, meta, user.UserID, loginPassword, "", resultSuccess, "") token, err := s.issueToken(user, session.SessionID, refreshToken) if err != nil { return authdomain.Token{}, err } s.enqueueLoginIPRiskJob(ctx, meta, token, "account", meta.Platform, meta.Language, meta.Timezone) return token, nil } // SetPassword 为已经三方登录创建出的用户首次设置密码。 // 该入口必须由 gateway 先校验 access token 后传入 user_id,不能再接受客户端自报用户。 func (s *Service) SetPassword(ctx context.Context, userID int64, password string, meta Meta) error { if userID <= 0 || password == "" { // user_id 必须来自 gateway 的 access token 校验结果,不能接受客户端自报身份。 return xerr.New(xerr.InvalidArgument, "user_id and password are required") } if s.authRepository == nil { return xerr.New(xerr.Unavailable, "auth repository is not configured") } user, err := s.freshUser(ctx, userID, s.now().UnixMilli(), meta.RequestID) if err != nil { s.audit(ctx, meta, userID, loginSetPass, "", resultFailed, string(xerr.CodeOf(err))) return err } if !user.CanLogin() { // 禁用用户不能新增密码身份。 s.audit(ctx, meta, userID, loginSetPass, "", resultFailed, string(xerr.UserDisabled)) return xerr.New(xerr.UserDisabled, "user is disabled") } // 密码进入持久层前必须先 hash。 passwordHash, err := hashPassword(password) if err != nil { return err } nowMs := s.now().UnixMilli() // repository 负责保证一个用户只能首次设置一次密码。 err = s.authRepository.SetPassword(ctx, authdomain.PasswordAccount{ AppCode: appcode.Normalize(meta.AppCode), UserID: user.UserID, DisplayUserID: user.CurrentDisplayUserID, PasswordHash: passwordHash, HashAlg: hashAlgBcrypt, CreatedAtMs: nowMs, UpdatedAtMs: nowMs, }) if err != nil { s.audit(ctx, meta, userID, loginSetPass, "", resultFailed, string(xerr.CodeOf(err))) return err } s.audit(ctx, meta, userID, loginSetPass, "", resultSuccess, "") return nil } func hashPassword(password string) (string, error) { // bcrypt 默认 cost 作为 v1 密码 hash 策略,后续可通过 HashAlg 升级。 body, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return "", err } return string(body), nil } func verifyPassword(passwordHash string, password string) error { // bcrypt 比对失败统一映射 AUTH_FAILED。 if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(password)); err != nil { return authFailed() } return nil }