Go Web后臺(tái)管理系統(tǒng)項(xiàng)目實(shí)現(xiàn)
一、背景介紹
這是一個(gè)基于 Go 語(yǔ)言開(kāi)發(fā)的 Web 后臺(tái)管理系統(tǒng),為筆者學(xué)習(xí)期間練手之作,較為粗糙
二、技術(shù)架構(gòu)
后端
- 語(yǔ)言 :采用 Go 語(yǔ)言(Golang)編寫,因其簡(jiǎn)潔高效、并發(fā)能力強(qiáng),且性能卓越,特別適合構(gòu)建高并發(fā)的 Web 服務(wù)。
- HTTP 路由 :使用 Gorilla Mux 庫(kù),它支持靈活的路由定義、中間件集成,方便構(gòu)建復(fù)雜的 API 和 Web 頁(yè)面路由。
- 數(shù)據(jù)庫(kù) :選用 MySQL 數(shù)據(jù)庫(kù),通過(guò)
github.com/go-sql-driver/mysql驅(qū)動(dòng)實(shí)現(xiàn)與 Go 應(yīng)用的交互,負(fù)責(zé)存儲(chǔ)用戶數(shù)據(jù)、會(huì)話信息等關(guān)鍵數(shù)據(jù)。
前端
筆者對(duì)前端知識(shí)不熟悉,使用AI生成相關(guān)代碼
三、代碼結(jié)構(gòu)與功能模塊
項(xiàng)目的代碼結(jié)構(gòu)清晰合理,按照功能模塊劃分為多個(gè)包,下面對(duì)主要包及其功能進(jìn)行介紹:
config 包
package config
import (
"database/sql"
"os"
"github.com/gorilla/sessions"
)
var (
// SessionStore 會(huì)話存儲(chǔ)
SessionStore = sessions.NewCookieStore([]byte("這是一個(gè)固定的密鑰,請(qǐng)?jiān)谏a(chǎn)環(huán)境中替換為更安全的值"))
// DB 數(shù)據(jù)庫(kù)連接
DB *sql.DB
)
// GetEnvOrDefault 獲取環(huán)境變量,如果不存在則使用默認(rèn)值
func GetEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
config 包主要負(fù)責(zé)存儲(chǔ)一些全局配置和資源,如會(huì)話存儲(chǔ)和數(shù)據(jù)庫(kù)連接。它定義了一個(gè)全局的會(huì)話存儲(chǔ) SessionStore,用于管理用戶會(huì)話。GetEnvOrDefault 函數(shù)用于獲取環(huán)境變量的值,如果變量不存在則返回默認(rèn)值,方便在不同環(huán)境下配置應(yīng)用。
db 包
package db
import (
"database/sql"
"fmt"
"log"
"time"
"GoWeb1/config"
"GoWeb1/models"
_ "github.com/go-sql-driver/mysql"
)
// InitDB 初始化數(shù)據(jù)庫(kù)
func InitDB() error {
// 連接數(shù)據(jù)庫(kù)
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
config.GetEnvOrDefault("DB_USER", "root"),
config.GetEnvOrDefault("DB_PASSWORD", "123456"),
config.GetEnvOrDefault("DB_HOST", "localhost"),
config.GetEnvOrDefault("DB_PORT", "3306"),
config.GetEnvOrDefault("DB_NAME", "goweb"),
)
var err error
config.DB, err = sql.Open("mysql", dsn)
if err != nil {
return fmt.Errorf("連接數(shù)據(jù)庫(kù)失敗: %v", err)
}
// 測(cè)試連接
if err = config.DB.Ping(); err != nil {
return fmt.Errorf("數(shù)據(jù)庫(kù)連接測(cè)試失敗: %v", err)
}
// 創(chuàng)建用戶表
_, err = config.DB.Exec(`
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL DEFAULT 'user',
login_attempts INT NOT NULL DEFAULT 0,
last_attempt DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
avatar VARCHAR(255) DEFAULT 'default.png',
status INT NOT NULL DEFAULT 1
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`)
if err != nil {
return fmt.Errorf("創(chuàng)建用戶表失敗: %v", err)
}
// 檢查是否存在默認(rèn)管理員用戶
var count int
err = config.DB.QueryRow("SELECT COUNT(*) FROM users WHERE username = 'admin'").Scan(&count)
if err != nil {
return fmt.Errorf("查詢管理員用戶數(shù)量失敗: %v", err)
}
// 如果沒(méi)有名為 admin 的用戶,才創(chuàng)建默認(rèn)管理員
if count == 0 {
adminHash, _ := utils.HashPassword("123456")
// 創(chuàng)建管理員用戶
_, err = config.DB.Exec(
"INSERT INTO users (username, password_hash, role, status) VALUES (?, ?, ?, ?)",
"admin", adminHash, "admin", 1,
)
if err != nil {
return fmt.Errorf("創(chuàng)建管理員用戶失敗: %v", err)
}
log.Println("已創(chuàng)建默認(rèn)管理員用戶 admin,密碼為 123456")
} else {
log.Println("默認(rèn)管理員用戶 admin 已存在,無(wú)需創(chuàng)建")
}
// 添加 avatar 字段到用戶表(如果不存在)
_, err = config.DB.Exec(`
ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar VARCHAR(255) DEFAULT 'default.png'
`)
if err != nil {
log.Printf("添加 avatar 字段警告: %v", err)
}
// 創(chuàng)建會(huì)話表
_, err = config.DB.Exec(`
CREATE TABLE IF NOT EXISTS sessions (
id VARCHAR(100) PRIMARY KEY,
username VARCHAR(50),
role VARCHAR(20),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`)
if err != nil {
log.Printf("創(chuàng)建會(huì)話表失敗: %v", err)
}
// 創(chuàng)建密碼重置表
_, err = config.DB.Exec(`
CREATE TABLE IF NOT EXISTS password_resets (
username VARCHAR(50) PRIMARY KEY,
token VARCHAR(100),
expiry DATETIME
)
`)
if err != nil {
log.Printf("創(chuàng)建密碼重置表失敗: %v", err)
}
log.Println("數(shù)據(jù)庫(kù)初始化成功")
return nil
}
// CreateAccessLogTable 創(chuàng)建訪問(wèn)記錄表
func CreateAccessLogTable() error {
_, err := config.DB.Exec("CREATE TABLE IF NOT EXISTS access_log (id INT AUTO_INCREMENT PRIMARY KEY, access_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
return err
}
// GetUserByUsername 根據(jù)用戶名獲取用戶信息
func GetUserByUsername(username string) (models.User, error) {
var user models.User
err := config.DB.QueryRow(
"SELECT id, username, password_hash, role, login_attempts, IFNULL(last_attempt, NOW()), created_at, updated_at, IFNULL(avatar, 'default.png'), status FROM users WHERE username = ?",
username,
).Scan(&user.ID, &user.Username, &user.PasswordHash, &user.Role, &user.LoginAttempts, &user.LastAttempt, &user.CreatedAt, &user.UpdatedAt, &user.Avatar, &user.Status)
return user, err
}
// UpdateUserLoginAttempts 更新用戶登錄嘗試信息
func UpdateUserLoginAttempts(userID int, attempts int, lastAttempt time.Time) error {
_, err := config.DB.Exec(
"UPDATE users SET login_attempts = ?, last_attempt = ? WHERE id = ?",
attempts, lastAttempt, userID,
)
return err
}
// IsUsernameExists 檢查用戶名是否存在
func IsUsernameExists(username string) bool {
var count int
config.DB.QueryRow("SELECT COUNT(*) FROM users WHERE username = ?", username).Scan(&count)
return count > 0
}
// CreateUser 創(chuàng)建新用戶
func CreateUser(username, passwordHash string, role string, status int) error {
_, err := config.DB.Exec(
"INSERT INTO users (username, password_hash, role, status) VALUES (?, ?, ?, ?)",
username, passwordHash, role, status,
)
return err
}
// CountRegisteredUsers 統(tǒng)計(jì)注冊(cè)用戶數(shù)量
func CountRegisteredUsers() (int, error) {
var count int
err := config.DB.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
return count, err
}
// CountAccessTrends 統(tǒng)計(jì)訪問(wèn)趨勢(shì)
func CountAccessTrends(timeRange string) (int, error) {
var query string
var startDateStr string
now := time.Now()
switch timeRange {
case "7天":
startDateStr = now.AddDate(0, 0, -7).Format("2006-01-02")
query = "SELECT COUNT(*) FROM access_log WHERE access_time >= ?"
case "30天":
startDateStr = now.AddDate(0, 0, -30).Format("2006-01-02")
query = "SELECT COUNT(*) FROM access_log WHERE access_time >= ?"
default:
query = "SELECT COUNT(*) FROM access_log"
// 不需要參數(shù),計(jì)算所有訪問(wèn)量
var count int
err := config.DB.QueryRow(query).Scan(&count)
return count, err
}
var count int
err := config.DB.QueryRow(query, startDateStr).Scan(&count)
return count, err
}
// GetAccessTrendData 獲取按日期分組的訪問(wèn)趨勢(shì)數(shù)據(jù)
func GetAccessTrendData(days int) ([]models.AccessTrendData, error) {
// 計(jì)算開(kāi)始日期,不依賴CURDATE()
endDate := time.Now()
startDate := endDate.AddDate(0, 0, -days)
// 格式化日期為MySQL日期格式
startDateStr := startDate.Format("2006-01-02")
query := `
SELECT
DATE(access_time) as date,
COUNT(*) as count
FROM
access_log
WHERE
access_time >= ?
GROUP BY
DATE(access_time)
ORDER BY
date ASC
`
rows, err := config.DB.Query(query, startDateStr)
if err != nil {
return nil, err
}
defer rows.Close()
var result []models.AccessTrendData
for rows.Next() {
var data models.AccessTrendData
if err := rows.Scan(&data.Date, &data.Count); err != nil {
return nil, err
}
result = append(result, data)
}
// 如果沒(méi)有數(shù)據(jù),填充空數(shù)據(jù)
if len(result) == 0 {
result = make([]models.AccessTrendData, days)
for i := 0; i < days; i++ {
date := time.Now().AddDate(0, 0, -days+i+1)
result[i] = models.AccessTrendData{
Date: date.Format("2006-01-02"),
Count: 0,
}
}
return result, nil
}
// 填充缺失的日期
filled := make([]models.AccessTrendData, 0)
currentDate := startDate.AddDate(0, 0, 1)
dataMap := make(map[string]int)
for _, data := range result {
dataMap[data.Date] = data.Count
}
for !currentDate.After(endDate) {
dateStr := currentDate.Format("2006-01-02")
count, exists := dataMap[dateStr]
if !exists {
count = 0
}
filled = append(filled, models.AccessTrendData{
Date: dateStr,
Count: count,
})
currentDate = currentDate.AddDate(0, 0, 1)
}
return filled, nil
}
db 包負(fù)責(zé)與數(shù)據(jù)庫(kù)進(jìn)行交互,完成各種數(shù)據(jù)的增刪改查操作。它提供了從初始化數(shù)據(jù)庫(kù)連接、創(chuàng)建必要表結(jié)構(gòu),到用戶認(rèn)證、數(shù)據(jù)統(tǒng)計(jì)等一系列功能。
InitDB 函數(shù)是數(shù)據(jù)庫(kù)模塊的核心入口,它根據(jù)環(huán)境變量配置連接到 MySQL 數(shù)據(jù)庫(kù),并創(chuàng)建必要的表結(jié)構(gòu),包括用戶表、會(huì)話表和密碼重置表。它還檢查是否存在默認(rèn)的管理員用戶(admin),如果不存在則創(chuàng)建,并為其設(shè)置默認(rèn)密碼。
GetUserByUsername 函數(shù)根據(jù)用戶名查詢用戶信息,返回一個(gè)包含用戶詳細(xì)信息的 models.User 結(jié)構(gòu)體。
UpdateUserLoginAttempts 用于更新用戶的登錄嘗試次數(shù)和最后登錄時(shí)間。
IsUsernameExists 檢查指定的用戶名是否已被注冊(cè)。
CreateUser 向數(shù)據(jù)庫(kù)中插入新的用戶記錄。
CountRegisteredUsers 和 CountAccessTrends 分別用于統(tǒng)計(jì)注冊(cè)用戶數(shù)量和訪問(wèn)趨勢(shì)數(shù)據(jù)。
GetAccessTrendData 獲取按日期分組的訪問(wèn)趨勢(shì)數(shù)據(jù),用于在前端繪制訪問(wèn)統(tǒng)計(jì)圖表。
handlers 包
package handlers
import (
"encoding/json"
"fmt"
"html/template"
"io"
"log"
"net/http"
"os"
"path/filepath"
"time"
"GoWeb1/config"
"GoWeb1/db"
"GoWeb1/utils"
"github.com/gorilla/sessions"
)
// LoginHandler 登錄處理函數(shù)
func LoginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
data := map[string]interface{}{}
if r.URL.Query().Get("registered") == "1" {
data["success"] = "注冊(cè)成功,請(qǐng)登錄"
}
if r.URL.Query().Get("reset") == "1" {
data["success"] = "密碼重置成功,請(qǐng)使用新密碼登錄"
}
tmpl := template.Must(template.ParseFiles("templates/login.html"))
tmpl.Execute(w, data)
return
}
// POST 處理
username := r.FormValue("username")
password := r.FormValue("password")
log.Printf("嘗試登錄: 用戶名=%s", username)
// 查詢用戶
user, err := db.GetUserByUsername(username)
if err != nil {
log.Printf("查詢用戶失敗: %v", err)
http.Error(w, "用戶名或密碼錯(cuò)誤", http.StatusUnauthorized)
return
}
log.Printf("找到用戶: ID=%d, 用戶名=%s, 角色=%s", user.ID, user.Username, user.Role)
// 檢查密碼
if !utils.CheckPassword(password, user.PasswordHash) {
log.Printf("密碼驗(yàn)證失敗")
http.Error(w, "用戶名或密碼錯(cuò)誤", http.StatusUnauthorized)
return
}
log.Printf("密碼驗(yàn)證成功")
// 檢查用戶狀態(tài)是否被禁用
if user.Status == 0 {
log.Printf("用戶 %s 已被管理員禁用", username)
http.Error(w, "您的賬戶已被禁用,請(qǐng)聯(lián)系管理員", http.StatusForbidden)
return
}
// 更新用戶的登錄嘗試次數(shù)和最后登錄時(shí)間
err = db.UpdateUserLoginAttempts(user.ID, 0, time.Now())
if err != nil {
log.Printf("更新用戶登錄時(shí)間失敗: %v", err)
// 繼續(xù)處理,不中斷登錄流程
}
// 清除所有現(xiàn)有的Cookie
for _, cookie := range r.Cookies() {
newCookie := &http.Cookie{
Name: cookie.Name,
Value: "",
Path: "/",
MaxAge: -1,
}
http.SetCookie(w, newCookie)
}
// 刪除該用戶的所有舊會(huì)話記錄
_, err = config.DB.Exec("DELETE FROM sessions WHERE username = ?", username)
if err != nil {
log.Printf("刪除舊會(huì)話記錄失敗: %v", err)
// 繼續(xù)處理,不中斷登錄流程
}
// 創(chuàng)建一個(gè)新的會(huì)話ID
sessionID := utils.GenerateRandomString(32)
http.SetCookie(w, &http.Cookie{
Name: "user_session",
Value: sessionID,
Path: "/",
HttpOnly: true,
MaxAge: 86400, // 1天
})
// 插入新的會(huì)話記錄
_, err = config.DB.Exec("INSERT INTO sessions (id, username, role) VALUES (?, ?, ?)", sessionID, user.Username, user.Role)
if err != nil {
log.Printf("保存會(huì)話失敗: %v", err)
http.Error(w, "服務(wù)器內(nèi)部錯(cuò)誤", http.StatusInternalServerError)
return
}
log.Printf("會(huì)話已創(chuàng)建,重定向到首頁(yè)")
http.Redirect(w, r, "/", http.StatusSeeOther)
}
// RegisterHandler 注冊(cè)處理
func RegisterHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
tmpl := template.Must(template.ParseFiles("templates/register.html"))
tmpl.Execute(w, nil)
return
}
// POST 請(qǐng)求處理
username := r.FormValue("username")
password := r.FormValue("password")
.confirmPassword := r.FormValue("confirm_password")
// 表單驗(yàn)證
var errorMsg string
if !utils.IsValidUsername(username) {
errorMsg = "用戶名必須是4-20個(gè)字符,且只能包含字母、數(shù)字和下劃線"
} else if db.IsUsernameExists(username) {
errorMsg = "用戶名已被使用"
} else if !utils.IsValidPassword(password) {
errorMsg = "密碼至少需要6個(gè)字符"
} else if password != .confirmPassword {
errorMsg = "兩次輸入的密碼不一致"
}
if errorMsg != "" {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(errorMsg))
return
}
// 創(chuàng)建用戶
hashedPassword, err := utils.HashPassword(password)
if err != nil {
log.Printf("密碼加密失敗: %v", err)
http.Error(w, "注冊(cè)失敗,請(qǐng)稍后再試", http.StatusInternalServerError)
return
}
err = db.CreateUser(username, hashedPassword, "user", 1)
if err != nil {
log.Printf("創(chuàng)建用戶失敗: %v", err)
http.Error(w, "注冊(cè)失敗,請(qǐng)稍后再試", http.StatusInternalServerError)
return
}
log.Printf("用戶 %s 注冊(cè)成功", username)
http.Redirect(w, r, "/login?registered=1", http.StatusSeeOther)
}
// LogoutHandler 登出處理
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
// 從Cookie獲取會(huì)話ID
cookie, err := r.Cookie("user_session")
if err == nil {
// 刪除數(shù)據(jù)庫(kù)中的會(huì)話記錄
_, err := config.DB.Exec("DELETE FROM sessions WHERE id = ?", cookie.Value)
if err != nil {
log.Printf("刪除會(huì)話記錄失敗: %v", err)
}
// 清除Cookie
http.SetCookie(w, &http.Cookie{
Name: "user_session",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
})
}
// 清除所有其他可能的Cookie
for _, c := range r.Cookies() {
http.SetCookie(w, &http.Cookie{
Name: c.Name,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
})
}
// 設(shè)置響應(yīng)頭,防止緩存
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
// 重定向到登錄頁(yè)面
http.Redirect(w, r, "/login", http.StatusSeeOther)
}
// ClearCookieHandler 清除會(huì)話Cookie處理函數(shù)
func ClearCookieHandler(w http.ResponseWriter, r *http.Request) {
// 清除會(huì)話Cookie
cookie := &http.Cookie{
Name: "session",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
}
http.SetCookie(w, cookie)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(`
<html>
<head>
<title>會(huì)話已清除</title>
<meta http-equiv="refresh" content="2;url=/login">
<style>
body { font-family: Arial, sans-serif; text-align: center; margin-top: 100px; }
</style>
</head>
<body>
<h1>會(huì)話已清除</h1>
<p>正在跳轉(zhuǎn)到登錄頁(yè)面...</p>
</body>
</html>
`))
}
handlers 包是項(xiàng)目的請(qǐng)求處理中心,它定義了各種 HTTP 請(qǐng)求的處理函數(shù),實(shí)現(xiàn)了用戶與系統(tǒng)的交互邏輯。
LoginHandler 處理用戶的登錄請(qǐng)求。對(duì)于 GET 請(qǐng)求,它渲染登錄頁(yè)面;對(duì)于 POST 請(qǐng)求,它驗(yàn)證用戶輸入的用戶名和密碼,查詢數(shù)據(jù)庫(kù)中的用戶信息,檢查密碼是否匹配以及用戶狀態(tài)是否正常。如果驗(yàn)證通過(guò),則為用戶創(chuàng)建一個(gè)新的會(huì)話 ID,存儲(chǔ)到數(shù)據(jù)庫(kù),并將其作為 Cookie 發(fā)送給客戶端,最后重定向用戶到首頁(yè)。
RegisterHandler 處理用戶的注冊(cè)請(qǐng)求。它對(duì)用戶輸入的注冊(cè)信息進(jìn)行驗(yàn)證,包括用戶名格式、密碼強(qiáng)度以及兩次輸入密碼是否一致。驗(yàn)證通過(guò)后,對(duì)密碼進(jìn)行加密,并將新用戶信息插入到數(shù)據(jù)庫(kù)中,注冊(cè)成功后重定向用戶到登錄頁(yè)面。
LogoutHandler 實(shí)現(xiàn)用戶登出功能。它通過(guò)獲取用戶 Cookie 中的會(huì)話 ID,從數(shù)據(jù)庫(kù)中刪除對(duì)應(yīng)的會(huì)話記錄,并清除客戶端的會(huì)話 Cookie,確保用戶安全退出系統(tǒng)。
ClearCookieHandler 用于清除會(huì)話 Cookie,它重置會(huì)話相關(guān)的 Cookie,并重定向用戶到登錄頁(yè)面。
這些處理函數(shù)共同協(xié)作,實(shí)現(xiàn)了用戶認(rèn)證與會(huì)話管理的核心功能,確保用戶能夠安全地登錄、注冊(cè)和退出系統(tǒng)。
middleware 包
package middleware
import (
"context"
"log"
"net/http"
"GoWeb1/config"
)
// AuthMiddleware 身份驗(yàn)證中間件
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 從Cookie獲取會(huì)話ID
cookie, err := r.Cookie("user_session")
if err != nil {
log.Printf("獲取會(huì)話Cookie失敗: %v", err)
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// 從數(shù)據(jù)庫(kù)中驗(yàn)證會(huì)話
var username, role string
err = config.DB.QueryRow("SELECT username, role FROM sessions WHERE id = ?", cookie.Value).Scan(&username, &role)
if err != nil {
log.Printf("查詢會(huì)話記錄失敗: %v", err)
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// 檢查用戶狀態(tài)是否被禁用
var status int
err = config.DB.QueryRow("SELECT status FROM users WHERE username = ?", username).Scan(&status)
if err != nil || status == 0 {
// 如果用戶被禁用,刪除會(huì)話并重定向到登錄頁(yè)面
config.DB.Exec("DELETE FROM sessions WHERE id = ?", cookie.Value)
http.SetCookie(w, &http.Cookie{
Name: "user_session",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
})
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// 會(huì)話有效,設(shè)置上下文并調(diào)用下一個(gè)處理函數(shù)
r = r.WithContext(context.WithValue(r.Context(), "username", username))
r = r.WithContext(context.WithValue(r.Context(), "role", role))
next(w, r)
}
}
// AdminMiddleware 管理員權(quán)限中間件
func AdminMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 從Cookie獲取會(huì)話ID
cookie, err := r.Cookie("user_session")
if err != nil {
log.Printf("獲取會(huì)話Cookie失敗: %v", err)
http.Error(w, "未授權(quán)", http.StatusUnauthorized)
return
}
// 從數(shù)據(jù)庫(kù)中獲取用戶名和角色
var username, role string
err = config.DB.QueryRow("SELECT username, role FROM sessions WHERE id = ?", cookie.Value).Scan(&username, &role)
if err != nil {
log.Printf("查詢會(huì)話記錄失敗: %v", err)
http.Error(w, "未授權(quán)", http.StatusUnauthorized)
return
}
// 檢查是否是管理員角色
if role != "admin" {
http.Error(w, "權(quán)限不足", http.StatusForbidden)
return
}
next(w, r)
}
}
middleware 包定義了兩個(gè)核心中間件:AuthMiddleware 和 AdminMiddleware。
AuthMiddleware 用于驗(yàn)證普通用戶的會(huì)話。它通過(guò)檢查請(qǐng)求中的會(huì)話 Cookie,查詢數(shù)據(jù)庫(kù)中的會(huì)話記錄來(lái)驗(yàn)證用戶是否已登錄。如果會(huì)話無(wú)效或用戶已被禁用,它會(huì)重定向用戶到登錄頁(yè)面。對(duì)于有效會(huì)話,它將用戶信息存儲(chǔ)到請(qǐng)求上下文中,供后續(xù)處理函數(shù)使用。
AdminMiddleware 則在 AuthMiddleware 的基礎(chǔ)上,進(jìn)一步驗(yàn)證用戶是否具有管理員權(quán)限。只有當(dāng)用戶角色為 admin 時(shí),才允許訪問(wèn)受保護(hù)的管理員路由。
這些中間件通過(guò)攔截請(qǐng)求,在請(qǐng)求到達(dá)處理函數(shù)之前驗(yàn)證用戶身份和權(quán)限,確保系統(tǒng)的安全性和功能的正確訪問(wèn)。
utils 包
package utils
import (
"crypto/rand"
"encoding/base64"
"regexp"
"golang.org/x/crypto/bcrypt"
)
// HashPassword 密碼加密函數(shù)
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
// CheckPassword 驗(yàn)證密碼
func CheckPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// GenerateRandomString 生成隨機(jī)字符串
func GenerateRandomString(length int) string {
b := make([]byte, length)
rand.Read(b)
return base64.URLEncoding.EncodeToString(b)[:length]
}
// IsValidUsername 驗(yàn)證用戶名格式
func IsValidUsername(username string) bool {
if len(username) < 4 || len(username) > 20 {
return false
}
// 只允許字母、數(shù)字和下劃線
re := regexp.MustCompile("^[a-zA-Z0-9_]+$")
return re.MatchString(username)
}
// IsValidPassword 驗(yàn)證密碼強(qiáng)度
func IsValidPassword(password string) bool {
return len(password) >= 6
}
utils 包提供了項(xiàng)目中重復(fù)使用的工具函數(shù)。
HashPassword 使用 bcrypt 算法對(duì)密碼進(jìn)行加密,生成安全的密碼哈希值。
CheckPassword 用于驗(yàn)證用戶輸入的明文密碼與數(shù)據(jù)庫(kù)中存儲(chǔ)的密碼哈希值是否匹配。
GenerateRandomString 生成指定長(zhǎng)度的隨機(jī)字符串,用于會(huì)話 ID、密碼重置令牌等場(chǎng)景。
IsValidUsername 和 IsValidPassword 分別用于驗(yàn)證用戶名和密碼是否符合規(guī)定的格式和強(qiáng)度要求。
四、總結(jié)
這個(gè)基于 Go 語(yǔ)言的 Web 后臺(tái)管理系統(tǒng)項(xiàng)目尚有許多bug和不足之處,如有指教將不勝感激
到此這篇關(guān)于Go Web后臺(tái)管理系統(tǒng)項(xiàng)目實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Go Web后臺(tái)管理系統(tǒng)項(xiàng)目?jī)?nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
小學(xué)生也能看懂的Golang異常處理recover panic
在其他語(yǔ)言里,宕機(jī)往往以異常的形式存在,底層拋出異常,上層邏輯通過(guò) try/catch 機(jī)制捕獲異常,沒(méi)有被捕獲的嚴(yán)重異常會(huì)導(dǎo)致宕機(jī),go語(yǔ)言追求簡(jiǎn)潔,優(yōu)雅,Go語(yǔ)言不支持傳統(tǒng)的 try…catch…finally 這種異常2021-09-09
解決Go中使用seed得到相同隨機(jī)數(shù)的問(wèn)題
這篇文章主要介紹了Go中使用seed得到相同隨機(jī)數(shù)的問(wèn)題,需要的朋友可以參考下2019-10-10
詳解Golang中文件系統(tǒng)事件監(jiān)聽(tīng)
文件系統(tǒng)事件是指文件系統(tǒng)相關(guān)的各種操作和狀態(tài)變化,當(dāng)一個(gè)應(yīng)用層的進(jìn)程操作文件或目錄時(shí),會(huì)觸發(fā)system call,內(nèi)核的notification子系統(tǒng)可以守在那里,把該進(jìn)程對(duì)文件的操作上報(bào)給應(yīng)用層的監(jiān)聽(tīng)進(jìn)程,這篇文章主要介紹了Golang之文件系統(tǒng)事件監(jiān)聽(tīng),需要的朋友可以參考下2024-01-01
解決Golang time.Parse和time.Format的時(shí)區(qū)問(wèn)題
這篇文章主要介紹了解決Golang time.Parse和time.Format的時(shí)區(qū)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-04-04
Golang分布式注冊(cè)中心實(shí)現(xiàn)流程講解
這篇文章主要介紹了Golang分布式注冊(cè)中心實(shí)現(xiàn)流程,注冊(cè)中心可以用于服務(wù)發(fā)現(xiàn),服務(wù)注冊(cè),配置管理等方面,在分布式系統(tǒng)中,服務(wù)的發(fā)現(xiàn)和注冊(cè)是非常重要的組成部分,需要的朋友可以參考下2023-05-05
關(guān)于Golang獲取當(dāng)前項(xiàng)目絕對(duì)路徑的問(wèn)題
這篇文章主要介紹了Golang獲取當(dāng)前項(xiàng)目絕對(duì)路徑的問(wèn)題,通常的做法是go run用于本地開(kāi)發(fā),用一個(gè)命令中快速測(cè)試代碼確實(shí)非常方便;在部署生產(chǎn)環(huán)境時(shí),我們會(huì)通過(guò)go build構(gòu)建出二進(jìn)制文件然后上傳到服務(wù)器再去執(zhí)行,那么會(huì)產(chǎn)生什么問(wèn)題呢?感興趣的朋友一起看看吧2022-04-04
Go語(yǔ)言基礎(chǔ)go build命令用法及示例詳解
這篇文章主要為大家介紹了Go語(yǔ)言基礎(chǔ)go build命令用法及示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2021-11-11

