From 7feebbabc07d92a925ab44231ba1c6a217f99b50 Mon Sep 17 00:00:00 2001 From: skyle1995 Date: Fri, 24 Oct 2025 03:08:43 +0800 Subject: [PATCH] Add a login verification code Fix the site icon --- controllers/admin/auth.go | 11 ++ controllers/admin/captcha.go | 153 +++++++++++++++++++++++++++ database/seed_settings.go | 6 +- go.mod | 3 + go.sum | 66 ++++++++++++ server/admin.go | 3 + server/routes.go | 9 ++ web/assets/favicon.svg | 11 ++ web/template/admin/layout.html | 3 + web/template/admin/login.html | 183 +++++++++++++++------------------ web/template/index.html | 6 +- 11 files changed, 348 insertions(+), 106 deletions(-) create mode 100644 controllers/admin/captcha.go create mode 100644 web/assets/favicon.svg diff --git a/controllers/admin/auth.go b/controllers/admin/auth.go index 23024af..bd44df7 100644 --- a/controllers/admin/auth.go +++ b/controllers/admin/auth.go @@ -46,6 +46,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { var body struct { Username string `json:"username"` Password string `json:"password"` + Captcha string `json:"captcha"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { utils.JsonResponse(w, http.StatusBadRequest, false, "请求参数错误", nil) @@ -55,6 +56,16 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { utils.JsonResponse(w, http.StatusBadRequest, false, "用户名和密码不能为空", nil) return } + if body.Captcha == "" { + utils.JsonResponse(w, http.StatusBadRequest, false, "验证码不能为空", nil) + return + } + + // 验证验证码 + if !VerifyCaptcha(r, body.Captcha) { + utils.JsonResponse(w, http.StatusBadRequest, false, "验证码错误", nil) + return + } db, err := database.GetDB() if err != nil { diff --git a/controllers/admin/captcha.go b/controllers/admin/captcha.go new file mode 100644 index 0000000..189554e --- /dev/null +++ b/controllers/admin/captcha.go @@ -0,0 +1,153 @@ +package admin + +import ( + "encoding/base64" + "encoding/json" + "math/rand" + "net/http" + "strings" + "time" + + "github.com/mojocn/base64Captcha" +) + +// 全局验证码存储器 +var store = base64Captcha.DefaultMemStore + +// CaptchaHandler 生成验证码图片 +// GET /admin/captcha - 返回验证码图片 +func CaptchaHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + // 随机生成4-6位长度 + // Go 1.20+ 无需手动设置随机种子,使用默认全局随机源即可 + captchaLength := 4 + rand.Intn(3) // 4-6位随机长度 + + // 配置验证码参数 - 使用字母数字混合 + driver := base64Captcha.DriverString{ + Height: 60, + Width: 200, + NoiseCount: 0, + ShowLineOptions: 2 | 4, + Length: captchaLength, + Source: "ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789", // 混合大小写字母和数字,去除易混淆字符 + Fonts: []string{"wqy-microhei.ttc"}, + } + + // 生成验证码 + captcha := base64Captcha.NewCaptcha(&driver, store) + id, b64s, _, err := captcha.Generate() + if err != nil { + http.Error(w, "生成验证码失败", http.StatusInternalServerError) + return + } + + // 将验证码ID存储到session中(这里简化处理,实际项目中应该使用更安全的方式) + // 设置cookie来存储验证码ID + cookie := &http.Cookie{ + Name: "captcha_id", + Value: id, + Path: "/", + HttpOnly: true, + Secure: false, // 生产环境应设置为true + MaxAge: 300, // 5分钟过期 + Expires: time.Now().Add(5 * time.Minute), + } + http.SetCookie(w, cookie) + + // 解码base64图片数据并返回 + w.Header().Set("Content-Type", "image/png") + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Expires", "0") + + // 直接返回base64编码的图片数据,让浏览器解析 + // 但是我们需要返回实际的图片数据,所以需要解码base64 + + // 去掉data:image/png;base64,前缀 + b64s = strings.TrimPrefix(b64s, "data:image/png;base64,") + + imgData, err := base64.StdEncoding.DecodeString(b64s) + if err != nil { + http.Error(w, "解码验证码图片失败", http.StatusInternalServerError) + return + } + + w.Write(imgData) +} + +// VerifyCaptcha 验证验证码 +// 这个函数将在登录处理中被调用 +// 支持大小写不敏感匹配 +func VerifyCaptcha(r *http.Request, captchaValue string) bool { + // 从cookie中获取验证码ID + cookie, err := r.Cookie("captcha_id") + if err != nil { + return false + } + + captchaId := cookie.Value + if captchaId == "" { + return false + } + + // 先尝试原始值验证 + if store.Verify(captchaId, captchaValue, false) { + // 验证成功后删除验证码 + store.Verify(captchaId, captchaValue, true) + return true + } + + // 如果原始值验证失败,尝试小写验证(因为显示的是大小写混合,但允许用户输入小写) + if store.Verify(captchaId, strings.ToLower(captchaValue), false) { + // 验证成功后删除验证码 + store.Verify(captchaId, strings.ToLower(captchaValue), true) + return true + } + + // 最后尝试大写验证 + if store.Verify(captchaId, strings.ToUpper(captchaValue), true) { + return true + } + + return false +} + +// CaptchaAPIHandler 验证码API接口(可选,用于AJAX验证) +// POST /admin/api/captcha/verify - 验证验证码 +func CaptchaAPIHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + var body struct { + Captcha string `json:"captcha"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "code": 1, + "msg": "请求参数错误", + }) + return + } + + isValid := VerifyCaptcha(r, body.Captcha) + + w.Header().Set("Content-Type", "application/json") + if isValid { + json.NewEncoder(w).Encode(map[string]interface{}{ + "code": 0, + "msg": "验证码正确", + }) + } else { + json.NewEncoder(w).Encode(map[string]interface{}{ + "code": 1, + "msg": "验证码错误", + }) + } +} diff --git a/database/seed_settings.go b/database/seed_settings.go index e5805f2..15834b7 100644 --- a/database/seed_settings.go +++ b/database/seed_settings.go @@ -24,17 +24,17 @@ func SeedDefaultSettings() error { }, { Name: "site_keywords", - Value: "验证,网络,管理系统,网络验证,账户管理", + Value: "验证,网络,管理系统,网络验证,卡密管理,账户管理", Description: "网站关键词,用于SEO优化,多个关键词用逗号分隔", }, { Name: "site_description", - Value: "专业的网络验证管理系统,提供便捷的在线网络验证服务和账户管理功能", + Value: "专业的网络验证管理系统,提供便捷的在线网络验证服务和设备管理功能", Description: "网站描述,用于SEO优化和社交媒体分享", }, { Name: "site_logo", - Value: "/assets/logo.png", + Value: "/favicon.ico", Description: "网站Logo图片路径", }, { diff --git a/go.mod b/go.mod index d29662f..143d94d 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/glebarez/sqlite v1.11.0 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/uuid v1.6.0 + github.com/mojocn/base64Captcha v1.3.8 github.com/redis/go-redis/v9 v9.13.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.9.1 @@ -24,6 +25,7 @@ require ( github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect @@ -38,6 +40,7 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect + golang.org/x/image v0.23.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.28.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 671a5d8..d397cd6 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= @@ -46,6 +48,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mojocn/base64Captcha v1.3.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg= +github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -81,18 +85,80 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= +golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/server/admin.go b/server/admin.go index abea1f4..a681d0c 100644 --- a/server/admin.go +++ b/server/admin.go @@ -32,6 +32,9 @@ func RegisterAdminRoutes(mux *http.ServeMux) { // 退出登录(无需拦截,幂等清理) mux.HandleFunc("/admin/logout", adminctl.LogoutHandler) + // 验证码生成路由(无需认证) + mux.HandleFunc("/admin/captcha", adminctl.CaptchaHandler) + // 后台布局页(需要管理员认证) mux.HandleFunc("/admin/layout", adminctl.AdminAuthRequired(adminctl.AdminLayoutHandler)) diff --git a/server/routes.go b/server/routes.go index 1e3ffed..ac74901 100644 --- a/server/routes.go +++ b/server/routes.go @@ -10,6 +10,7 @@ import ( // RegisterRoutes 聚合注册所有路由 func RegisterRoutes(mux *http.ServeMux) { registerStaticRoutes(mux) + registerFaviconRoute(mux) RegisterHomeRoutes(mux) RegisterAdminRoutes(mux) @@ -35,3 +36,11 @@ func registerStaticRoutes(mux *http.ServeMux) { log.Printf("初始化静态资源文件系统失败: %v", err) } } + +// registerFaviconRoute 注册favicon路由 +func registerFaviconRoute(mux *http.ServeMux) { + // 将 /favicon.ico 重定向到 /assets/favicon.svg + mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/assets/favicon.svg", http.StatusMovedPermanently) + }) +} diff --git a/web/assets/favicon.svg b/web/assets/favicon.svg new file mode 100644 index 0000000..c634fd6 --- /dev/null +++ b/web/assets/favicon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/web/template/admin/layout.html b/web/template/admin/layout.html index ff33551..de743da 100644 --- a/web/template/admin/layout.html +++ b/web/template/admin/layout.html @@ -4,6 +4,9 @@ {{ .Title }} - {{ .SystemName }} + + + diff --git a/web/template/admin/login.html b/web/template/admin/login.html index b5f6a87..2ed66e2 100644 --- a/web/template/admin/login.html +++ b/web/template/admin/login.html @@ -5,7 +5,11 @@ {{ .Title }} - + + + + + -
- - - + +
- - - + - + +