feat:captcha

#112 #3
This commit is contained in:
samwaf
2025-03-24 10:12:24 +08:00
parent 0cf45cb723
commit 835e715f46
19 changed files with 858 additions and 4 deletions
+21 -1
View File
@@ -123,4 +123,24 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
wenlng/go-captcha
Apache License
Copyright (c) 2021 Awen <wengaolng@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+7
View File
@@ -63,6 +63,13 @@ func (wafCache *WafCache) SetWithTTlRenewTime(key string, value interface{}, ttl
ttl: ttl,
}
}
func (wafCache *WafCache) GetBytes(key string) ([]byte, error) {
key1Value := wafCache.Get(key)
if str, ok := key1Value.([]byte); ok {
return str, nil
}
return nil, errors.New("数据不存在")
}
func (wafCache *WafCache) GetString(key string) (string, error) {
key1Value := wafCache.Get(key)
if str, ok := key1Value.(string); ok {
+1 -1
View File
@@ -87,7 +87,7 @@ docker run --rm -v %cd%:/workspace samwaflocalcompile
## 集成的三方库
前端: 使用TDesign Vue Starter
后端: gorm,excelize(360EntSecGroup-Skylar),godlp(bytedance),gin,gocron,
grule-rule-engine,ip2region,sqlitedriver,viper,libinjection-go,corazawaf,go-acme/lego
grule-rule-engine,ip2region,sqlitedriver,viper,libinjection-go,corazawaf,go-acme/lego,wenlng/go-captcha
数据:
ipv6(GeoLite2-Country.mmdb) by maxmind
## TODO List
+2
View File
@@ -7,4 +7,6 @@ const (
CACHE_TOKEN = "CACHE_TOKEN" //鉴权信息
CACHE_DNS_BOT_IP = "CACHE_DNS_BOT_IP" //IP反向域名解析
CACHE_DNS_NORMAL_IP = "CACHE_DNS_NORMAL_IP" //正常IP
CACHE_CAPTCHA_TRY = "CACHE_CAPTCHA_TRY" //验证码临时
CACHE_CAPTCHA_PASS = "CACHE_CAPTCHA_PASS" //通过验证的码
)
+1
View File
@@ -29,6 +29,7 @@ var (
GCONFIG_RECORD_DNS_BOT_EXPIRE_HOURS int64 = 24 //DNS bot有效期 单位小时 默认1天
GCONFIG_RECORD_DNS_NORMAL_EXPIRE_HOURS int64 = 7 * 24 //DNS 正常有效期 单位小时 默认7天
GCONFIG_RECORD_SPIDER_DENY int64 = 0 //爬虫禁止访问开关 默认 0 只检测不阻止访问 1 检测并阻止访问
GCONFIG_RECORD_FAKE_SPIDER_CAPTCHA int64 = 0 //伪爬虫进行图形挑战开关 0 放过 1 显示图形验证码
GCONFIG_RECORD_HIDE_SERVER_HEADER int64 = 1 // 是否隐藏Server头信息 1隐藏 0不隐藏
GCONFIG_RECORD_FORCE_BIND_2FA int64 = 0 // 是否强制绑定双因素认证(1强制 0不强制)
GCONFIG_RECORD_DEBUG_ENABLE int64 = 0 //调试开关 默认关闭
+4
View File
@@ -13,6 +13,7 @@ require (
github.com/gin-gonic/gin v1.10.0
github.com/go-acme/lego/v4 v4.20.4
github.com/go-co-op/gocron v1.17.1
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/gorilla/websocket v1.5.0
github.com/hyperjumptech/grule-rule-engine v1.15.0
github.com/kardianos/service v1.2.2
@@ -27,6 +28,8 @@ require (
github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.9.0
github.com/twmb/franz-go v1.18.0
github.com/wenlng/go-captcha-assets v1.0.5
github.com/wenlng/go-captcha/v2 v2.0.3
go.uber.org/zap v1.25.0
golang.org/x/mod v0.21.0
golang.org/x/net v0.33.0
@@ -117,6 +120,7 @@ require (
golang.org/x/arch v0.12.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect
golang.org/x/image v0.16.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/tools v0.25.0 // indirect
google.golang.org/protobuf v1.36.1 // indirect
+9
View File
@@ -102,6 +102,8 @@ github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
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/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
@@ -258,6 +260,10 @@ github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/valllabh/ocsf-schema-golang v1.0.3 h1:eR8k/3jP/OOqB8LRCtdJ4U+vlgd/gk5y3KMXoodrsrw=
github.com/valllabh/ocsf-schema-golang v1.0.3/go.mod h1:sZ3as9xqm1SSK5feFWIR2CuGeGRhsM7TR1MbpBctzPk=
github.com/wenlng/go-captcha-assets v1.0.5 h1:TL+31Qe/kJwcuYyU+jHedjSTZnMu1XKgktKL++lH9Js=
github.com/wenlng/go-captcha-assets v1.0.5/go.mod h1:zinRACsdYcL/S6pHgI9Iv7FKTU41d00+43pNX+b9+MM=
github.com/wenlng/go-captcha/v2 v2.0.3 h1:QTZ39/gVDisPSgvL9O2X2HbTuj5P/z8QsdGB/aayg9c=
github.com/wenlng/go-captcha/v2 v2.0.3/go.mod h1:5hac1em3uXoyC5ipZ0xFv9umNM/waQvYAQdr0cx/h34=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@@ -280,6 +286,8 @@ golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw=
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
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.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
@@ -331,6 +339,7 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
+1
View File
@@ -33,6 +33,7 @@ type WebLog struct {
UNIX_ADD_TIME int64 `json:"unix_add_time" gorm:"index"` //添加日期unix
RISK_LEVEL int `json:"risk_level"` //危险等级 0:正常 1:轻微 2:有害 3:严重 4:特别严重
GUEST_IDENTIFICATION string `json:"guest_identification"` //访客身份识别
IsBot int `json:"is_bot"` //是否是机器人 0 不是机器人 1 机器人
TimeSpent int64 `json:"time_spent"` //用时
NetSrcIp string `json:"net_src_ip"` //获取的原始IP
SrcByteBody []byte `json:"src_byte_body"` //原始body信息
+3
View File
@@ -14,6 +14,7 @@ import (
"SamWaf/wafconfig"
"SamWaf/wafdb"
"SamWaf/wafenginecore"
"SamWaf/wafenginecore/wafcaptcha"
"SamWaf/wafinit"
"SamWaf/wafmangeweb"
"SamWaf/wafnotify"
@@ -167,6 +168,8 @@ func (m *wafSystenService) run() {
//初始化cache
global.GCACHE_WAFCACHE = cache.InitWafCache()
//初始化验证码服务
wafcaptcha.InitCaptchaService(global.GCACHE_WAFCACHE)
//初始化锁写不锁度
global.GWAF_MEASURE_PROCESS_DEQUEENGINE = cache.InitWafOnlyLockWrite()
// 创建 Snowflake 实例
+8
View File
@@ -35,6 +35,7 @@ type Hosts struct {
ResponseTimeOut int `json:"response_time_out"` //响应超时时间 默认60秒,为0则无限等待
HealthyJSON string `json:"healthy_json"` //后端健康度检测 json
InsecureSkipVerify int `json:"insecure_skip_verify"` //是否开启后端https证书有效性验证 默认 0 是校验 1 是不校验
CaptchaJSON string `json:"captcha_json"` //验证码配置 json
}
type HostsDefense struct {
@@ -58,3 +59,10 @@ type HealthyConfig struct {
ExpectedCodes string `json:"expected_codes"` // 预期状态码
LastErrorReason string `json:"last_error_reason"` // 最后一次错误原因
}
// CaptchaConfig 验证码配置
type CaptchaConfig struct {
IsEnableCaptcha int `json:"is_enable_captcha"` // 是否开启验证码 1开启 0关闭
ExcludeURLs string `json:"exclude_urls"` // 排除验证码的URL列表
ExpireTime int `json:"expire_time"` // 验证通过后的有效期(小时)
}
+2 -2
View File
@@ -30,7 +30,7 @@ type WafHostAddReq struct {
ResponseTimeOut int `json:"response_time_out"` //响应超时时间
HealthyJSON string `json:"healthy_json"` //后端健康度检测 json
InsecureSkipVerify int `json:"insecure_skip_verify"` //是否开启后端https证书有效性验证 默认 0 是校验 1 是不校验
CaptchaJSON string `json:"captcha_json"` //验证码配置 json
}
type WafHostDelReq struct {
@@ -69,7 +69,7 @@ type WafHostEditReq struct {
ResponseTimeOut int `json:"response_time_out"` //响应超时时间
HealthyJSON string `json:"healthy_json"` //后端健康度检测 json
InsecureSkipVerify int `json:"insecure_skip_verify"` //是否开启后端https证书有效性验证 默认 0 是校验 1 是不校验
CaptchaJSON string `json:"captcha_json"` //验证码配置 json
}
type WafHostGuardStatusReq struct {
+2
View File
@@ -61,6 +61,7 @@ func (receiver *WafHostService) AddApi(wafHostAddReq request.WafHostAddReq) (str
ResponseTimeOut: wafHostAddReq.ResponseTimeOut,
HealthyJSON: wafHostAddReq.HealthyJSON,
InsecureSkipVerify: wafHostAddReq.InsecureSkipVerify,
CaptchaJSON: wafHostAddReq.CaptchaJSON,
}
global.GWAF_LOCAL_DB.Create(wafHost)
return wafHost.Code, nil
@@ -112,6 +113,7 @@ func (receiver *WafHostService) ModifyApi(wafHostEditReq request.WafHostEditReq)
"ResponseTimeOut": wafHostEditReq.ResponseTimeOut,
"HealthyJSON": wafHostEditReq.HealthyJSON,
"InsecureSkipVerify": wafHostEditReq.InsecureSkipVerify,
"CaptchaJSON": wafHostEditReq.CaptchaJSON,
}
err := global.GWAF_LOCAL_DB.Debug().Model(model.Hosts{}).Where("CODE=?", wafHostEditReq.CODE).Updates(hostMap).Error
+3
View File
@@ -0,0 +1,3 @@
package wafbot
//TODO 拉取最新数据进行更新
+53
View File
@@ -0,0 +1,53 @@
package wafenginecore
import (
"SamWaf/enums"
"SamWaf/global"
"SamWaf/innerbean"
"SamWaf/wafenginecore/wafcaptcha"
"net/http"
)
// checkCaptchaToken 返回false 要验证信息 true 不验证信息
func (waf *WafEngine) checkCaptchaToken(r *http.Request, log innerbean.WebLog) bool {
// 首先从Cookie中获取验证标识
cookie, err := r.Cookie("samwaf_captcha_token")
if err == nil && cookie.Value != "" {
// 检查缓存中是否存在该标识
if global.GCACHE_WAFCACHE.IsKeyExist(enums.CACHE_CAPTCHA_PASS + cookie.Value) {
return true
}
}
// 如果Cookie中没有或无效,则检查请求头
token := r.Header.Get("X-SamWaf-Captcha-Token")
if token != "" {
// 检查缓存中是否存在该标识
if global.GCACHE_WAFCACHE.IsKeyExist(enums.CACHE_CAPTCHA_PASS + token) {
return true
}
}
//是bot而且危险程度是0,那么不用进行验证码挑战
if log.IsBot == 1 {
if log.RISK_LEVEL == 0 {
return true
} else {
if log.GUEST_IDENTIFICATION == "查询超时" || log.GUEST_IDENTIFICATION == "查询失败" {
return true
}
//伪爬虫是否开启图形验证
if global.GCONFIG_RECORD_FAKE_SPIDER_CAPTCHA == 0 {
return true
}
}
}
return false
}
// 处理验证码
func (waf *WafEngine) handleCaptchaRequest(w http.ResponseWriter, r *http.Request, expireTime int, log innerbean.WebLog) {
// 使用验证码服务处理请求
captchaService := wafcaptcha.GetService()
captchaService.HandleCaptchaRequest(w, r, expireTime, log)
}
+1
View File
@@ -38,6 +38,7 @@ func (waf *WafEngine) CheckBot(r *http.Request, weblogbean *innerbean.WebLog, fo
botResult = global.GCACHE_WAFCACHE.Get(enums.CACHE_DNS_BOT_IP + weblogbean.SRC_IP).(wafbot.BotResult)
}
if botResult.IsBot == true {
weblogbean.IsBot = 1
if botResult.IsNormalBot {
weblogbean.GUEST_IDENTIFICATION = botResult.BotName
} else {
+263
View File
@@ -0,0 +1,263 @@
package wafcaptcha
/*
import (
"encoding/json"
"fmt"
"github.com/golang/freetype/truetype"
"github.com/wenlng/go-captcha-assets/bindata/chars"
"github.com/wenlng/go-captcha-assets/resources/fonts/fzshengsksjw"
"github.com/wenlng/go-captcha-assets/resources/images"
"github.com/wenlng/go-captcha/v2/base/option"
"github.com/wenlng/go-captcha/v2/click"
"log"
"net/http"
"os"
"strconv"
"strings"
"sync/atomic"
"time"
)
var textCapt click.Captcha
var lightTextCapt click.Captcha
func init() {
builder := click.NewBuilder(
click.WithRangeLen(option.RangeVal{Min: 4, Max: 6}),
click.WithRangeVerifyLen(option.RangeVal{Min: 2, Max: 4}),
//click.WithRangeLen(option.RangeVal{Min: 2, Max: 4}),
//click.WithDisabledRangeVerifyLen(true),
click.WithRangeThumbColors([]string{
"#1f55c4",
"#780592",
"#2f6b00",
"#910000",
"#864401",
"#675901",
"#016e5c",
}),
click.WithRangeColors([]string{
"#fde98e",
"#60c1ff",
"#fcb08e",
"#fb88ff",
"#b4fed4",
"#cbfaa9",
"#78d6f8",
}),
)
// fonts
fonts, err := fzshengsksjw.GetFont()
if err != nil {
log.Fatalln(err)
}
// background images
imgs, err := images.GetImages()
if err != nil {
log.Fatalln(err)
}
// thumb images
//thumbImages, err := thumbs.GetThumbs()
//if err != nil {
// log.Fatalln(err)
//}
// set resources
builder.SetResources(
click.WithChars(chars.GetChineseChars()),
//click.WithChars([]string{
// "1A",
// "5E",
// "3d",
// "0p",
// "78",
// "DL",
// "CB",
// "9M",
//}),
//click.WithChars(chars.GetAlphaChars()),
click.WithFonts([]*truetype.Font{fonts}),
click.WithBackgrounds(imgs),
//click.WithThumbBackgrounds(thumbImages),
)
textCapt = builder.Make()
// ============================
builder.Clear()
builder.SetOptions(
click.WithRangeLen(option.RangeVal{Min: 4, Max: 6}),
click.WithRangeVerifyLen(option.RangeVal{Min: 2, Max: 4}),
click.WithRangeThumbColors([]string{
"#4a85fb",
"#d93ffb",
"#56be01",
"#ee2b2b",
"#cd6904",
"#b49b03",
"#01ad90",
}),
)
builder.SetResources(
click.WithChars(chars.GetChineseChars()),
click.WithFonts([]*truetype.Font{fonts}),
click.WithBackgrounds(imgs),
)
lightTextCapt = builder.Make()
}
// GetClickBasicCaptData .
func GetClickBasicCaptData(w http.ResponseWriter, r *http.Request) {
var capt click.Captcha
if r.URL.Query().Get("type") == "light" {
capt = lightTextCapt
} else {
capt = textCapt
}
captData, err := capt.Generate()
if err != nil {
log.Fatalln(err)
}
dotData := captData.GetData()
if dotData == nil {
bt, _ := json.Marshal(map[string]interface{}{
"code": 1,
"message": "gen captcha data failed",
})
_, _ = fmt.Fprintf(w, string(bt))
return
}
var masterImageBase64, thumbImageBase64 string
masterImageBase64, err = captData.GetMasterImage().ToBase64()
if err != nil {
bt, _ := json.Marshal(map[string]interface{}{
"code": 1,
"message": "base64 data failed",
})
_, _ = fmt.Fprintf(w, string(bt))
return
}
thumbImageBase64, err = captData.GetThumbImage().ToBase64()
if err != nil {
bt, _ := json.Marshal(map[string]interface{}{
"code": 1,
"message": "base64 data failed",
})
_, _ = fmt.Fprintf(w, string(bt))
return
}
dotsByte, _ := json.Marshal(dotData)
key := GenUniqueId()
cache.WriteCache(key, dotsByte)
fmt.Println("dot>>>>>", string(dotsByte))
bt, _ := json.Marshal(map[string]interface{}{
"code": 0,
"captcha_key": key,
"image_base64": masterImageBase64,
"thumb_base64": thumbImageBase64,
})
_, _ = fmt.Fprintf(w, string(bt))
}
var num int64
const (
Continuity = "20060102150405"
)
func GenUniqueId() string {
t := time.Now()
s := t.Format(Continuity)
m := t.UnixNano()/1e6 - t.UnixNano()/1e9*1e3
ms := sup(m, 3)
p := os.Getpid() % 1000
ps := sup(int64(p), 3)
i := atomic.AddInt64(&num, 1)
r := i % 10000
rs := sup(r, 4)
n := fmt.Sprintf("%s%s%s%s", s, ms, ps, rs)
return n
}
func sup(i int64, n int) string {
m := fmt.Sprintf("%d", i)
for len(m) < n {
m = fmt.Sprintf("0%s", m)
}
return m
}
// CheckClickData .
func CheckClickData(w http.ResponseWriter, r *http.Request) {
code := 1
_ = r.ParseForm()
dots := r.Form.Get("dots")
key := r.Form.Get("key")
if dots == "" || key == "" {
bt, _ := json.Marshal(map[string]interface{}{
"code": code,
"message": "dots or key param is empty",
})
_, _ = fmt.Fprintf(w, string(bt))
return
}
cacheDataByte := cache.ReadCache(key)
if len(cacheDataByte) == 0 {
bt, _ := json.Marshal(map[string]interface{}{
"code": code,
"message": "illegal key",
})
_, _ = fmt.Fprintf(w, string(bt))
return
}
src := strings.Split(dots, ",")
var dct map[int]*click.Dot
if err := json.Unmarshal(cacheDataByte, &dct); err != nil {
bt, _ := json.Marshal(map[string]interface{}{
"code": code,
"message": "illegal key",
})
_, _ = fmt.Fprintf(w, string(bt))
return
}
chkRet := false
if (len(dct) * 2) == len(src) {
for i := 0; i < len(dct); i++ {
dot := dct[i]
j := i * 2
k := i*2 + 1
sx, _ := strconv.ParseFloat(fmt.Sprintf("%v", src[j]), 64)
sy, _ := strconv.ParseFloat(fmt.Sprintf("%v", src[k]), 64)
chkRet = click.CheckPoint(int64(sx), int64(sy), int64(dot.X), int64(dot.Y), int64(dot.Width), int64(dot.Height), 0)
if !chkRet {
break
}
}
}
if chkRet {
code = 0
}
bt, _ := json.Marshal(map[string]interface{}{
"code": code,
})
_, _ = fmt.Fprintf(w, string(bt))
return
}
*/
+436
View File
@@ -0,0 +1,436 @@
package wafcaptcha
import (
"SamWaf/cache"
"SamWaf/common/zlog"
"SamWaf/enums"
"SamWaf/global"
"SamWaf/innerbean"
"SamWaf/utils"
"encoding/json"
"fmt"
"github.com/golang/freetype/truetype"
uuid "github.com/satori/go.uuid"
"github.com/wenlng/go-captcha-assets/bindata/chars"
"github.com/wenlng/go-captcha-assets/resources/fonts/fzshengsksjw"
"github.com/wenlng/go-captcha-assets/resources/images"
"github.com/wenlng/go-captcha/v2/base/option"
"github.com/wenlng/go-captcha/v2/click"
"log"
"net/http"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"go.uber.org/zap"
)
var (
captchaService *CaptchaService
once sync.Once
)
// CaptchaService 验证码服务结构体
type CaptchaService struct {
cache *cache.WafCache
//text
textCapt click.Captcha
lightTextCapt click.Captcha
}
// InitCaptchaService 初始化验证码服务,传入缓存引用
func InitCaptchaService(cache *cache.WafCache) {
once.Do(func() {
captchaService = &CaptchaService{
cache: cache,
}
captchaService.InitTextCapt()
})
}
func (s *CaptchaService) InitTextCapt() {
builder := click.NewBuilder(
click.WithRangeLen(option.RangeVal{Min: 4, Max: 6}),
click.WithRangeVerifyLen(option.RangeVal{Min: 2, Max: 4}),
//click.WithRangeLen(option.RangeVal{Min: 2, Max: 4}),
//click.WithDisabledRangeVerifyLen(true),
click.WithRangeThumbColors([]string{
"#1f55c4",
"#780592",
"#2f6b00",
"#910000",
"#864401",
"#675901",
"#016e5c",
}),
click.WithRangeColors([]string{
"#fde98e",
"#60c1ff",
"#fcb08e",
"#fb88ff",
"#b4fed4",
"#cbfaa9",
"#78d6f8",
}),
)
// fonts
fonts, err := fzshengsksjw.GetFont()
if err != nil {
log.Fatalln(err)
}
// background images
imgs, err := images.GetImages()
if err != nil {
log.Fatalln(err)
}
// thumb images
//thumbImages, err := thumbs.GetThumbs()
//if err != nil {
// log.Fatalln(err)
//}
// set resources
builder.SetResources(
click.WithChars(chars.GetChineseChars()),
//click.WithChars([]string{
// "1A",
// "5E",
// "3d",
// "0p",
// "78",
// "DL",
// "CB",
// "9M",
//}),
//click.WithChars(chars.GetAlphaChars()),
click.WithFonts([]*truetype.Font{fonts}),
click.WithBackgrounds(imgs),
//click.WithThumbBackgrounds(thumbImages),
)
s.textCapt = builder.Make()
// ============================
builder.Clear()
builder.SetOptions(
click.WithRangeLen(option.RangeVal{Min: 4, Max: 6}),
click.WithRangeVerifyLen(option.RangeVal{Min: 2, Max: 4}),
click.WithRangeThumbColors([]string{
"#4a85fb",
"#d93ffb",
"#56be01",
"#ee2b2b",
"#cd6904",
"#b49b03",
"#01ad90",
}),
)
builder.SetResources(
click.WithChars(chars.GetChineseChars()),
click.WithFonts([]*truetype.Font{fonts}),
click.WithBackgrounds(imgs),
)
s.lightTextCapt = builder.Make()
}
// GetService 获取验证码服务实例
func GetService() *CaptchaService {
if captchaService == nil {
zlog.Warn("验证码服务未初始化,请先调用 InitCaptchaService")
// 返回一个空服务,避免空指针异常
return &CaptchaService{}
}
return captchaService
}
// HandleCaptchaRequest 处理验证码请求
func (s *CaptchaService) HandleCaptchaRequest(w http.ResponseWriter, r *http.Request, expireTime int, weblog innerbean.WebLog) {
// 从路径中提取验证码相关部分
path := r.URL.Path
// 记录访问日志
zlog.Debug("验证码请求", zap.String("path", path), zap.String("method", r.Method), zap.String("remote_addr", r.RemoteAddr))
if strings.HasPrefix(path, "/samwaf_captcha/click_basic") {
s.GetClickBasicCaptData(w, r)
} else if strings.HasPrefix(path, "/samwaf_captcha/verify") {
// 根据请求参数确定验证码类型
captchaType := r.URL.Query().Get("type")
s.VerifyCaptcha(w, r, captchaType, expireTime, weblog)
} else if strings.HasPrefix(path, "/samwaf_captcha/") {
cleanPath := strings.TrimPrefix(path, "/samwaf_captcha/")
s.ServeStaticFile(w, r, cleanPath)
} else {
// 记录日志信息
weblog.ACTION = "禁止"
weblog.RULE = "显示图形验证码"
global.GQEQUE_LOG_DB.Enqueue(weblog)
// 默认显示验证码选择页面
s.ShowCaptchaHomePage(w, r)
}
}
// ServeStaticFile 提供静态文件服务
func (s *CaptchaService) ServeStaticFile(w http.ResponseWriter, r *http.Request, filePath string) {
// 安全检查:防止路径遍历攻击
if containsPathTraversal(filePath) {
zlog.Warn("检测到路径遍历尝试", zap.String("path", filePath), zap.String("remote_addr", r.RemoteAddr))
http.Error(w, "403 Forbidden", http.StatusForbidden)
return
}
// 规范化文件路径,移除所有 ".." 和多余的斜杠
cleanPath := path.Clean(filePath)
// 确保路径不以 "/" 或 "\" 开头,防止访问根目录
if strings.HasPrefix(cleanPath, "/") || strings.HasPrefix(cleanPath, "\\") {
cleanPath = cleanPath[1:]
}
// 根据文件扩展名设置Content-Type
if strings.HasSuffix(cleanPath, ".css") {
w.Header().Set("Content-Type", "text/css")
} else if strings.HasSuffix(cleanPath, ".js") {
w.Header().Set("Content-Type", "application/javascript")
} else if strings.HasSuffix(cleanPath, ".html") {
w.Header().Set("Content-Type", "text/html")
} else if strings.HasSuffix(cleanPath, ".png") {
w.Header().Set("Content-Type", "image/png")
} else if strings.HasSuffix(cleanPath, ".jpg") || strings.HasSuffix(cleanPath, ".jpeg") {
w.Header().Set("Content-Type", "image/jpeg")
}
// 构建安全的完整路径
basePath := utils.GetCurrentDir() + "/data/captcha/"
fullPath := filepath.Join(basePath, cleanPath)
// 再次验证路径是否在允许的目录内
absBasePath, _ := filepath.Abs(basePath)
absFullPath, _ := filepath.Abs(fullPath)
if !strings.HasPrefix(absFullPath, absBasePath) {
zlog.Warn("检测到目录遍历尝试", zap.String("path", filePath), zap.String("fullPath", fullPath), zap.String("remote_addr", r.RemoteAddr))
http.Error(w, "403 Forbidden", http.StatusForbidden)
return
}
// 检查文件是否存在
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
http.NotFound(w, r)
return
}
// 提供文件服务
http.ServeFile(w, r, fullPath)
}
// containsPathTraversal 检查路径中是否包含路径遍历尝试
func containsPathTraversal(filePath string) bool {
// 检查常见的路径遍历模式
return strings.Contains(filePath, "../") ||
strings.Contains(filePath, "..\\") ||
strings.Contains(filePath, "%2e%2e%2f") || // ../
strings.Contains(filePath, "%2e%2e/") || // ../
strings.Contains(filePath, "..%2f") || // ../
strings.Contains(filePath, "%2e%2e%5c") || // ..\
strings.Contains(filePath, "..%5c") || // ..\
strings.Contains(filePath, "\\\\") || // 双反斜杠
strings.Contains(filePath, "//") // 双正斜杠
}
// GetClickBasicCaptData 获取基础点击验证码数据
func (s *CaptchaService) GetClickBasicCaptData(w http.ResponseWriter, r *http.Request) {
var capt click.Captcha
if r.URL.Query().Get("type") == "light" {
capt = s.lightTextCapt
} else {
capt = s.textCapt
}
captData, err := capt.Generate()
if err != nil {
log.Fatalln(err)
}
dotData := captData.GetData()
if dotData == nil {
bt, _ := json.Marshal(map[string]interface{}{
"code": 1,
"message": "gen captcha data failed",
})
_, _ = fmt.Fprintf(w, string(bt))
return
}
var masterImageBase64, thumbImageBase64 string
masterImageBase64, err = captData.GetMasterImage().ToBase64()
if err != nil {
bt, _ := json.Marshal(map[string]interface{}{
"code": 1,
"message": "base64 data failed",
})
_, _ = fmt.Fprintf(w, string(bt))
return
}
thumbImageBase64, err = captData.GetThumbImage().ToBase64()
if err != nil {
bt, _ := json.Marshal(map[string]interface{}{
"code": 1,
"message": "base64 data failed",
})
_, _ = fmt.Fprintf(w, string(bt))
return
}
dotsByte, _ := json.Marshal(dotData)
key := uuid.NewV4().String()
//key := helper.StringToMD5(string(dotsByte))
s.cache.SetWithTTl(enums.CACHE_CAPTCHA_TRY+key, dotsByte, 1*time.Minute)
bt, _ := json.Marshal(map[string]interface{}{
"code": 0,
"captcha_key": key,
"image_base64": masterImageBase64,
"thumb_base64": thumbImageBase64,
})
_, _ = fmt.Fprintf(w, string(bt))
}
// VerifyCaptcha 验证验证码
func (s *CaptchaService) VerifyCaptcha(w http.ResponseWriter, r *http.Request, captchaType string, expireTime int, webLog innerbean.WebLog) {
code := 1
_ = r.ParseForm()
dots := r.Form.Get("dots")
key := r.Form.Get("key")
if dots == "" || key == "" {
bt, _ := json.Marshal(map[string]interface{}{
"code": code,
"message": "dots or key param is empty",
})
_, _ = fmt.Fprintf(w, string(bt))
return
}
cacheDataByte, err := s.cache.GetBytes(enums.CACHE_CAPTCHA_TRY + key)
if err != nil {
bt, _ := json.Marshal(map[string]interface{}{
"code": code,
"message": "illegal key",
})
_, _ = fmt.Fprintf(w, string(bt))
return
}
s.cache.Remove(enums.CACHE_CAPTCHA_TRY + key)
if len(cacheDataByte) == 0 {
bt, _ := json.Marshal(map[string]interface{}{
"code": code,
"message": "illegal key",
})
_, _ = fmt.Fprintf(w, string(bt))
return
}
src := strings.Split(dots, ",")
var dct map[int]*click.Dot
if err := json.Unmarshal(cacheDataByte, &dct); err != nil {
bt, _ := json.Marshal(map[string]interface{}{
"code": code,
"message": "illegal key",
})
_, _ = fmt.Fprintf(w, string(bt))
return
}
chkRet := false
if (len(dct) * 2) == len(src) {
for i := 0; i < len(dct); i++ {
dot := dct[i]
j := i * 2
k := i*2 + 1
sx, _ := strconv.ParseFloat(fmt.Sprintf("%v", src[j]), 64)
sy, _ := strconv.ParseFloat(fmt.Sprintf("%v", src[k]), 64)
chkRet = click.CheckPoint(int64(sx), int64(sy), int64(dot.X), int64(dot.Y), int64(dot.Width), int64(dot.Height), 0)
if !chkRet {
break
}
}
}
if chkRet {
code = 0
// 生成验证通过的标识
captchaPassToken := uuid.NewV4().String()
// 将标识存入缓存
s.cache.SetWithTTl(enums.CACHE_CAPTCHA_PASS+captchaPassToken, "ok", time.Duration(expireTime)*time.Hour)
// 设置Cookie
cookie := &http.Cookie{
Name: "samwaf_captcha_token",
Value: captchaPassToken,
Path: "/",
HttpOnly: true,
Secure: r.TLS != nil, // 如果是HTTPS请求则设置Secure
MaxAge: expireTime * 3600,
}
http.SetCookie(w, cookie)
// 同时在响应头中也设置验证标识
w.Header().Set("X-SamWaf-Captcha-Token", captchaPassToken)
webLog.ACTION = "放行"
webLog.RULE = "图形验证码验证通过"
global.GQEQUE_LOG_DB.Enqueue(webLog)
} else {
webLog.ACTION = "禁止"
webLog.RULE = "图形验证码验证失败"
global.GQEQUE_LOG_DB.Enqueue(webLog)
}
bt, _ := json.Marshal(map[string]interface{}{
"code": code,
})
_, _ = fmt.Fprintf(w, string(bt))
return
}
// ShowCaptchaHomePage 显示验证码首页
func (s *CaptchaService) ShowCaptchaHomePage(w http.ResponseWriter, r *http.Request) {
// 设置内容类型
w.Header().Set("Content-Type", "text/html; charset=utf-8")
// 从指定目录加载index.html
http.ServeFile(w, r, utils.GetCurrentDir()+"/data/captcha/index.html")
}
// 辅助函数
// writeJSONResponse 写入JSON响应
func writeJSONResponse(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(data)
}
// generateRandomKey 生成随机密钥
func generateRandomKey() string {
// 实际实现中应该使用更安全的随机数生成方法
return "random_key_123456"
}
// generateImageBase64 生成图片的Base64编码
func generateImageBase64() string {
// 实际实现中应该生成真实的验证码图片
return "base64_encoded_image_data"
}
// generateThumbBase64 生成缩略图的Base64编码
func generateThumbBase64() string {
// 实际实现中应该生成真实的缩略图
return "base64_encoded_thumb_data"
}
+36
View File
@@ -356,6 +356,42 @@ func (waf *WafEngine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
// 验证码检测
captchaConfig := model.CaptchaConfig{
IsEnableCaptcha: 0,
ExcludeURLs: "",
ExpireTime: 24,
}
err = json.Unmarshal([]byte(hostTarget.Host.CaptchaJSON), &captchaConfig)
if err != nil {
zlog.Debug("解析captcha json失败")
}
if captchaConfig.IsEnableCaptcha == 1 {
if !waf.checkCaptchaToken(r, weblogbean) {
// 检查当前URL是否在排除列表中
currentURL := strings.ToLower(r.URL.Path)
isExcluded := false
if len(captchaConfig.ExcludeURLs) > 0 {
// 将换行分隔的URL列表拆分为数组
excludeURLs := strings.Split(captchaConfig.ExcludeURLs, "\n")
for _, excludeURL := range excludeURLs {
// 去除可能的空白字符并转为小写
excludeURL = strings.TrimSpace(strings.ToLower(excludeURL))
if excludeURL != "" && strings.HasPrefix(currentURL, excludeURL) {
isExcluded = true
break
}
}
}
if !isExcluded {
waf.handleCaptchaRequest(w, r, captchaConfig.ExpireTime, weblogbean)
return
}
}
}
}
}
+5
View File
@@ -79,6 +79,9 @@ func setConfigIntValue(name string, value int64, change int) {
case "force_bind_2fa":
global.GCONFIG_RECORD_FORCE_BIND_2FA = value
break
case "fake_spider_captcha":
global.GCONFIG_RECORD_FAKE_SPIDER_CAPTCHA = value
break
default:
zlog.Warn("Unknown config item:", name)
}
@@ -218,4 +221,6 @@ func TaskLoadSetting(initLoad bool) {
updateConfigStringItem(initLoad, "gpt", "gpt_model", global.GCONFIG_RECORD_GPT_MODEL, "GPT模型名称", "string", "")
updateConfigIntItem(initLoad, "security", "hide_server_header", global.GCONFIG_RECORD_HIDE_SERVER_HEADER, "是否隐藏Server响应头(1隐藏 0不隐藏)", "int", "")
updateConfigIntItem(initLoad, "security", "force_bind_2fa", global.GCONFIG_RECORD_FORCE_BIND_2FA, "是否强制绑定双因素认证(1强制 0不强制)", "options", "0|不强制,1|强制")
updateConfigIntItem(initLoad, "system", "fake_spider_captcha", global.GCONFIG_RECORD_FAKE_SPIDER_CAPTCHA, "伪爬虫进行图形挑战开关 0 放过 1 显示图形验证码", "int", "")
}