139 lines
5.6 KiB
Go
139 lines
5.6 KiB
Go
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.ValidDisplayUserID(displayUserID) {
|
||
// 非法短号和不存在短号统一映射 AUTH_FAILED,避免账号枚举。
|
||
s.audit(ctx, meta, 0, loginPassword, "", resultFailed, string(xerr.AuthFailed))
|
||
return authdomain.Token{}, authFailed()
|
||
}
|
||
|
||
nowMs := s.now().UnixMilli()
|
||
// repository 会按当前 active display_user_id 查询,并在查询前处理靓号懒过期。
|
||
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, "")
|
||
return s.issueToken(user, session.SessionID, refreshToken)
|
||
}
|
||
|
||
// 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
|
||
}
|