Files
Prism/internal/api/handlers_auth.go
T
rose_cat707 024c15a836
CI / test (push) Has been cancelled
CI / docker (push) Has been cancelled
feat: Prism HTTP/SOCKS5 代理网关及管理台
Go 控制面(SQLite + REST API)嵌入 Mihomo 数据面,Vue 3 多语言 Web 管理台。
含订阅/节点/规则/出站/监控/日志、OpenAPI、API Key、Gitea Actions CI 与开源文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 12:18:58 +08:00

117 lines
2.8 KiB
Go

package api
import (
"math/rand"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
"github.com/prism/proxy/internal/auth"
)
type loginReq struct {
Password string `json:"password"`
}
func (s *Server) authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
settings, err := s.app.Store.GetSettings(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
c.Abort()
return
}
if !s.authorizeRequest(c, settings) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
c.Abort()
return
}
c.Next()
}
}
func (s *Server) authStatus(c *gin.Context) {
settings, err := s.app.Store.GetSettings(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"enabled": settings["auth_enabled"] == "true"})
}
func (s *Server) login(c *gin.Context) {
var req loginReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx := c.Request.Context()
settings, err := s.app.Store.GetSettings(ctx)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ip := clientIP(c)
cfg := auth.ParseLoginConfig(settings)
check, err := s.loginLimiter.Check(ctx, ip, cfg)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if !check.Allowed {
writeLoginLocked(c, check.RetryAfter)
return
}
hash := settings["admin_password"]
if !checkPassword(req.Password, hash) {
time.Sleep(loginFailureDelay())
result, err := s.loginLimiter.RecordFailure(ctx, ip, cfg)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if result.Locked {
writeLoginLocked(c, result.RetryAfter)
return
}
c.JSON(http.StatusUnauthorized, gin.H{
"error": "invalid password",
"remaining": result.Remaining,
})
return
}
if err := s.loginLimiter.RecordSuccess(ctx, ip); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
token := issueToken(hash, 24*time.Hour)
c.JSON(http.StatusOK, gin.H{"token": token})
}
func writeLoginLocked(c *gin.Context, retryAfter time.Duration) {
secs := int(retryAfter.Seconds())
if secs < 1 {
secs = 1
}
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "too many login attempts",
"retry_after": secs,
})
}
func loginFailureDelay() time.Duration {
return 300*time.Millisecond + time.Duration(rand.Intn(200))*time.Millisecond
}
func checkPassword(password, stored string) bool {
if strings.HasPrefix(stored, "$2a$") {
return bcrypt.CompareHashAndPassword([]byte(stored), []byte(password)) == nil
}
return password == stored
}