package app import ( "crypto/sha256" "crypto/subtle" "encoding/hex" "encoding/json" "errors" "io" "net" "net/http" "time" "golang.org/x/crypto/bcrypt" ) type loginLimit struct { Count int Until time.Time } type credentials struct { Password string `json:"password"` Token string `json:"token"` CurrentPassword string `json:"currentPassword"` } func decode(w http.ResponseWriter, r *http.Request, target any) error { r.Body = http.MaxBytesReader(w, r.Body, 16<<20) decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(target); err != nil { return errors.New("请求内容无效或过大") } var extra any if err := decoder.Decode(&extra); err != io.EOF { return errors.New("请求必须是单个 JSON 对象") } return nil } func jsonResponse(w http.ResponseWriter, status int, value any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(value) } func writeError(w http.ResponseWriter, status int, message string) { jsonResponse(w, status, map[string]string{"error": message}) } func hashToken(token string) string { b := sha256.Sum256([]byte(token)) return hex.EncodeToString(b[:]) } func validPassword(password string) bool { return len(password) >= 12 && len(password) <= 72 } func (a *App) session(r *http.Request) (string, string, error) { cookie, err := r.Cookie("cw_session") if err != nil { return "", "", err } hash := hashToken(cookie.Value) var csrf string var expires int64 err = a.store.db.QueryRow("SELECT csrf,expires FROM sessions WHERE token_hash=?", hash).Scan(&csrf, &expires) if err != nil || expires <= time.Now().Unix() { return "", "", errors.New("会话已过期") } return hash, csrf, nil } func (a *App) newSession(w http.ResponseWriter) error { token, csrf := randomToken(), randomToken() expires := time.Now().Add(24 * time.Hour) if _, err := a.store.db.Exec("INSERT INTO sessions(token_hash,csrf,expires) VALUES(?,?,?)", hashToken(token), csrf, expires.Unix()); err != nil { return err } http.SetCookie(w, &http.Cookie{Name: "cw_session", Value: token, Path: "/", HttpOnly: true, Secure: a.secureCookie, SameSite: http.SameSiteStrictMode, MaxAge: 86400, Expires: expires}) jsonResponse(w, 200, map[string]any{"authenticated": true, "csrf": csrf}) return nil } func (a *App) protect(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, csrf, err := a.session(r) if err != nil { writeError(w, 401, "请先登录") return } if r.Method != "GET" && r.Method != "HEAD" { if subtle.ConstantTimeCompare([]byte(r.Header.Get("X-CSRF-Token")), []byte(csrf)) != 1 { writeError(w, 403, "会话校验失败,请重新登录") return } } next.ServeHTTP(w, r) }) } func (a *App) authStatus(w http.ResponseWriter, r *http.Request) { password, err := a.store.Get("password") if err != nil { writeError(w, 500, "数据库读取失败") return } _, csrf, err := a.session(r) jsonResponse(w, 200, map[string]any{"initialized": password != "", "authenticated": err == nil, "csrf": csrf}) } func (a *App) setup(w http.ResponseWriter, r *http.Request) { a.authMu.Lock() defer a.authMu.Unlock() password, err := a.store.Get("password") if err != nil { writeError(w, 500, "数据库读取失败") return } if password != "" { writeError(w, 409, "管理员已经设置") return } var input credentials if err := decode(w, r, &input); err != nil { writeError(w, 400, err.Error()) return } if subtle.ConstantTimeCompare([]byte(input.Token), []byte(a.setupToken)) != 1 { writeError(w, 403, "首次设置令牌不正确,请查看服务启动日志") return } if !validPassword(input.Password) { writeError(w, 400, "密码需为 12–72 字节") return } hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost) if err != nil { writeError(w, 500, "密码处理失败") return } if err := a.store.Set("password", string(hash)); err != nil { writeError(w, 500, "保存失败") return } _ = a.store.Set("setup_token", "") a.setupToken = "" if err := a.newSession(w); err != nil { writeError(w, 500, "会话创建失败") } } func (a *App) login(w http.ResponseWriter, r *http.Request) { a.authMu.Lock() defer a.authMu.Unlock() ip, _, _ := net.SplitHostPort(r.RemoteAddr) limit := a.failures[ip] if limit.Count >= 5 && time.Now().Before(limit.Until) { writeError(w, 429, "登录尝试过多,请 5 分钟后重试") return } if time.Now().After(limit.Until) { limit = loginLimit{} } var input credentials if err := decode(w, r, &input); err != nil { writeError(w, 400, err.Error()) return } password, err := a.store.Get("password") if err != nil { writeError(w, 500, "读取失败") return } if password == "" || bcrypt.CompareHashAndPassword([]byte(password), []byte(input.Password)) != nil { if len(a.failures) > 1024 { a.failures = map[string]loginLimit{} } limit.Count++ limit.Until = time.Now().Add(5 * time.Minute) a.failures[ip] = limit writeError(w, 401, "密码不正确") return } delete(a.failures, ip) if err := a.newSession(w); err != nil { writeError(w, 500, "会话创建失败") } } func (a *App) logout(w http.ResponseWriter, r *http.Request) { hash, _, _ := a.session(r) _, err := a.store.db.Exec("DELETE FROM sessions WHERE token_hash=?", hash) if err != nil { writeError(w, 500, "退出失败") return } http.SetCookie(w, &http.Cookie{Name: "cw_session", Path: "/", HttpOnly: true, Secure: a.secureCookie, SameSite: http.SameSiteStrictMode, MaxAge: -1}) jsonResponse(w, 200, map[string]bool{"ok": true}) } func (a *App) password(w http.ResponseWriter, r *http.Request) { a.authMu.Lock() defer a.authMu.Unlock() var input credentials if err := decode(w, r, &input); err != nil { writeError(w, 400, err.Error()) return } old, err := a.store.Get("password") if err != nil || bcrypt.CompareHashAndPassword([]byte(old), []byte(input.CurrentPassword)) != nil { writeError(w, 400, "当前密码不正确") return } if !validPassword(input.Password) { writeError(w, 400, "新密码需为 12–72 字节") return } hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost) if err != nil { writeError(w, 500, "密码处理失败") return } tx, err := a.store.db.Begin() if err != nil { writeError(w, 500, "保存失败") return } defer tx.Rollback() if _, err = tx.Exec("UPDATE kv SET value=? WHERE key='password'", string(hash)); err == nil { _, err = tx.Exec("DELETE FROM sessions") } if err != nil || tx.Commit() != nil { writeError(w, 500, "保存失败") return } if err := a.newSession(w); err != nil { writeError(w, 500, "会话创建失败") } }