From 7bad171474311d0780dff8a9352bc344f42dd8ad Mon Sep 17 00:00:00 2001 From: youmulijiang Date: Sun, 9 Aug 2026 09:56:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=B7=BB=E5=8A=A0=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E7=A0=81=E5=8A=9F=E8=83=BD,=E5=8F=AF?= =?UTF-8?q?=E4=BB=A5=E5=9C=A8=E7=B3=BB=E7=BB=9F=E8=AE=BE=E7=BD=AE->?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E4=B8=AD=E5=BF=83=E4=B8=AD=E5=BC=80=E5=90=AF?= =?UTF-8?q?=E3=80=82=E5=BC=80=E5=90=AF=E5=90=8E=EF=BC=8C=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E6=97=B6=E9=9C=80=E8=A6=81=E8=BE=93=E5=85=A5=E5=9B=BE=E5=BD=A2?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E7=A0=81=EF=BC=8C=E5=8F=AF=E6=9C=89=E6=95=88?= =?UTF-8?q?=E9=98=B2=E6=AD=A2=E6=9A=B4=E5=8A=9B=E7=A0=B4=E8=A7=A3=E3=80=82?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=85=B3=E9=97=AD=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 10 +++-- go.sum | 38 +++++++++++++++++++ internal/app/app.go | 3 ++ internal/config/config.go | 3 +- internal/handler/auth.go | 55 +++++++++++++++++++++++---- internal/handler/captcha.go | 33 ++++++++++++++++ internal/handler/config.go | 29 ++++++++++++++ web/static/css/style.css | 36 ++++++++++++++++++ web/static/js/auth.js | 75 ++++++++++++++++++++++++++++++++++--- web/static/js/settings.js | 33 ++++++++++++++++ web/templates/index.html | 23 ++++++++++++ 11 files changed, 321 insertions(+), 17 deletions(-) create mode 100644 internal/handler/captcha.go diff --git a/go.mod b/go.mod index 721ded918..9e15ce173 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module cyberstrike-ai // 若 go mod download 超时,可执行: go env -w GOPROXY=https://goproxy.cn,direct // 或使用 scripts/bootstrap-go.sh -go 1.25 +go 1.25.0 require ( github.com/bwmarrin/discordgo v0.29.0 @@ -24,6 +24,7 @@ require ( github.com/larksuite/oapi-sdk-go/v3 v3.4.22 github.com/mattn/go-sqlite3 v1.14.18 github.com/modelcontextprotocol/go-sdk v1.2.0 + github.com/mojocn/base64Captcha v1.3.8 github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1 github.com/pkoukk/tiktoken-go v0.1.8 github.com/robfig/cron/v3 v3.0.1 @@ -38,7 +39,7 @@ require ( go.uber.org/zap v1.26.0 golang.org/x/net v0.35.0 golang.org/x/term v0.32.0 - golang.org/x/text v0.26.0 + golang.org/x/text v0.40.0 golang.org/x/time v0.14.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -65,6 +66,7 @@ require ( github.com/go-resty/resty/v2 v2.6.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/google/jsonschema-go v0.3.0 // indirect github.com/goph/emperror v0.17.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect @@ -97,9 +99,9 @@ require ( golang.org/x/arch v0.15.0 // indirect golang.org/x/crypto v0.39.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect - golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect + golang.org/x/image v0.44.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.15.0 // indirect + golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.33.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect diff --git a/go.sum b/go.sum index 95a823112..5db04730e 100644 --- a/go.sum +++ b/go.sum @@ -97,6 +97,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +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/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -114,6 +116,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -176,6 +179,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mojocn/base64Captcha v1.3.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg= +github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4= github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c= github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -296,17 +301,26 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 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.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +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.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= +golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I= +golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -319,7 +333,10 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT 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.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +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/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= @@ -331,8 +348,14 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -353,14 +376,21 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.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.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.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.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +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/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -369,9 +399,14 @@ golang.org/x/text v0.3.6/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.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -381,8 +416,11 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 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/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/app/app.go b/internal/app/app.go index a8cbc491f..1998e5b79 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -394,6 +394,8 @@ func New(cfg *config.Config, log *logger.Logger, configPath string) (*App, error groupHandler := handler.NewGroupHandler(db, log.Logger) authHandler := handler.NewAuthHandler(authManager, cfg, configPath, log.Logger) authHandler.SetAudit(auditSvc) + captchaSvc := handler.NewCaptchaService() + authHandler.SetCaptchaService(captchaSvc) attackChainHandler := handler.NewAttackChainHandler(db, &cfg.OpenAI, log.Logger) vulnerabilityHandler := handler.NewVulnerabilityHandler(db, log.Logger) assetHandler := handler.NewAssetHandler(db, log.Logger) @@ -900,6 +902,7 @@ func setupRoutes( authRoutes := api.Group("/auth") loginRL := security.NewRateLimiter(10, 1*time.Minute) { + authRoutes.GET("/captcha", authHandler.GetCaptcha) authRoutes.POST("/login", security.RateLimitMiddleware(loginRL), authHandler.Login) authRoutes.POST("/logout", security.AuthMiddleware(authManager), authHandler.Logout) authRoutes.POST("/change-password", security.AuthMiddleware(authManager), security.RequirePermission("auth:self"), authHandler.ChangePassword) diff --git a/internal/config/config.go b/internal/config/config.go index 25f2508cc..db9d9abf1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1189,7 +1189,8 @@ func normalizeHitlModeForPrompt(mode string) string { } type AuthConfig struct { - SessionDurationHours int `yaml:"session_duration_hours" json:"session_duration_hours"` + SessionDurationHours int `yaml:"session_duration_hours" json:"session_duration_hours"` + CaptchaEnabled bool `yaml:"captcha_enabled" json:"captcha_enabled"` } // MonitorConfig MCP 状态监控(tool_executions)保留策略。 diff --git a/internal/handler/auth.go b/internal/handler/auth.go index 4157115e2..e815ad62d 100644 --- a/internal/handler/auth.go +++ b/internal/handler/auth.go @@ -15,11 +15,12 @@ import ( // AuthHandler handles authentication-related endpoints. type AuthHandler struct { - manager *security.AuthManager - config *config.Config - configPath string - logger *zap.Logger - audit *audit.Service + manager *security.AuthManager + config *config.Config + configPath string + logger *zap.Logger + audit *audit.Service + captchaSvc *CaptchaService } // SetAudit wires platform audit logging. @@ -27,6 +28,11 @@ func (h *AuthHandler) SetAudit(s *audit.Service) { h.audit = s } +// SetCaptchaService 注入验证码服务。 +func (h *AuthHandler) SetCaptchaService(svc *CaptchaService) { + h.captchaSvc = svc +} + // NewAuthHandler creates a new AuthHandler. func NewAuthHandler(manager *security.AuthManager, cfg *config.Config, configPath string, logger *zap.Logger) *AuthHandler { return &AuthHandler{ @@ -38,8 +44,10 @@ func NewAuthHandler(manager *security.AuthManager, cfg *config.Config, configPat } type loginRequest struct { - Username string `json:"username"` - Password string `json:"password" binding:"required"` + Username string `json:"username"` + Password string `json:"password" binding:"required"` + CaptchaID string `json:"captcha_id"` + CaptchaVal string `json:"captcha"` } type changePasswordRequest struct { @@ -55,6 +63,18 @@ func (h *AuthHandler) Login(c *gin.Context) { return } + // 若开启验证码,则先校验验证码 + if h.config.Auth.CaptchaEnabled && h.captchaSvc != nil { + if strings.TrimSpace(req.CaptchaID) == "" || strings.TrimSpace(req.CaptchaVal) == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "请输入验证码", "captcha_required": true}) + return + } + if !h.captchaSvc.Verify(req.CaptchaID, req.CaptchaVal) { + c.JSON(http.StatusBadRequest, gin.H{"error": "验证码错误或已过期", "captcha_required": true}) + return + } + } + token, expiresAt, err := h.manager.Authenticate(req.Username, req.Password) if err != nil { if h.audit != nil { @@ -226,6 +246,27 @@ func (h *AuthHandler) Validate(c *gin.Context) { }) } +// GetCaptcha 返回验证码状态及图片(公开接口,无需认证)。 +// 若验证码功能未开启,仅返回 enabled:false。 +func (h *AuthHandler) GetCaptcha(c *gin.Context) { + if !h.config.Auth.CaptchaEnabled || h.captchaSvc == nil { + c.JSON(http.StatusOK, gin.H{"enabled": false}) + return + } + + id, b64s, err := h.captchaSvc.Generate() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "生成验证码失败"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "enabled": true, + "id": id, + "b64s": b64s, + }) +} + func permissionKeys(perms map[string]bool) []string { keys := make([]string, 0, len(perms)) for key, ok := range perms { diff --git a/internal/handler/captcha.go b/internal/handler/captcha.go new file mode 100644 index 000000000..5c085a69a --- /dev/null +++ b/internal/handler/captcha.go @@ -0,0 +1,33 @@ +package handler + +import ( + "github.com/mojocn/base64Captcha" +) + +// CaptchaService 验证码生成与校验服务(基于 base64Captcha)。 +type CaptchaService struct { + store base64Captcha.Store + driver base64Captcha.Driver +} + +// NewCaptchaService 创建默认数字验证码服务(内存存储,自动过期)。 +func NewCaptchaService() *CaptchaService { + // 高度 80px,宽度 240px,5 位数字,干扰线密度 0.7,最大倾斜角 80 + driver := base64Captcha.NewDriverDigit(80, 240, 5, 0.7, 80) + return &CaptchaService{ + store: base64Captcha.DefaultMemStore, + driver: driver, + } +} + +// Generate 生成一个新验证码,返回 id 和 base64 编码的图片字符串。 +func (s *CaptchaService) Generate() (id, b64s string, err error) { + c := base64Captcha.NewCaptcha(s.driver, s.store) + id, b64s, _, err = c.Generate() + return +} + +// Verify 校验验证码,校验后该 id 立即失效(clear=true)。 +func (s *CaptchaService) Verify(id, answer string) bool { + return s.store.Verify(id, answer, true) +} diff --git a/internal/handler/config.go b/internal/handler/config.go index e3c26e5cd..be7282ab6 100644 --- a/internal/handler/config.go +++ b/internal/handler/config.go @@ -256,6 +256,11 @@ func (h *ConfigHandler) ApplyWechatRobotBinding(wc config.RobotWechatConfig) err return nil } +// AuthPublicConfig 暴露给前端的认证配置(不含敏感字段)。 +type AuthPublicConfig struct { + CaptchaEnabled bool `json:"captcha_enabled"` +} + // GetConfigResponse 获取配置响应 type GetConfigResponse struct { AI config.AIConfig `json:"ai"` @@ -273,6 +278,7 @@ type GetConfigResponse struct { Robots config.RobotsConfig `json:"robots,omitempty"` MultiAgent config.MultiAgentPublic `json:"multi_agent,omitempty"` C2 config.C2Public `json:"c2"` + Auth AuthPublicConfig `json:"auth"` } // ToolConfigInfo 工具配置信息 @@ -379,6 +385,7 @@ func (h *ConfigHandler) GetConfig(c *gin.Context) { C2: h.config.C2.Public(), Robots: h.config.Robots, MultiAgent: multiPub, + Auth: AuthPublicConfig{CaptchaEnabled: h.config.Auth.CaptchaEnabled}, }) } @@ -706,6 +713,11 @@ func (h *ConfigHandler) GetTools(c *gin.Context) { }) } +// AuthConfigUpdate 认证配置更新请求(仅含可由前端更改的字段)。 +type AuthConfigUpdate struct { + CaptchaEnabled *bool `json:"captcha_enabled,omitempty"` +} + // UpdateConfigRequest 更新配置请求 type UpdateConfigRequest struct { AI *config.AIConfig `json:"ai,omitempty"` @@ -723,6 +735,7 @@ type UpdateConfigRequest struct { Robots *config.RobotsConfig `json:"robots,omitempty"` MultiAgent *config.MultiAgentAPIUpdate `json:"multi_agent,omitempty"` C2 *config.C2APIUpdate `json:"c2,omitempty"` + Auth *AuthConfigUpdate `json:"auth,omitempty"` } // AgentConfigUpdate 用于 PATCH /api/config 的 agent 段:仅 JSON 中出现的字段(指针非 nil)覆盖内存配置。 @@ -1128,6 +1141,14 @@ func (h *ConfigHandler) UpdateConfig(c *gin.Context) { } } + // 更新认证配置(验证码开关) + if req.Auth != nil { + if req.Auth.CaptchaEnabled != nil { + h.config.Auth.CaptchaEnabled = *req.Auth.CaptchaEnabled + h.logger.Info("更新验证码配置", zap.Bool("captcha_enabled", h.config.Auth.CaptchaEnabled)) + } + } + // 保存配置到文件 if err := h.saveConfig(); err != nil { h.logger.Error("保存配置失败", zap.Error(err)) @@ -1699,6 +1720,7 @@ func (h *ConfigHandler) saveConfig() error { updateAgentConfig(root, h.config.Agent) updateMCPConfig(root, h.config.MCP) updateAIConfig(root, h.config.AI) + updateAuthConfig(root, h.config.Auth) removeKeyFromMap(root.Content[0], "openai") updateVisionConfig(root, h.config.Vision) updateFOFAConfig(root, h.config.FOFA) @@ -1819,6 +1841,13 @@ func updateAgentConfig(doc *yaml.Node, agent config.AgentConfig) { setStringInMap(agentNode, "system_prompt_path", agent.SystemPromptPath) } +func updateAuthConfig(doc *yaml.Node, cfg config.AuthConfig) { + root := doc.Content[0] + authNode := ensureMap(root, "auth") + setIntInMap(authNode, "session_duration_hours", cfg.SessionDurationHours) + setBoolInMap(authNode, "captcha_enabled", cfg.CaptchaEnabled) +} + func updateMCPConfig(doc *yaml.Node, cfg config.MCPConfig) { root := doc.Content[0] mcpNode := ensureMap(root, "mcp") diff --git a/web/static/css/style.css b/web/static/css/style.css index 17e8ecb6c..ba7b14667 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -6242,6 +6242,42 @@ html[data-theme="dark"] .chat-input-container .send-btn:disabled::before { font-size: 0.8125rem; } +.captcha-input-row { + display: flex; + gap: 8px; + align-items: center; +} + +.captcha-input-row input { + flex: 1; +} + +.captcha-refresh-btn { + background: none; + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 2px; + cursor: pointer; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + transition: border-color 0.2s; +} + +.captcha-refresh-btn:hover { + border-color: var(--accent-color); +} + +.captcha-img { + display: block; + height: 40px; + width: auto; + max-width: 120px; + border-radius: 6px; +} + .login-card .login-submit { width: 100%; justify-content: center; diff --git a/web/static/js/auth.js b/web/static/js/auth.js index d1b263de6..1cdffe1a4 100644 --- a/web/static/js/auth.js +++ b/web/static/js/auth.js @@ -13,10 +13,53 @@ let robotBindingExpiresAt = 0; let robotBindingLifetimeMs = 5 * 60 * 1000; let activeRobotBindingCode = ''; +// 验证码状态 +let captchaEnabled = false; +let captchaID = ''; + function isTokenValid() { return !!authToken && authTokenExpiry instanceof Date && authTokenExpiry.getTime() > Date.now(); } +async function loadCaptchaIfNeeded() { + try { + const res = await fetch('/api/auth/captcha'); + if (!res.ok) return; + const data = await res.json().catch(() => ({})); + captchaEnabled = !!data.enabled; + const row = document.getElementById('login-captcha-row'); + if (!row) return; + if (captchaEnabled) { + row.style.display = ''; + // 已有验证码时不重新生成,避免多次调用 showLoginOverlay 导致验证码意外刷新 + if (!captchaID) { + captchaID = data.id || ''; + const img = document.getElementById('login-captcha-img'); + if (img && data.b64s) img.src = data.b64s; + } + } else { + row.style.display = 'none'; + } + } catch (_) { + // 网络异常时不显示验证码,允许继续登录 + } +} + +async function refreshLoginCaptcha() { + try { + const res = await fetch('/api/auth/captcha'); + if (!res.ok) return; + const data = await res.json().catch(() => ({})); + captchaID = data.id || ''; + const img = document.getElementById('login-captcha-img'); + if (img && data.b64s) img.src = data.b64s; + const input = document.getElementById('login-captcha'); + if (input) input.value = ''; + } catch (_) {} +} + +window.refreshLoginCaptcha = refreshLoginCaptcha; + function saveAuth(token, expiresAt, meta = {}) { const expiry = expiresAt instanceof Date ? expiresAt : new Date(expiresAt); authToken = token; @@ -104,11 +147,10 @@ function showLoginOverlay(message = '') { if (message) { errorBox.textContent = message; errorBox.style.display = 'block'; - } else { - errorBox.textContent = ''; - errorBox.style.display = 'none'; } + // 无消息时保留已有错误,避免后台 apiFetch 触发 ensureAuthenticated 时意外清除 } + loadCaptchaIfNeeded(); setTimeout(function () { if (usernameInput && !usernameInput.value) { usernameInput.focus(); @@ -123,6 +165,7 @@ function hideLoginOverlay() { const errorBox = document.getElementById('login-error'); const usernameInput = document.getElementById('login-username'); const passwordInput = document.getElementById('login-password'); + const captchaInput = document.getElementById('login-captcha'); closeAppModal('login-overlay'); if (errorBox) { errorBox.textContent = ''; @@ -131,9 +174,14 @@ function hideLoginOverlay() { if (passwordInput) { passwordInput.value = ''; } + if (captchaInput) { + captchaInput.value = ''; + } if (usernameInput && !authUser) { usernameInput.value = ''; } + // 重置验证码状态,确保下次打开弹窗时获取新验证码 + captchaID = ''; } function ensureAuthPromise() { @@ -254,6 +302,12 @@ async function submitLogin(event) { return; } + // 用户主动提交时清除上一次的错误提示 + if (errorBox) { + errorBox.textContent = ''; + errorBox.style.display = 'none'; + } + const username = usernameInput ? usernameInput.value.trim() : ''; const password = passwordInput.value.trim(); if (!password) { @@ -272,12 +326,19 @@ async function submitLogin(event) { } try { + const captchaInput = document.getElementById('login-captcha'); + const captchaVal = captchaInput ? captchaInput.value.trim() : ''; + const body = { username, password }; + if (captchaEnabled) { + body.captcha_id = captchaID; + body.captcha = captchaVal; + } const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ username, password }), + body: JSON.stringify(body), }); const result = await response.json().catch(() => ({})); if (!response.ok || !result.token) { @@ -288,6 +349,10 @@ async function submitLogin(event) { errorBox.textContent = result.error || fallback; errorBox.style.display = 'block'; } + // 验证码错误时刷新验证码 + if (result.captcha_required || captchaEnabled) { + await refreshLoginCaptcha(); + } return; } @@ -910,7 +975,7 @@ async function logout() { // 无论如何都清除本地认证信息 clearAuthStorage(); hideLoginOverlay(); - showLoginOverlay(typeof window.t === 'function' ? window.t('auth.loggedOut') : '已退出登录'); + showLoginOverlay(); } } diff --git a/web/static/js/settings.js b/web/static/js/settings.js index eb6470dab..4462be97d 100644 --- a/web/static/js/settings.js +++ b/web/static/js/settings.js @@ -1114,6 +1114,12 @@ async function loadConfig(loadTools = true, options = {}) { initSettingsCustomSelects(); refreshSettingsCustomSelects(); + // 初始化验证码开关 + const captchaEnabledCb = document.getElementById('security-captcha-enabled'); + if (captchaEnabledCb) { + captchaEnabledCb.checked = currentConfig.auth?.captcha_enabled === true; + } + // 只有在需要时才加载工具列表(MCP管理页面需要,系统设置页面不需要) if (loadTools) { // 设置每页显示数量(会在分页控件渲染时设置) @@ -3883,6 +3889,33 @@ function resetPasswordForm() { }); } +async function saveCaptchaSetting(enabled) { + if (typeof requirePermission === 'function' && !requirePermission('config:write')) { + // 恢复开关状态 + const cb = document.getElementById('security-captcha-enabled'); + if (cb) cb.checked = !enabled; + return; + } + try { + const response = await apiFetch('/api/config', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ auth: { captcha_enabled: enabled } }), + }); + if (!response.ok) { + const data = await response.json().catch(() => ({})); + if (typeof notifyApiError === 'function') notifyApiError(data.error || '保存失败'); + const cb = document.getElementById('security-captcha-enabled'); + if (cb) cb.checked = !enabled; + return; + } + } catch (error) { + console.error('保存验证码设置失败:', error); + const cb = document.getElementById('security-captcha-enabled'); + if (cb) cb.checked = !enabled; + } +} + async function changePassword() { const currentInput = document.getElementById('auth-current-password'); const newInput = document.getElementById('auth-new-password'); diff --git a/web/templates/index.html b/web/templates/index.html index f65ebde3e..48d96fde8 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -49,6 +49,15 @@

+