Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 66995c0c78 | |||
| 120e690795 |
+11
@@ -1,6 +1,16 @@
|
||||
FROM golang:latest AS backend-builder
|
||||
RUN apt update
|
||||
RUN apt install -y build-essential
|
||||
|
||||
# 安装 OpenCV 开发库和依赖
|
||||
RUN apt install -y \
|
||||
libopencv-dev \
|
||||
libopencv-core-dev \
|
||||
libopencv-imgproc-dev \
|
||||
libopencv-imgcodecs-dev \
|
||||
libopencv-videoio-dev \
|
||||
pkg-config
|
||||
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
ENV CGO_ENABLED=1
|
||||
@@ -11,6 +21,7 @@ RUN apt install -y upx-ucl
|
||||
RUN upx --best --lzma bin/server
|
||||
|
||||
FROM golang:latest
|
||||
|
||||
COPY --from=backend-builder /build/bin/server /app/server
|
||||
#COPY static /app/static
|
||||
#COPY static/*.so /app/
|
||||
|
||||
@@ -33,6 +33,7 @@ require (
|
||||
github.com/yalue/onnxruntime_go v1.12.1
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
go.uber.org/zap v1.27.0
|
||||
gocv.io/x/gocv v0.42.0
|
||||
golang.org/x/crypto v0.43.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
|
||||
@@ -244,6 +244,8 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
gocv.io/x/gocv v0.42.0 h1:AAsrFJH2aIsQHukkCovWqj0MCGZleQpVyf5gNVRXjQI=
|
||||
gocv.io/x/gocv v0.42.0/go.mod h1:zYdWMj29WAEznM3Y8NsU3A0TRq/wR/cy75jeUypThqU=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
|
||||
@@ -16,10 +16,12 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/avast/retry-go/v4"
|
||||
"github.com/deatil/go-cryptobin/cryptobin/crypto"
|
||||
"github.com/samber/oops"
|
||||
"github.com/tidwall/gjson"
|
||||
@@ -188,20 +190,35 @@ func (c *CpdailyCasLogin) Login(ctx context.Context, schoolDetail *school.School
|
||||
} else {
|
||||
allInputs["password"] = encryptCasPassword(password, salt)
|
||||
}
|
||||
|
||||
var SliderCaptchaIds = []string{"1018764144934965"}
|
||||
// 判断是否需要验证码
|
||||
if isCasNeedCaptcha(ctx, loginResponseUrl, loginType, username, c.client) {
|
||||
// 解决验证码
|
||||
captchaUrl := getCasCaptchaUrl(loginResponseUrl, loginType)
|
||||
// 如果是滑块验证码
|
||||
if slices.Contains(SliderCaptchaIds, schoolDetail.Id) {
|
||||
e := retry.Do(func() error {
|
||||
return utils.Catch(func() {
|
||||
captchaUrl := getCasSliderCaptchaUrl(loginResponseUrl)
|
||||
verifyUrl := getCasSliderVerifyUrl(loginResponseUrl)
|
||||
ResolveCasSliderCaptcha(ctx, captchaUrl, verifyUrl, c.client)
|
||||
})
|
||||
}, retry.MaxDelay(15*time.Second), retry.Attempts(5), retry.Context(ctx))
|
||||
if e != nil {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "滑块验证码解析失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
} else {
|
||||
// 解决常规图片验证码
|
||||
captchaUrl := getCasCaptchaUrl(loginResponseUrl, loginType)
|
||||
|
||||
captchaResult := ResolveOcrCaptcha(ctx, captchaUrl, c.client)
|
||||
captchaResult := ResolveOcrCaptcha(ctx, captchaUrl, c.client)
|
||||
|
||||
switch loginType {
|
||||
case 0:
|
||||
allInputs["captchaResponse"] = captchaResult
|
||||
default:
|
||||
allInputs["captcha"] = captchaResult
|
||||
switch loginType {
|
||||
case 0:
|
||||
allInputs["captchaResponse"] = captchaResult
|
||||
default:
|
||||
allInputs["captcha"] = captchaResult
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
loginPostResponse, err := c.client.R().
|
||||
@@ -529,3 +546,12 @@ func getCasCaptchaUrl(url *url.URL, loginType int) string {
|
||||
return fmt.Sprintf("%s://%s/authserver/getCaptcha.htl", url.Scheme, url.Host)
|
||||
}
|
||||
}
|
||||
|
||||
func getCasSliderCaptchaUrl(url *url.URL) string {
|
||||
// /authserver/common/openSliderCaptcha.htl?_=1762873725346
|
||||
return fmt.Sprintf("%s://%s/authserver/common/openSliderCaptcha.htl", url.Scheme, url.Host)
|
||||
}
|
||||
|
||||
func getCasSliderVerifyUrl(url *url.URL) string {
|
||||
return fmt.Sprintf("%s://%s/authserver/common/verifySliderCaptcha.htl", url.Scheme, url.Host)
|
||||
}
|
||||
|
||||
@@ -2,71 +2,53 @@ package login
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
"likedream-goframe/internal/common/cst"
|
||||
"likedream-goframe/internal/application/recognizer/ddddocr"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"likedream-goframe/internal/utils"
|
||||
"os"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/samber/oops"
|
||||
"github.com/tidwall/gjson"
|
||||
"go.uber.org/zap"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
func ResolveOcrCaptcha(ctx context.Context, url string, client *resty.Client) string {
|
||||
captchaResponse, err := client.R().
|
||||
WithContext(ctx).
|
||||
Get(url)
|
||||
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("captchaUrl", url).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
|
||||
defer func() {
|
||||
_ = captchaResponse.Body.Close()
|
||||
}()
|
||||
|
||||
ocrUrl := g.Cfg().MustGetWithEnv(ctx, cst.ConfigOcrServerUrl).String()
|
||||
|
||||
ocrResponse, err := httpclient.Common.R().
|
||||
SetMultipartFields(&resty.MultipartField{
|
||||
Name: "file",
|
||||
FileName: "captcha.jpg",
|
||||
ContentType: captchaResponse.Header().Get("Content-Type"),
|
||||
Reader: captchaResponse.Body,
|
||||
}).
|
||||
WithContext(ctx).
|
||||
Post(ocrUrl + "/ocr")
|
||||
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("captchaUrl", url).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
|
||||
defer func() {
|
||||
_ = ocrResponse.Body.Close()
|
||||
}()
|
||||
|
||||
responseBytes, err := io.ReadAll(ocrResponse.Body)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
responseJSON := gjson.ParseBytes(responseBytes)
|
||||
|
||||
data := responseJSON.Get("data").String()
|
||||
|
||||
if data == "" {
|
||||
utils.OkOrPanic(oops.
|
||||
In("ResolveOcrCaptcha").
|
||||
With("captchaUrl", url).
|
||||
With("message", responseJSON.Get("message").String()).
|
||||
WithContext(ctx).
|
||||
New("OCR服务返回数据为空"))
|
||||
}
|
||||
|
||||
codePath := DownCode2File(ctx, url, client)
|
||||
img, _ := utils.ReadImg(codePath) //读取验证码图片
|
||||
data := ddddocr.AutoOCRVerification(img)
|
||||
return data
|
||||
}
|
||||
|
||||
func DownCode2File(ctx context.Context, url string, client *resty.Client) string {
|
||||
res, err := client.R().
|
||||
WithContext(ctx).
|
||||
Get(url)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("captchaUrl", url).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
defer res.Body.Close()
|
||||
|
||||
codeFileName := "code" + utils.GenerateRandomString(16) //生成验证码文件名称
|
||||
codeFileName += ".png"
|
||||
utils.PathExistForCreate("./assets/code/") //检测是否存在路径,如果不存在则创建
|
||||
filepath := fmt.Sprintf("./assets/code/%s", codeFileName)
|
||||
file, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
logger.Logger.Error("生成验证码失败", zap.Error(err))
|
||||
return ""
|
||||
}
|
||||
|
||||
_, err = io.Copy(file, res.Body)
|
||||
if err != nil {
|
||||
logger.Logger.Error("生成验证码失败", zap.Error(err))
|
||||
return ""
|
||||
}
|
||||
|
||||
file.Close()
|
||||
return filepath
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"likedream-goframe/internal/application/recognizer/slider"
|
||||
"likedream-goframe/internal/common/errors"
|
||||
"likedream-goframe/internal/utils"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/samber/oops"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
type CasSliderCaptchaReponse struct {
|
||||
BigImage string `json:"bigImage"`
|
||||
SmallImage string `json:"smallImage"`
|
||||
TagWidth int `json:"tagWidth"`
|
||||
YHeight int `json:"yHeight"`
|
||||
}
|
||||
|
||||
type CasSliderResult struct {
|
||||
ErrorCode int `json:"errorCode"`
|
||||
ErrorMsg string `json:"errorMsg"`
|
||||
}
|
||||
|
||||
func ResolveCasSliderCaptcha(ctx context.Context, captchaUrl string, verifyUrl string, client *resty.Client) {
|
||||
res, err := client.R().
|
||||
WithContext(ctx).
|
||||
Get(captchaUrl)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("captchaUrl", captchaUrl).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
defer res.Body.Close()
|
||||
bytes, _ := io.ReadAll(res.Body)
|
||||
var captcha CasSliderCaptchaReponse
|
||||
sonic.Unmarshal(bytes, &captcha)
|
||||
width := slider.VerifySliderWidthByBase64(captcha.BigImage, captcha.SmallImage)
|
||||
res2, err2 := client.R().
|
||||
WithContext(ctx).
|
||||
SetFormData(map[string]string{
|
||||
"canvasLength": "280",
|
||||
"moveLength": fmt.Sprintf("%d", width),
|
||||
}).
|
||||
Post(verifyUrl)
|
||||
utils.OkOrPanic(err2, func(err error) error {
|
||||
return oops.
|
||||
With("verifyUrl", verifyUrl).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
defer res2.Body.Close()
|
||||
bytes, _ = io.ReadAll(res2.Body)
|
||||
str := string(bytes)
|
||||
if len(str) > 0 {
|
||||
var result CasSliderResult
|
||||
sonic.Unmarshal(bytes, &result)
|
||||
if result.ErrorCode != 1 {
|
||||
utils.OkOrPanic(oops.WithContext(ctx).Wrap(errors.NewBusinessError(500, "滑块解析错误")))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -548,7 +548,9 @@ func (c *CpdailyAutoSignTask) FillCustomForm(ctx context.Context, form map[strin
|
||||
ticket = submitCaptchaResolver.GetCaptchaTicket(ctx, captchaInfo)
|
||||
})
|
||||
}, retry.MaxDelay(15*time.Second), retry.Attempts(5), retry.Context(ctx))
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "提交验证码识别失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
|
||||
if e != nil {
|
||||
utils.OkOrPanic(errors.NewBusinessError(503, "提交验证码识别失败").WithInnerError(e), oops.WithContext(ctx).Wrap)
|
||||
}
|
||||
|
||||
resultForm["ticket"] = gvar.New(ticket)
|
||||
} else {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,192 @@
|
||||
package slider
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"os"
|
||||
|
||||
"gocv.io/x/gocv"
|
||||
)
|
||||
|
||||
func VerifySliderWidth(bgImgPath, sliderImgPath string) int {
|
||||
// 检查背景图片文件是否存在
|
||||
if _, err := os.Stat(bgImgPath); os.IsNotExist(err) {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 检查滑块图片文件是否存在
|
||||
if _, err := os.Stat(sliderImgPath); os.IsNotExist(err) {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 读取背景图片
|
||||
bgImg := gocv.IMRead(bgImgPath, gocv.IMReadColor)
|
||||
if bgImg.Empty() {
|
||||
return -1
|
||||
}
|
||||
defer bgImg.Close()
|
||||
|
||||
// 读取滑块图片(包含 alpha 通道)
|
||||
sliderImgWithAlpha := gocv.IMRead(sliderImgPath, gocv.IMReadUnchanged)
|
||||
if sliderImgWithAlpha.Empty() {
|
||||
return -1
|
||||
}
|
||||
defer sliderImgWithAlpha.Close()
|
||||
|
||||
// 分离滑块图片的通道
|
||||
channels := gocv.Split(sliderImgWithAlpha)
|
||||
defer func() {
|
||||
for i := range channels {
|
||||
channels[i].Close()
|
||||
}
|
||||
}()
|
||||
|
||||
var sliderImg gocv.Mat
|
||||
var mask gocv.Mat
|
||||
|
||||
// 如果有 alpha 通道(4通道),提取 mask
|
||||
if len(channels) == 4 {
|
||||
// 前3个通道是 BGR
|
||||
sliderImg = gocv.NewMat()
|
||||
defer sliderImg.Close()
|
||||
gocv.Merge(channels[:3], &sliderImg)
|
||||
|
||||
// 使用 alpha 通道作为 mask
|
||||
mask = channels[3].Clone()
|
||||
defer mask.Close()
|
||||
} else if len(channels) == 3 {
|
||||
// 没有透明通道,直接使用原图
|
||||
sliderImg = sliderImgWithAlpha.Clone()
|
||||
defer sliderImg.Close()
|
||||
mask = gocv.NewMat() // 空 mask
|
||||
defer mask.Close()
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 创建结果矩阵用于存储匹配结果
|
||||
result := gocv.NewMat()
|
||||
defer result.Close()
|
||||
|
||||
// 直接在原图上使用模板匹配找到滑块位置
|
||||
if mask.Empty() {
|
||||
gocv.MatchTemplate(bgImg, sliderImg, &result, gocv.TmCcoeffNormed, gocv.NewMat())
|
||||
} else {
|
||||
// 带 mask 的模板匹配
|
||||
gocv.MatchTemplate(bgImg, sliderImg, &result, gocv.TmCcoeffNormed, mask)
|
||||
}
|
||||
|
||||
// 找到最佳匹配位置
|
||||
_, maxVal, _, maxLoc := gocv.MinMaxLoc(result)
|
||||
|
||||
// 如果匹配度太低,返回 -1 表示失败
|
||||
if maxVal < 0.6 {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 获取原图上的 x 坐标,然后按比例缩放到目标尺寸
|
||||
originalWidth := bgImg.Cols()
|
||||
targetWidth := 280
|
||||
scale := float64(targetWidth) / float64(originalWidth)
|
||||
scaledX := int(float64(maxLoc.X) * scale)
|
||||
|
||||
// 返回缩放后的 x 坐标(滑块需要移动的距离)
|
||||
return scaledX
|
||||
}
|
||||
|
||||
// VerifySliderWidthByBase64 通过 base64 编码的图片计算滑块验证码需要移动的距离
|
||||
// bgImgBase64: 背景图片的 base64 编码
|
||||
// sliderImgBase64: 滑块图片的 base64 编码
|
||||
// 返回: 滑块需要移动的 x 距离(已缩放到 280 宽度),失败返回 -1
|
||||
func VerifySliderWidthByBase64(bgImgBase64, sliderImgBase64 string) int {
|
||||
// 解码背景图片的 base64
|
||||
bgImgData, err := base64.StdEncoding.DecodeString(bgImgBase64)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 解码滑块图片的 base64
|
||||
sliderImgData, err := base64.StdEncoding.DecodeString(sliderImgBase64)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 从内存中读取背景图片
|
||||
bgImg, err := gocv.IMDecode(bgImgData, gocv.IMReadColor)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
if bgImg.Empty() {
|
||||
return -1
|
||||
}
|
||||
defer bgImg.Close()
|
||||
|
||||
// 从内存中读取滑块图片(包含 alpha 通道)
|
||||
sliderImgWithAlpha, err := gocv.IMDecode(sliderImgData, gocv.IMReadUnchanged)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
if sliderImgWithAlpha.Empty() {
|
||||
return -1
|
||||
}
|
||||
defer sliderImgWithAlpha.Close()
|
||||
|
||||
// 分离滑块图片的通道
|
||||
channels := gocv.Split(sliderImgWithAlpha)
|
||||
defer func() {
|
||||
for i := range channels {
|
||||
channels[i].Close()
|
||||
}
|
||||
}()
|
||||
|
||||
var sliderImg gocv.Mat
|
||||
var mask gocv.Mat
|
||||
|
||||
// 如果有 alpha 通道(4通道),提取 mask
|
||||
if len(channels) == 4 {
|
||||
// 前3个通道是 BGR
|
||||
sliderImg = gocv.NewMat()
|
||||
defer sliderImg.Close()
|
||||
gocv.Merge(channels[:3], &sliderImg)
|
||||
|
||||
// 使用 alpha 通道作为 mask
|
||||
mask = channels[3].Clone()
|
||||
defer mask.Close()
|
||||
} else if len(channels) == 3 {
|
||||
// 没有透明通道,直接使用原图
|
||||
sliderImg = sliderImgWithAlpha.Clone()
|
||||
defer sliderImg.Close()
|
||||
mask = gocv.NewMat() // 空 mask
|
||||
defer mask.Close()
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 创建结果矩阵用于存储匹配结果
|
||||
result := gocv.NewMat()
|
||||
defer result.Close()
|
||||
|
||||
// 直接在原图上使用模板匹配找到滑块位置
|
||||
if mask.Empty() {
|
||||
gocv.MatchTemplate(bgImg, sliderImg, &result, gocv.TmCcoeffNormed, gocv.NewMat())
|
||||
} else {
|
||||
// 带 mask 的模板匹配
|
||||
gocv.MatchTemplate(bgImg, sliderImg, &result, gocv.TmCcoeffNormed, mask)
|
||||
}
|
||||
|
||||
// 找到最佳匹配位置
|
||||
_, maxVal, _, maxLoc := gocv.MinMaxLoc(result)
|
||||
|
||||
// 如果匹配度太低,返回 -1 表示失败
|
||||
if maxVal < 0.6 {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 获取原图上的 x 坐标,然后按比例缩放到目标尺寸
|
||||
originalWidth := bgImg.Cols()
|
||||
targetWidth := 280
|
||||
scale := float64(targetWidth) / float64(originalWidth)
|
||||
scaledX := int(float64(maxLoc.X) * scale)
|
||||
|
||||
// 返回缩放后的 x 坐标(滑块需要移动的距离)
|
||||
return scaledX
|
||||
}
|
||||
Reference in New Issue
Block a user