mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-12 10:16:37 +08:00
feat(auth): implement decoupled sliding-window rate limiting for login and oauth
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"Wavelet/pkg/idgen"
|
||||
"Wavelet/pkg/logger"
|
||||
"Wavelet/pkg/response"
|
||||
"Wavelet/pkg/util"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -79,15 +81,16 @@ func invalidateTokenCache(ctx context.Context, tokenHash string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Login 用户密码登录
|
||||
// @Summary 用户密码登录
|
||||
// @Description 使用用户名和密码登录,登录成功后建立 Session。若管理员已关闭密码登录功能则返回错误。
|
||||
// Login 用户登录
|
||||
// @Summary 用户登录
|
||||
// @Description 使用用户名和密码登录系统,验证通过后建立 Session 并返回用户信息。
|
||||
// @Tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body user.loginRequest true "登录请求参数"
|
||||
// @Success 200 {object} response.Any "登录成功,返回用户信息"
|
||||
// @Failure 400 {object} response.Any "用户名或密码错误"
|
||||
// @Failure 429 {object} response.Any "登录尝试过于频繁"
|
||||
// @Failure 500 {object} response.Any "服务内部错误"
|
||||
// @Router /api/v1/user/login [post]
|
||||
func Login(c *gin.Context) {
|
||||
@@ -97,8 +100,28 @@ func Login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := GetUserByUsername(c.Request.Context(), req.Username)
|
||||
ctx := c.Request.Context()
|
||||
clientIP := c.ClientIP()
|
||||
rateKey := "auth:login:ip:" + clientIP
|
||||
if clientIP == "" {
|
||||
rateKey = "auth:login:user:" + req.Username
|
||||
}
|
||||
|
||||
limiter := getLimiter(ctx)
|
||||
if limiter != nil {
|
||||
res, err := limiter.Allow(ctx, rateKey, contracts.Rate{
|
||||
Limit: 5,
|
||||
Period: 1 * time.Minute,
|
||||
})
|
||||
if err == nil && !res.Allowed {
|
||||
response.AbortTooManyRequests(c, errTooManyLoginAttempts)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
user, err := GetUserByUsername(ctx, req.Username)
|
||||
if err != nil {
|
||||
util.DummyCheckPassword(req.Password)
|
||||
response.AbortUnauthorized(c, errPasswordMismatch)
|
||||
return
|
||||
}
|
||||
@@ -108,9 +131,13 @@ func Login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if limiter != nil {
|
||||
_ = limiter.Reset(ctx, rateKey)
|
||||
}
|
||||
|
||||
if user.ID == 0 {
|
||||
newID := idgen.NextUint64ID()
|
||||
if err := getDB(c.Request.Context()).Model(&User{}).Where("username = ?", user.Username).Update("id", newID).Error; err == nil {
|
||||
if err := getDB(ctx).Model(&User{}).Where("username = ?", user.Username).Update("id", newID).Error; err == nil {
|
||||
user.ID = newID
|
||||
}
|
||||
}
|
||||
@@ -122,7 +149,7 @@ func Login(c *gin.Context) {
|
||||
user.NeedChangePassword = needChange
|
||||
sess.Set("need_change_password", needChange)
|
||||
if err := sess.Save(); err != nil {
|
||||
logger.ErrorF(c.Request.Context(), "save session failed on login: %v", err)
|
||||
logger.ErrorF(ctx, "save session failed on login: %v", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response.OK(user))
|
||||
@@ -137,6 +164,7 @@ func Login(c *gin.Context) {
|
||||
// @Param request body user.registerRequest true "注册请求参数"
|
||||
// @Success 200 {object} response.Any "注册并登录成功,返回用户信息"
|
||||
// @Failure 400 {object} response.Any "参数错误、用户名已存在或注册已关闭"
|
||||
// @Failure 429 {object} response.Any "注册尝试过于频繁"
|
||||
// @Failure 500 {object} response.Any "服务内部错误"
|
||||
// @Router /api/v1/user/register [post]
|
||||
func Register(c *gin.Context) {
|
||||
@@ -146,6 +174,19 @@ func Register(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
clientIP := c.ClientIP()
|
||||
if limiter := getLimiter(ctx); limiter != nil && clientIP != "" {
|
||||
res, err := limiter.Allow(ctx, "auth:register:ip:"+clientIP, contracts.Rate{
|
||||
Limit: 10,
|
||||
Period: 1 * time.Minute,
|
||||
})
|
||||
if err == nil && !res.Allowed {
|
||||
response.AbortTooManyRequests(c, errTooManyLoginAttempts)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
newUser := &User{
|
||||
Username: req.Username,
|
||||
Email: req.Email,
|
||||
|
||||
Reference in New Issue
Block a user