diff --git a/auth/middleware.go b/auth/middleware.go new file mode 100644 index 0000000..ac5a7a2 --- /dev/null +++ b/auth/middleware.go @@ -0,0 +1,187 @@ +// auth/middleware.go +package auth + +import ( + "crypto/subtle" + "net/http" + "time" +) + +type AuthMiddleware struct { + username string + password string + sessions *SessionManager +} + +func NewAuthMiddleware(username, password string, sessions *SessionManager) *AuthMiddleware { + return &AuthMiddleware{ + username: username, + password: password, + sessions: sessions, + } +} + +func (a *AuthMiddleware) Login(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + a.serveLoginPage(w) + return + } + + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + username := r.FormValue("username") + password := r.FormValue("password") + + // 使用常量时间比较防止时序攻击 + usernameMatch := subtle.ConstantTimeCompare([]byte(username), []byte(a.username)) == 1 + passwordMatch := subtle.ConstantTimeCompare([]byte(password), []byte(a.password)) == 1 + + if usernameMatch && passwordMatch { + sessionID := a.sessions.Create(30 * time.Minute) + + http.SetCookie(w, &http.Cookie{ + Name: "session_id", + Value: sessionID, + Path: "/", + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteStrictMode, + MaxAge: 1800, + }) + + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + // 登录失败,延迟响应防止暴力破解 + time.Sleep(2 * time.Second) + http.Redirect(w, r, "/login?error=1", http.StatusSeeOther) +} + +func (a *AuthMiddleware) Logout(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("session_id") + if err == nil { + a.sessions.Delete(cookie.Value) + } + + http.SetCookie(w, &http.Cookie{ + Name: "session_id", + Value: "", + Path: "/", + HttpOnly: true, + Secure: true, + MaxAge: -1, + }) + + http.Redirect(w, r, "/login", http.StatusSeeOther) +} + +func (a *AuthMiddleware) Require(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("session_id") + if err != nil || !a.sessions.Valid(cookie.Value) { + http.Redirect(w, r, "/login", http.StatusFound) + return + } + next.ServeHTTP(w, r) + }) +} + +func (a *AuthMiddleware) serveLoginPage(w http.ResponseWriter) { + html := ` + +
+ +