Files
opsx-licence/backend/internal/service/auth/auth_service.go
T
Logan.Li a01b89f942
CI / backend (push) Waiting to run
feat(auth): 记住登录 —— 刷新令牌持久化,不再在本机保存明文密码
- 后端:新增 refresh_token 模型与服务,登录签发 refresh token,支持静默续期与登出撤销
  (backend/internal/model/refresh_token.go, backend/internal/service/auth/refresh.go,
   auth_controller/auth_service/dto/router/jwt 配套改动)
- 前端:登录页改为「记住登录」(只持久化引导令牌与用户名,不落盘密码),401 自动刷新并重试原请求
  (stores/user.js, api/request.js, api/auth.js, main.js, Login.vue 配套改动)
- 部署:已在容器中验证运行(镜像构建于 2026-11-03 00:50,服务 healthy)
- 安全:仓库内无任何 .env/证书/密钥文件变更
2026-11-03 01:12:22 +08:00

152 lines
4.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package auth
import (
"backend/config"
"backend/internal/dto"
"backend/internal/model"
"backend/utils"
"errors"
"fmt"
"time"
"gorm.io/gorm"
)
// AuthService 认证业务逻辑层 // Logan.Li
type AuthService struct {
db *gorm.DB
cfg *config.Config
}
// 登录防护参数:连续失败 5 次锁定 15 分钟(账号级,与 IP 限流互补) // Logan.Li
const (
maxLoginAttempts = 5
loginLockDuration = 15 * time.Minute
)
// NewAuthService 创建 service 实例 // Logan.Li
func NewAuthService(db *gorm.DB, cfg *config.Config) *AuthService {
return &AuthService{db: db, cfg: cfg}
}
// LoginResult 登录/刷新成功后的令牌与用户信息(供 controller 组装响应) // Logan.Li
type LoginResult struct {
AccessToken string
RefreshToken string
RefreshExpiresAt time.Time
User *model.User
}
// issueAccessToken 签发访问令牌(HS256,24h) // Logan.Li
func (s *AuthService) issueAccessToken(user *model.User) (string, error) {
token, err := utils.GenerateToken(user.ID, user.Username, user.Role, s.cfg.JWTSecret)
if err != nil {
return "", errors.New("令牌生成失败")
}
return token, nil
}
// Login 校验账号密码并签发 JWT。
// 含账号级防护:连续失败达上限后锁定一段时间;成功登录清零计数。 // Logan.Li
func (s *AuthService) Login(req dto.LoginRequest, ip, userAgent string) (*LoginResult, error) {
var user model.User
if err := s.db.Where("username = ?", req.Username).First(&user).Error; err != nil {
return nil, errors.New("用户名或密码错误")
}
if !user.IsActive {
return nil, errors.New("账号已被禁用")
}
// 账号锁定检查(到点自动解锁) // Logan.Li
if user.LockedUntil != nil && user.LockedUntil.After(time.Now()) {
remaining := int(time.Until(*user.LockedUntil).Minutes()) + 1
return nil, fmt.Errorf("失败次数过多,账号已锁定,请约 %d 分钟后再试", remaining)
}
if !utils.CheckPassword(req.Password, user.Password) {
s.registerLoginFailure(&user)
return nil, errors.New("用户名或密码错误")
}
// 登录成功:清零失败计数与锁定 // Logan.Li
if user.FailedLoginAttempts > 0 || user.LockedUntil != nil {
s.db.Model(&user).Updates(map[string]interface{}{
"failed_login_attempts": 0,
"locked_until": nil,
})
}
// 更新最后登录时间 // Logan.Li
now := time.Now()
s.db.Model(&user).Update("last_login", now)
// 签发访问令牌 + 刷新令牌(支撑「记住登录」,密码不落盘) // Logan.Li
accessToken, err := s.issueAccessToken(&user)
if err != nil {
return nil, err
}
refreshToken, refreshExpiresAt, err := s.IssueRefreshToken(user.ID, ip, userAgent)
if err != nil {
return nil, err
}
return &LoginResult{
AccessToken: accessToken,
RefreshToken: refreshToken,
RefreshExpiresAt: refreshExpiresAt,
User: &user,
}, nil
}
// registerLoginFailure 记录一次登录失败,达到上限则锁定账号 // Logan.Li
func (s *AuthService) registerLoginFailure(user *model.User) {
attempts := user.FailedLoginAttempts + 1
updates := map[string]interface{}{"failed_login_attempts": attempts}
if attempts >= maxLoginAttempts {
lockedUntil := time.Now().Add(loginLockDuration)
updates["locked_until"] = &lockedUntil
updates["failed_login_attempts"] = 0 // 锁定期结束后重新计数 // Logan.Li
}
s.db.Model(user).Updates(updates)
}
// GetCurrentUser 根据 ID 获取当前用户信息 // Logan.Li
func (s *AuthService) GetCurrentUser(userID uint) (*dto.UserInfoResponse, error) {
var user model.User
if err := s.db.First(&user, userID).Error; err != nil {
return nil, errors.New("用户不存在")
}
return &dto.UserInfoResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Avatar: user.Avatar,
Role: user.Role,
}, nil
}
// ChangePassword 修改当前用户密码;同时刷新会话失效基线,使所有旧令牌立即作废 // Logan.Li
func (s *AuthService) ChangePassword(userID uint, req dto.ChangePasswordRequest) error {
var user model.User
if err := s.db.First(&user, userID).Error; err != nil {
return errors.New("用户不存在")
}
if !utils.CheckPassword(req.OldPassword, user.Password) {
return errors.New("原密码错误")
}
user.Password = utils.HashPassword(req.NewPassword)
user.TokenValidAfter = time.Now() // 旧 JWT 全部失效,需重新登录 // Logan.Li
user.FailedLoginAttempts = 0
user.LockedUntil = nil
if err := s.db.Save(&user).Error; err != nil {
return err
}
// 刷新令牌一并撤销,否则「记住登录」会绕过改密失效基线 // Logan.Li
s.RevokeAllForUser(user.ID)
return nil
}