fix: align admin behavior and Kratos integrations
This commit is contained in:
@@ -2,11 +2,12 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/internal/conf"
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
@@ -18,23 +19,22 @@ import (
|
||||
)
|
||||
|
||||
type Public struct {
|
||||
system *service.SystemService
|
||||
settings *service.SettingsService
|
||||
audit *service.AuditService
|
||||
scheduler *worker.TaskScheduler
|
||||
store *captchaStore
|
||||
keyLong int
|
||||
imageWidth int
|
||||
imageHeight int
|
||||
system *service.SystemService
|
||||
settings *service.SettingsService
|
||||
audit *service.AuditService
|
||||
scheduler *worker.TaskScheduler
|
||||
runtime *conf.Runtime
|
||||
store *captchaStore
|
||||
}
|
||||
|
||||
func NewPublic(config *conf.AdminBackend, system *service.SystemService, settings *service.SettingsService, audit *service.AuditService, scheduler *worker.TaskScheduler) *Public {
|
||||
expiration := 3 * time.Minute
|
||||
func NewPublic(runtime *conf.Runtime, system *service.SystemService, settings *service.SettingsService, audit *service.AuditService, scheduler *worker.TaskScheduler) *Public {
|
||||
return &Public{system: system, settings: settings, audit: audit, scheduler: scheduler, runtime: runtime, store: &captchaStore{service: system, runtime: runtime}}
|
||||
}
|
||||
|
||||
func (h *Public) captchaConfig() (int, int, int) {
|
||||
keyLong, width, height := 6, 240, 80
|
||||
config := h.runtime.Admin()
|
||||
if config != nil && config.Captcha != nil {
|
||||
if config.Captcha.StoreExpiration != nil {
|
||||
expiration = config.Captcha.StoreExpiration.AsDuration()
|
||||
}
|
||||
if config.Captcha.KeyLong > 0 {
|
||||
keyLong = int(config.Captcha.KeyLong)
|
||||
}
|
||||
@@ -45,23 +45,18 @@ func NewPublic(config *conf.AdminBackend, system *service.SystemService, setting
|
||||
height = int(config.Captcha.ImgHeight)
|
||||
}
|
||||
}
|
||||
return &Public{system: system, settings: settings, audit: audit, scheduler: scheduler, store: &captchaStore{service: system, expiration: expiration}, keyLong: keyLong, imageWidth: width, imageHeight: height}
|
||||
return keyLong, width, height
|
||||
}
|
||||
|
||||
func (h *Public) Captcha(c *gin.Context) {
|
||||
keyLong, width, height := h.keyLong, h.imageWidth, h.imageHeight
|
||||
keyLong, width, height := h.captchaConfig()
|
||||
security, _ := h.settings.CurrentSecurity(c.Request.Context())
|
||||
openCaptcha := true
|
||||
if security != nil {
|
||||
keyLong, width, height = security.KeyLong, security.ImgWidth, security.ImgHeight
|
||||
if security.CaptchaOpen > 0 {
|
||||
failures, _ := cachedInt(c.Request.Context(), h.system, "login:fail:"+c.ClientIP())
|
||||
openCaptcha = failures >= security.CaptchaOpen
|
||||
}
|
||||
}
|
||||
if !openCaptcha {
|
||||
httpx.Write(c, httpx.CodeSuccess, gin.H{"captchaId": "", "picPath": "", "captchaLength": keyLong, "openCaptcha": false}, "验证码获取成功")
|
||||
return
|
||||
ttl := time.Duration(security.CaptchaTimeout) * time.Second
|
||||
failures, _ := ensureLoginIPCounter(c.Request.Context(), h.system, c.ClientIP(), ttl)
|
||||
openCaptcha = security.CaptchaOpen == 0 || failures > security.CaptchaOpen
|
||||
}
|
||||
driver := base64Captcha.NewDriverDigit(height, width, keyLong, 0.7, 80)
|
||||
id, picture, _, err := base64Captcha.NewCaptcha(driver, h.store).Generate()
|
||||
@@ -81,42 +76,71 @@ func (h *Public) Login(c *gin.Context) {
|
||||
security, _ := h.settings.CurrentSecurity(c.Request.Context())
|
||||
if security != nil && security.LockEnable {
|
||||
if _, locked, _ := h.system.CacheGet(c.Request.Context(), "login:lock:"+req.Username); locked {
|
||||
httpx.Fail(c, "账户已锁定,请稍后重试")
|
||||
httpx.Fail(c, "账号已锁定,请 "+strconv.Itoa(security.LockDuration)+" 分钟后再试")
|
||||
_ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "账号已锁定", Agent: c.Request.UserAgent()})
|
||||
return
|
||||
}
|
||||
}
|
||||
requireCaptcha := security == nil || security.CaptchaOpen == 0
|
||||
if security != nil && security.CaptchaOpen > 0 {
|
||||
failures, _ := cachedInt(c.Request.Context(), h.system, "login:fail:"+c.ClientIP())
|
||||
requireCaptcha = failures >= security.CaptchaOpen
|
||||
ipTTL := time.Hour
|
||||
if security != nil {
|
||||
if security.CaptchaTimeout > 0 {
|
||||
ipTTL = time.Duration(security.CaptchaTimeout) * time.Second
|
||||
}
|
||||
failures, _ := ensureLoginIPCounter(c.Request.Context(), h.system, c.ClientIP(), ipTTL)
|
||||
requireCaptcha = security.CaptchaOpen == 0 || failures > security.CaptchaOpen
|
||||
}
|
||||
if requireCaptcha && (req.CaptchaID == "" || req.Captcha == "" || !h.store.Verify(req.CaptchaID, req.Captcha, true)) {
|
||||
_, _ = h.system.CacheIncrement(c.Request.Context(), c.ClientIP(), ipTTL)
|
||||
_ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "验证码错误", Agent: c.Request.UserAgent()})
|
||||
httpx.Fail(c, "验证码错误")
|
||||
return
|
||||
}
|
||||
result, err := h.system.Login(c.Request.Context(), req.Username, req.Password)
|
||||
if err != nil {
|
||||
_ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "用户名不存在或者密码错误", Agent: c.Request.UserAgent()})
|
||||
ttl := time.Hour
|
||||
if security != nil && security.CaptchaTimeout > 0 {
|
||||
ttl = time.Duration(security.CaptchaTimeout) * time.Second
|
||||
_, _ = h.system.CacheIncrement(c.Request.Context(), c.ClientIP(), ipTTL)
|
||||
if errors.Is(err, biz.ErrUserDisabled) {
|
||||
_ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "用户被禁止登录", Agent: c.Request.UserAgent()})
|
||||
httpx.Fail(c, "用户被禁止登录")
|
||||
return
|
||||
}
|
||||
failures, _ := incrementCached(c.Request.Context(), h.system, "login:fail:"+c.ClientIP(), ttl)
|
||||
if security != nil && security.LockEnable && failures >= security.LockThreshold {
|
||||
_ = h.system.CacheSet(c.Request.Context(), "login:lock:"+req.Username, "1", time.Duration(security.LockDuration)*time.Minute)
|
||||
_ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "用户名不存在或者密码错误", Agent: c.Request.UserAgent()})
|
||||
if security != nil && security.LockEnable {
|
||||
lockTTL := time.Duration(security.LockDuration) * time.Minute
|
||||
failures, _ := h.system.CacheIncrement(c.Request.Context(), "login:fail:"+req.Username, lockTTL)
|
||||
if int(failures) >= security.LockThreshold {
|
||||
_ = h.system.CacheSet(c.Request.Context(), "login:lock:"+req.Username, "1", lockTTL)
|
||||
}
|
||||
}
|
||||
httpx.Fail(c, "用户名不存在或者密码错误")
|
||||
return
|
||||
}
|
||||
userID, _ := result.User["ID"].(uint)
|
||||
_ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: true, Agent: c.Request.UserAgent(), UserID: userID})
|
||||
_ = h.system.CacheDelete(c.Request.Context(), "login:fail:"+c.ClientIP())
|
||||
_ = h.system.CacheDelete(c.Request.Context(), "login:fail:"+req.Username)
|
||||
_ = h.system.CacheDelete(c.Request.Context(), "login:lock:"+req.Username)
|
||||
maxAge := int(time.Until(time.UnixMilli(result.ExpiresAt)).Seconds())
|
||||
http.SetCookie(c.Writer, &http.Cookie{Name: "x-token", Value: result.Token, Path: "/", MaxAge: maxAge, HttpOnly: true, SameSite: http.SameSiteStrictMode})
|
||||
httpx.SetTokenCookie(c, result.Token, maxAge)
|
||||
httpx.Write(c, httpx.CodeSuccess, result, "登录成功")
|
||||
}
|
||||
|
||||
func ensureLoginIPCounter(ctx context.Context, system *service.SystemService, ip string, expiration time.Duration) (int, error) {
|
||||
value, exists, err := system.CacheGet(ctx, ip)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if exists {
|
||||
return strconv.Atoi(value)
|
||||
}
|
||||
if expiration <= 0 {
|
||||
expiration = time.Hour
|
||||
}
|
||||
if err = system.CacheSet(ctx, ip, "1", expiration); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func (h *Public) CheckDatabase(c *gin.Context) {
|
||||
initialized, err := h.system.IsInitialized(c.Request.Context())
|
||||
if err != nil {
|
||||
@@ -163,27 +187,19 @@ func (h *Public) InitializeDatabase(engine *gin.Engine) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func cachedInt(ctx context.Context, system *service.SystemService, key string) (int, error) {
|
||||
value, ok, err := system.CacheGet(ctx, key)
|
||||
if err != nil || !ok {
|
||||
return 0, err
|
||||
}
|
||||
return strconv.Atoi(value)
|
||||
}
|
||||
|
||||
func incrementCached(ctx context.Context, system *service.SystemService, key string, ttl time.Duration) (int, error) {
|
||||
value, err := system.CacheIncrement(ctx, key, ttl)
|
||||
return int(value), err
|
||||
}
|
||||
|
||||
type captchaStore struct {
|
||||
service *service.SystemService
|
||||
expiration time.Duration
|
||||
service *service.SystemService
|
||||
runtime *conf.Runtime
|
||||
}
|
||||
|
||||
func (s *captchaStore) key(id string) string { return "captcha:" + id }
|
||||
func (s *captchaStore) Set(id, value string) error {
|
||||
return s.service.CacheSet(context.Background(), s.key(id), value, s.expiration)
|
||||
expiration := 3 * time.Minute
|
||||
config := s.runtime.Admin()
|
||||
if config != nil && config.Captcha != nil && config.Captcha.StoreExpiration != nil {
|
||||
expiration = config.Captcha.StoreExpiration.AsDuration()
|
||||
}
|
||||
return s.service.CacheSet(context.Background(), s.key(id), value, expiration)
|
||||
}
|
||||
func (s *captchaStore) Get(id string, clear bool) string {
|
||||
value, ok, err := s.service.CacheGet(context.Background(), s.key(id))
|
||||
|
||||
Reference in New Issue
Block a user