安全修复: - CRITICAL: SSO重定向URL注入漏洞 - 修复redirect_uri白名单验证 - HIGH: SSO ClientSecret未验证 - 使用crypto/subtle.ConstantTimeCompare验证 - HIGH: 邮件验证码熵值过低(3字节) - 提升到6字节(48位熵) - HIGH: 短信验证码熵值过低(4字节) - 提升到6字节 - HIGH: Goroutine使用已取消上下文 - auth_email.go使用独立context+超时 - HIGH: SQL LIKE查询注入风险 - permission/role仓库使用escapeLikePattern 新功能: - Go SDK: sdk/go/user-management/ 完整SDK实现 - CAS SSO框架: internal/auth/cas.go CAS协议支持 其他: - L1Cache实例问题修复 - AuthMiddleware共享l1Cache - 设备指纹XSS防护 - 内存存储替代localStorage - 响应格式协议中间件 - 导出无界查询修复
211 lines
5.6 KiB
Go
211 lines
5.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/user-management-system/internal/auth"
|
|
"github.com/user-management-system/internal/domain"
|
|
)
|
|
|
|
func (s *AuthService) SetEmailActivationService(svc *EmailActivationService) {
|
|
s.emailActivationSvc = svc
|
|
}
|
|
|
|
func (s *AuthService) SetEmailCodeService(svc *EmailCodeService) {
|
|
s.emailCodeSvc = svc
|
|
}
|
|
|
|
// HasEmailCodeService 判断邮箱验证码登录服务是否已配置
|
|
func (s *AuthService) HasEmailCodeService() bool {
|
|
return s != nil && s.emailCodeSvc != nil
|
|
}
|
|
|
|
func (s *AuthService) RegisterWithActivation(ctx context.Context, req *RegisterRequest) (*UserInfo, error) {
|
|
if err := s.validatePassword(req.Password); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.verifyPhoneRegistration(ctx, req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
exists, err := s.userRepo.ExistsByUsername(ctx, req.Username)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if exists {
|
|
return nil, errors.New("username already exists")
|
|
}
|
|
|
|
if req.Email != "" {
|
|
exists, err = s.userRepo.ExistsByEmail(ctx, req.Email)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if exists {
|
|
return nil, errors.New("email already exists")
|
|
}
|
|
}
|
|
|
|
if req.Phone != "" {
|
|
exists, err = s.userRepo.ExistsByPhone(ctx, req.Phone)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if exists {
|
|
return nil, errors.New("phone already exists")
|
|
}
|
|
}
|
|
|
|
hashedPassword, err := auth.HashPassword(req.Password)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
initialStatus := domain.UserStatusActive
|
|
if s.emailActivationSvc != nil && req.Email != "" {
|
|
initialStatus = domain.UserStatusInactive
|
|
}
|
|
|
|
user := &domain.User{
|
|
Username: req.Username,
|
|
Email: domain.StrPtr(req.Email),
|
|
Phone: domain.StrPtr(req.Phone),
|
|
Password: hashedPassword,
|
|
Nickname: req.Nickname,
|
|
Status: initialStatus,
|
|
}
|
|
if err := s.userRepo.Create(ctx, user); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s.bestEffortAssignDefaultRoles(ctx, user.ID, "register_with_activation")
|
|
|
|
if s.emailActivationSvc != nil && req.Email != "" {
|
|
nickname := req.Nickname
|
|
if nickname == "" {
|
|
nickname = req.Username
|
|
}
|
|
// 使用独立上下文避免请求结束后被取消
|
|
go func() {
|
|
bgCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
if err := s.emailActivationSvc.SendActivationEmail(bgCtx, user.ID, req.Email, nickname); err != nil {
|
|
log.Printf("auth: send activation email failed, user_id=%d email=%s err=%v", user.ID, req.Email, err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
userInfo := s.buildUserInfo(user)
|
|
s.publishEvent(ctx, domain.EventUserRegistered, userInfo)
|
|
return userInfo, nil
|
|
}
|
|
|
|
func (s *AuthService) ActivateEmail(ctx context.Context, token string) error {
|
|
if s.emailActivationSvc == nil {
|
|
return errors.New("email activation service is not configured")
|
|
}
|
|
|
|
userID, err := s.emailActivationSvc.ValidateActivationToken(ctx, token)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
user, err := s.userRepo.GetByID(ctx, userID)
|
|
if err != nil {
|
|
return fmt.Errorf("user not found: %w", err)
|
|
}
|
|
|
|
if user.Status == domain.UserStatusActive {
|
|
return errors.New("account already activated")
|
|
}
|
|
if user.Status != domain.UserStatusInactive {
|
|
return errors.New("account status does not allow activation")
|
|
}
|
|
|
|
return s.userRepo.UpdateStatus(ctx, userID, domain.UserStatusActive)
|
|
}
|
|
|
|
func (s *AuthService) ResendActivationEmail(ctx context.Context, email string) error {
|
|
if s.emailActivationSvc == nil {
|
|
return errors.New("email activation service is not configured")
|
|
}
|
|
|
|
user, err := s.userRepo.GetByEmail(ctx, email)
|
|
if err != nil {
|
|
if isUserNotFoundError(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
if user.Status == domain.UserStatusActive {
|
|
return nil
|
|
}
|
|
if user.Status != domain.UserStatusInactive {
|
|
return errors.New("account status does not allow activation")
|
|
}
|
|
|
|
nickname := user.Nickname
|
|
if nickname == "" {
|
|
nickname = user.Username
|
|
}
|
|
return s.emailActivationSvc.SendActivationEmail(ctx, user.ID, email, nickname)
|
|
}
|
|
|
|
func (s *AuthService) SendEmailLoginCode(ctx context.Context, email string) error {
|
|
if s.emailCodeSvc == nil {
|
|
return errors.New("email code service is not configured")
|
|
}
|
|
|
|
_, err := s.userRepo.GetByEmail(ctx, email)
|
|
if err != nil {
|
|
if isUserNotFoundError(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
return s.emailCodeSvc.SendEmailCode(ctx, email, "login")
|
|
}
|
|
|
|
func (s *AuthService) LoginByEmailCode(ctx context.Context, email, code, ip string) (*LoginResponse, error) {
|
|
if s.emailCodeSvc == nil {
|
|
return nil, errors.New("email code login is disabled")
|
|
}
|
|
|
|
if err := s.emailCodeSvc.VerifyEmailCode(ctx, email, "login", code); err != nil {
|
|
s.writeLoginLog(ctx, nil, domain.LoginTypeEmailCode, ip, false, err.Error())
|
|
return nil, err
|
|
}
|
|
|
|
user, err := s.userRepo.GetByEmail(ctx, email)
|
|
if err != nil {
|
|
if isUserNotFoundError(err) {
|
|
s.writeLoginLog(ctx, nil, domain.LoginTypeEmailCode, ip, false, "email not registered")
|
|
return nil, errors.New("email not registered")
|
|
}
|
|
s.writeLoginLog(ctx, nil, domain.LoginTypeEmailCode, ip, false, err.Error())
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.ensureUserActive(user); err != nil {
|
|
s.writeLoginLog(ctx, &user.ID, domain.LoginTypeEmailCode, ip, false, err.Error())
|
|
s.recordLoginAnomaly(ctx, &user.ID, ip, "", "", false)
|
|
return nil, err
|
|
}
|
|
|
|
s.bestEffortUpdateLastLogin(ctx, user.ID, ip, "email_code")
|
|
s.writeLoginLog(ctx, &user.ID, domain.LoginTypeEmailCode, ip, true, "")
|
|
s.recordLoginAnomaly(ctx, &user.ID, ip, "", "", true)
|
|
s.publishEvent(ctx, domain.EventUserLogin, map[string]interface{}{
|
|
"user_id": user.ID,
|
|
"username": user.Username,
|
|
"ip": ip,
|
|
"method": "email_code",
|
|
})
|
|
|
|
return s.generateLoginResponseWithoutRemember(ctx, user)
|
|
}
|