mirror of
https://github.com/skyle1995/NetworkAuth.git
synced 2026-05-25 02:24:05 +08:00
Add a login verification code
Fix the site icon
This commit is contained in:
@@ -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 {
|
||||
|
||||
153
controllers/admin/captcha.go
Normal file
153
controllers/admin/captcha.go
Normal file
@@ -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": "验证码错误",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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图片路径",
|
||||
},
|
||||
{
|
||||
|
||||
3
go.mod
3
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
|
||||
|
||||
66
go.sum
66
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=
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
11
web/assets/favicon.svg
Normal file
11
web/assets/favicon.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#2563eb"/>
|
||||
<stop offset="100%" stop-color="#60a5fa"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="2" y="2" width="28" height="28" rx="5" fill="url(#g)"/>
|
||||
<path d="M16 7 L21.5 16 L16 25 L10.5 16 Z" fill="#fff" opacity="0.95"/>
|
||||
<circle cx="16" cy="16" r="2.5" fill="#2563eb"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 487 B |
@@ -4,6 +4,9 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>{{ .Title }} - {{ .SystemName }}</title>
|
||||
<!-- 站点图标 -->
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/static/css/admin.css" />
|
||||
<script type="module" src="./static/lib/include.js"></script>
|
||||
</head>
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>{{ .Title }}</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/layui@2.10.1/dist/css/layui.css">
|
||||
<!-- 站点图标 -->
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<!-- 请勿在项目正式环境中引用该 layui.css 地址 -->
|
||||
<link href="//unpkg.com/layui@2.12.1/dist/css/layui.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
@@ -17,10 +21,9 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.login-container {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 20px;
|
||||
.demo-login-container {
|
||||
width: 400px;
|
||||
margin: 21px auto 0;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
|
||||
@@ -43,51 +46,25 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
.login-form {
|
||||
padding: 40px 30px;
|
||||
padding: 30px 20px;
|
||||
}
|
||||
.layui-form-item {
|
||||
margin-bottom: 25px;
|
||||
|
||||
/* 调整表单项间距 */
|
||||
.login-form .layui-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.layui-input {
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 4px;
|
||||
padding: 12px 15px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.layui-input:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
.layui-btn-fluid {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
font-size: 16px;
|
||||
letter-spacing: 1px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.layui-btn-fluid:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
/* 修复登录按钮文字垂直位置偏下:使用flex进行垂直水平居中,并设置固定高度 */
|
||||
.login-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 44px;
|
||||
padding: 0 16px;
|
||||
line-height: normal; /* 避免与高度不一致导致的文字偏移 */
|
||||
font-weight: 500;
|
||||
}
|
||||
.error-msg {
|
||||
color: #ff5722;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
display: none;
|
||||
|
||||
/* 最后一个表单项不需要底部边距 */
|
||||
.login-form .layui-form-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.demo-login-other .layui-icon {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 0 2px;
|
||||
top: 2px;
|
||||
font-size: 26px;
|
||||
}
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
@@ -98,8 +75,9 @@
|
||||
|
||||
/* 响应式设计 - 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
.login-container {
|
||||
margin: 10px;
|
||||
.demo-login-container {
|
||||
width: 90%;
|
||||
margin: 10px auto;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.login-header {
|
||||
@@ -109,16 +87,19 @@
|
||||
font-size: 20px;
|
||||
}
|
||||
.login-form {
|
||||
padding: 30px 20px;
|
||||
padding: 25px 15px;
|
||||
}
|
||||
.layui-form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
/* 移动端表单项间距调整 */
|
||||
.login-form .layui-form-item {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-container {
|
||||
margin: 5px;
|
||||
.demo-login-container {
|
||||
width: 95%;
|
||||
margin: 5px auto;
|
||||
border-radius: 0;
|
||||
min-height: calc(100vh - 10px);
|
||||
display: flex;
|
||||
@@ -131,48 +112,35 @@
|
||||
font-size: 18px;
|
||||
}
|
||||
.login-form {
|
||||
padding: 25px 15px;
|
||||
padding: 20px 15px;
|
||||
}
|
||||
|
||||
/* 小屏幕表单项间距调整 */
|
||||
.login-form .layui-form-item {
|
||||
margin-bottom: 16px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.layui-input {
|
||||
padding: 15px;
|
||||
font-size: 16px; /* 防止iOS缩放 */
|
||||
}
|
||||
.login-btn {
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 超小屏幕适配 */
|
||||
@media (max-width: 320px) {
|
||||
.login-form {
|
||||
padding: 20px 10px;
|
||||
}
|
||||
.login-header {
|
||||
padding: 15px 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<h1>{{ .SystemName }}</h1>
|
||||
<p>管理员登录</p>
|
||||
</div>
|
||||
|
||||
<div class="login-form">
|
||||
<form class="layui-form" id="loginForm">
|
||||
<form class="layui-form">
|
||||
<div class="demo-login-container">
|
||||
<div class="login-header">
|
||||
<h1>{{ .SystemName }}</h1>
|
||||
<p>管理员登录</p>
|
||||
</div>
|
||||
|
||||
<div class="login-form">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-wrap">
|
||||
<div class="layui-input-prefix">
|
||||
<i class="layui-icon layui-icon-username"></i>
|
||||
</div>
|
||||
<input type="text" name="username" placeholder="请输入用户名" lay-verify="required" lay-reqtext="请输入用户名" class="layui-input" autocomplete="off">
|
||||
<input type="text" name="username" value="" lay-verify="required" placeholder="用户名" lay-reqtext="请填写用户名" autocomplete="off" class="layui-input" lay-affix="clear">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -181,35 +149,48 @@
|
||||
<div class="layui-input-prefix">
|
||||
<i class="layui-icon layui-icon-password"></i>
|
||||
</div>
|
||||
<input type="password" name="password" placeholder="请输入密码" lay-verify="required" lay-reqtext="请输入密码" class="layui-input" autocomplete="off">
|
||||
<input type="password" name="password" value="" lay-verify="required" placeholder="密 码" lay-reqtext="请填写密码" autocomplete="off" class="layui-input" lay-affix="eye">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<input type="checkbox" name="remember" title="记住登录状态" lay-skin="primary">
|
||||
<div class="layui-row">
|
||||
<div class="layui-col-xs7">
|
||||
<div class="layui-input-wrap">
|
||||
<div class="layui-input-prefix">
|
||||
<i class="layui-icon layui-icon-vercode"></i>
|
||||
</div>
|
||||
<input type="text" name="captcha" value="" lay-verify="required" placeholder="验证码" lay-reqtext="请填写验证码" autocomplete="off" class="layui-input" lay-affix="clear">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-xs5">
|
||||
<div style="margin-left: 5px; text-align: right;">
|
||||
<img id="captcha-img" src="/admin/captcha" onclick="this.src='/admin/captcha?t='+ new Date().getTime();" style="cursor: pointer; height: 38px; border-radius: 4px; width: 100%;" title="点击刷新验证码">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<button class="layui-btn layui-btn-fluid login-btn" lay-submit lay-filter="login">立即登录</button>
|
||||
<button class="layui-btn layui-btn-fluid" lay-submit lay-filter="demo-login">立即登录</button>
|
||||
</div>
|
||||
|
||||
<div class="error-msg" id="errorMsg"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="login-footer">
|
||||
<p>{{ .FooterText }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="login-footer">
|
||||
<p>{{ .FooterText }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script src="https://unpkg.com/layui@2.10.1/dist/layui.js"></script>
|
||||
<!-- 请勿在项目正式环境中引用该 layui.js 地址 -->
|
||||
<script src="//unpkg.com/layui@2.12.1/dist/layui.js"></script>
|
||||
<script>
|
||||
layui.use(['form', 'layer'], function(){
|
||||
layui.use(function(){
|
||||
var form = layui.form;
|
||||
var layer = layui.layer;
|
||||
|
||||
// 登录提交回调:向 /admin/login 发送请求,并依据 code===0 判断成功与否
|
||||
form.on('submit(login)', function(data){
|
||||
form.on('submit(demo-login)', function(data){
|
||||
var loadIndex = layer.load(1, {
|
||||
shade: [0.1, '#fff']
|
||||
});
|
||||
@@ -238,17 +219,19 @@
|
||||
});
|
||||
} else {
|
||||
const msg = (result && (result.msg || result.message)) || '登录失败,请检查用户名和密码';
|
||||
document.getElementById('errorMsg').style.display = 'block';
|
||||
document.getElementById('errorMsg').textContent = msg;
|
||||
layer.msg(msg, {icon: 2});
|
||||
|
||||
// 登录失败时刷新验证码
|
||||
document.getElementById('captcha-img').src = '/admin/captcha?t=' + new Date().getTime();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
layer.close(loadIndex);
|
||||
console.error('登录错误:', error);
|
||||
document.getElementById('errorMsg').style.display = 'block';
|
||||
document.getElementById('errorMsg').textContent = '网络错误,请稍后重试';
|
||||
layer.msg('网络错误,请稍后重试', {icon: 2});
|
||||
|
||||
// 网络错误时也刷新验证码
|
||||
document.getElementById('captcha-img').src = '/admin/captcha?t=' + new Date().getTime();
|
||||
});
|
||||
|
||||
return false; // 阻止表单跳转
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
|
||||
<!-- 站 点 图 标 -->
|
||||
<link href='/favicon.ico' rel='icon' type='image/x-icon'>
|
||||
<link href="/favicon.ico" rel="shortcut icon">
|
||||
<link href="/favicon.ico" rel="bookmark">
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="bookmark" href="/favicon.ico" />
|
||||
<!-- 样 式 文 件 -->
|
||||
<link rel="stylesheet" href="//lib.baomitu.com/layui/2.8.17/css/layui.css"/>
|
||||
<style>
|
||||
|
||||
Reference in New Issue
Block a user