前面幾集,我們把文章列表、內頁都搞定了,網站看起來像模像樣了。但是!你的後台 /admin 任何人都可以進去,這就像你家大門沒鎖一樣危險!想像一下,有人不小心點到 /admin/posts/new,就可以開始亂寫文章,那還得了!
所以,這一集我們要來幫部落格裝上 「電子門鎖」:
1. Session 登入:網站要能記得你是誰,不用每次都輸入帳密。
2. 後台保護:沒登入的人,通通給我滾去登入頁!
3. 草稿保護:只有登入的作者自己,才能預覽還沒發佈的秘密草稿!
🔍 變更清單:今天動了哪些檔案?
我們就像變形金剛一樣,這次要加入幾個新零件,才能讓「登入系統」順利運作!
新增:migrations/2025xxxxxx_add_password_hash_to_users.sql
新增:internal/http/handlers/admin_auth.go
新增:internal/http/handlers/mw_auth.go
新增:web/templates/pages/admin_login.html
新增:cmd/tools/hashpass/main.go
修改:cmd/server/main.go
(啟用 Session/CSRF、註冊路由)
修改:internal/http/handlers/front_posts.go
(草稿僅登入可見)
在 cmd/server/main.go 裡,我們要啟用兩個超重要的功能:Session 和 CSRF 防護。
A. Session:網站的「記憶力」🧠
Session(會話)就像你上網時,網站發給你的**「VIP 通行證」**。網站收到你的帳密驗證你身份後,就會給你這張證,之後你逛網站,網站只要看到這張證就知道:「喔,你是某某某,不用再登入一次了!」
// cmd/server/main.go 核心設定區塊
e.Use(session.Middleware(sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY"))))) // 啟用 Session 記憶功能!密鑰(SESSION_KEY)記得要藏好喔!
B. CSRF:防止壞蛋盜用你的身份🛡️
CSRF(跨站請求偽造)簡單來說,就是防止壞蛋在你不知情的情況下,偷偷用你的身份去點擊後台的「刪除文章」按鈕。
我們啟用 CSRF 後,所有的表單都會加上一個**「防偽碼」**(csrf token),沒有這個碼,指令一律作廢!
// cmd/server/main.go 核心設定區塊
e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
TokenLookup: "header:X-CSRF-Token,form:csrf", // 從表單或 Header 檢查防偽碼
CookieName: "csrf",
CookieSameSite: http.SameSiteLaxMode, // 比較安全的 Cookie 設定
}))
C. 路由:區分「開放區」和「禁區」🚧
在 cmd/server/main.go,我們設定了後台專用的「禁區」:/admin。
// cmd/server/main.go 路由區塊
// 認證:這三個網址是開放的,給任何人登入/登出用
auth := handlers.NewAdminAuthHandler(usersRepo)
e.GET("/admin/login", auth.GetLogin) // 顯示登入頁
e.POST("/admin/login", auth.PostLogin) // 處理登入
e.POST("/admin/logout", auth.PostLogout) // 處理登出
// 🚨 後台保護區!🚨
// e.Group("/admin", handlers.RequireLogin) -> 這裡告訴 Echo:所有 /admin/* 的網址,都要先經過 handlers.RequireLogin 這個「登入檢查員」!
admin := e.Group("/admin", handlers.RequireLogin)
admin.GET("/posts", postsHandler.AdminIndex) // 列出所有文章
admin.GET("/posts/new", postsHandler.AdminNew) // 建立新文章
//... 略,所有後台操作都放在這裡,享受保護!
在 internal/http/handlers/mw_auth.go 裡,我們寫了一個超盡責的登入檢查員 RequireLogin。
這個函式會檢查你的 Session 裡有沒有「使用者 ID」(uid)。如果沒有,代表你沒登入,就立刻把你踢回 /admin/login 登入頁!
// internal/http/handlers/mw_auth.go
// 這是中介軟體!像門衛一樣,在處理網址前先檢查一次。
func RequireLogin(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
sess, _ := session.Get("auth", c) // 檢查你的 VIP 通行證(Session)
// ❌ 如果通行證不存在或沒有 uid (使用者 ID),就是沒登入!
if sess == nil || sess.Values["uid"] == nil {
return c.Redirect(http.StatusFound, "/admin/login") // 踢回登入頁!
}
// ✅ 檢查通過,放你進去,執行下一個流程
return next(c)
}
}
💡 另一個 CurrentUserID(c) 函式,則是讓你隨時可以知道:「現在是誰登入著?」這對我們接下來保護草稿很有用!
在上一集,我們已經讓沒發佈的文章顯示 404 頁。現在,我們可以在顯示文章內頁的 Handler 裡,加入「是否登入」的判斷:
// internal/http/handlers/front_posts.go(在 PostPage 裡)
// ... 取得 post 後
if post.Status != "published" { // 這是草稿嗎?
if CurrentUserID(c) == 0 { // 檢查:沒登入?
return echo.NewHTTPError(http.StatusNotFound) // 沒登入就 404!
}
// 💡 嘿嘿,如果有登入,文章就算不是 'published',程式也會繼續往下走,讓他看見草稿!
}
這是核心的 internal/http/handlers/admin_auth.go,負責接收你輸入的帳密,檢查對不對,並設定你的 Session 通行證!
// internal/http/handlers/admin_auth.go(節錄 PostLogin)
func (h *AdminAuthHandler) PostLogin(c echo.Context) error {
// 1. 抓到使用者輸入的 Email 和 Password
var f loginForm
// ... 略
u, err := h.Users.FindByEmail(c, f.Email) // 2. 用 Email 去資料庫找使用者
// 3. 檢查:找不到使用者,或者密碼比對不成功(用 bcrypt 加密比對才安全!)
if err != nil || bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(f.Password)) != nil {
return c.Render(http.StatusUnauthorized, "pages/admin_login.html", map[string]any{"error": "帳號或密碼錯誤"}) // 登入失敗!
}
// 4. 登入成功!設定 Session 通行證
sess, _ := session.Get("auth", c)
sess.Values["uid"] = u.ID // 記住使用者 ID
// ... 略
sess.Options = &sessions.Options{ // 設定通行證的有效期限與保護方式
Path: "/",
MaxAge: int((24 * time.Hour).Seconds()), // 通行證有效 24 小時
HttpOnly: true, // 只有程式才能動這張證,網頁前端 JS 無法存取,超安全!
SameSite: http.SameSiteLaxMode,
}
_ = sess.Save(c.Request(), c.Response()) // 存檔!把通行證發給你
return c.Redirect(http.StatusFound, "/admin/posts") // 帶你回後台首頁!
}
超級重要! 密碼絕對、絕對、絕對不能存純文字!要用 bcrypt 這種加密方式鎖起來,就算資料庫被偷,壞人也看不到密碼本尊!
我們用這個小工具來產生一串加密後的密碼,貼到你的資料庫裡:
// cmd/tools/hashpass/main.go
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
func main() {
pw := "admin123" // ⬅️ **把這裡改成你要的初始密碼!**
hash, _ := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
fmt.Println(string(hash)) // 輸出密碼的加密字串
}
操作步驟:
把 cmd/tools/hashpass/main.go 存起來。
執行 go run cmd/tools/hashpass/main.go。
你會得到一串超長的字,例如 $2a$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx。
把這串字貼到你的 users 表格中,你作者帳號的 password_hash 欄位裡!
📋 驗收清單:後台鎖起來沒?
檢查一下,你的部落格是不是已經安全升級了?
未登入訪問 /admin/* → 會被擋住,強制轉跳到 /admin/login。
登入成功 → 網站會記得你,直接帶你回 /admin/posts。
登出 → Session 通行證被收回,下次又要重登了。
草稿:未登入的人點文章 → 404 找不到!已登入的人點文章 → 看得到草稿!
安全防護:後台表單有 CSRF 防偽碼;Session Cookie 設定了 HttpOnly(前端 JS 無法偷看),超級安全!
🎉 恭喜你! 你的 Go + Echo 部落格已經有基礎的後台保護了!現在你可以放心地在後台寫稿,不用擔心被人偷看了!