|
|
|
|
@ -2,6 +2,7 @@ package captcha
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
@ -17,10 +18,17 @@ import (
|
|
|
|
|
"go.mongodb.org/mongo-driver/mongo/options"
|
|
|
|
|
"google.golang.org/grpc"
|
|
|
|
|
|
|
|
|
|
"github.com/wenlng/go-captcha/v2/base/option"
|
|
|
|
|
"github.com/wenlng/go-captcha/v2/slide"
|
|
|
|
|
"github.com/wenlng/go-captcha/v2/click"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// alphanumChars is the character pool for the click captcha.
|
|
|
|
|
// Visually ambiguous characters (I, O, 0, 1, l) are excluded.
|
|
|
|
|
var alphanumChars = []string{
|
|
|
|
|
"A", "B", "C", "D", "E", "F", "G", "H", "J", "K",
|
|
|
|
|
"L", "M", "N", "P", "Q", "R", "S", "T", "U", "V",
|
|
|
|
|
"W", "X", "Y", "Z", "2", "3", "4", "5", "6", "7", "8", "9",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Config struct {
|
|
|
|
|
RpcConfig config.Captcha
|
|
|
|
|
MongodbConfig config.Mongo
|
|
|
|
|
@ -31,14 +39,14 @@ type Config struct {
|
|
|
|
|
type server struct {
|
|
|
|
|
pbcaptcha.UnimplementedCaptchaServer
|
|
|
|
|
conf config.Captcha
|
|
|
|
|
capt slide.Captcha
|
|
|
|
|
capt click.Captcha
|
|
|
|
|
collection *mongo.Collection
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// captchaDoc is the MongoDB document that stores the verification answer.
|
|
|
|
|
type captchaDoc struct {
|
|
|
|
|
CaptchaID string `bson:"captcha_id"`
|
|
|
|
|
X int `bson:"x"`
|
|
|
|
|
Y int `bson:"y"`
|
|
|
|
|
DotsJSON string `bson:"dots_json"` // JSON-encoded map[int]*click.Dot (answer dots)
|
|
|
|
|
ExpiredAt time.Time `bson:"expired_at"`
|
|
|
|
|
CreateTime time.Time `bson:"create_time"`
|
|
|
|
|
VerifyTime time.Time `bson:"verify_time,omitempty"`
|
|
|
|
|
@ -66,17 +74,15 @@ func Start(ctx context.Context, cfg *Config, _ discovery.SvcDiscoveryRegistry, g
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resources, err := loadResources()
|
|
|
|
|
capt, err := buildClickCaptcha()
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.ZError(ctx, "captcha load resources failed", err)
|
|
|
|
|
log.ZError(ctx, "captcha build click captcha failed", err)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
builder := slide.NewBuilder()
|
|
|
|
|
builder.SetResources(resources...)
|
|
|
|
|
s := &server{
|
|
|
|
|
conf: cfg.RpcConfig,
|
|
|
|
|
capt: builder.Make(),
|
|
|
|
|
capt: capt,
|
|
|
|
|
collection: collection,
|
|
|
|
|
}
|
|
|
|
|
if s.conf.ExpireSeconds <= 0 {
|
|
|
|
|
@ -95,24 +101,31 @@ func (s *server) GenerateCaptcha(ctx context.Context, _ *pbcaptcha.GenerateCaptc
|
|
|
|
|
log.ZError(ctx, "captcha generate failed", err)
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
block := captData.GetData()
|
|
|
|
|
masterImage, err := captData.GetMasterImage().ToBase64DataWithQuality(option.QualityNone)
|
|
|
|
|
|
|
|
|
|
dots := captData.GetData() // answer dots: map[int]*click.Dot
|
|
|
|
|
masterImage, err := captData.GetMasterImage().ToBase64DataWithQuality(0)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.ZError(ctx, "captcha encode master image failed", err)
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
tileImage, err := captData.GetTileImage().ToBase64Data()
|
|
|
|
|
thumbImage, err := captData.GetThumbImage().ToBase64Data()
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.ZError(ctx, "captcha encode tile image failed", err)
|
|
|
|
|
log.ZError(ctx, "captcha encode thumb image failed", err)
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dotsJSON, err := json.Marshal(dots)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.ZError(ctx, "captcha marshal dots failed", err)
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
id := uuid.NewString()
|
|
|
|
|
now := time.Now()
|
|
|
|
|
expiredAt := now.Add(time.Duration(s.conf.ExpireSeconds) * time.Second)
|
|
|
|
|
_, err = s.collection.InsertOne(ctx, captchaDoc{
|
|
|
|
|
CaptchaID: id,
|
|
|
|
|
X: block.X,
|
|
|
|
|
Y: block.Y,
|
|
|
|
|
DotsJSON: string(dotsJSON),
|
|
|
|
|
ExpiredAt: expiredAt,
|
|
|
|
|
CreateTime: now,
|
|
|
|
|
})
|
|
|
|
|
@ -120,26 +133,26 @@ func (s *server) GenerateCaptcha(ctx context.Context, _ *pbcaptcha.GenerateCaptc
|
|
|
|
|
log.ZError(ctx, "captcha insert mongodb failed", err, "captchaID", id)
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.ZDebug(ctx, "captcha generated", "captchaID", id, "dotCount", len(dots), "expireAt", expiredAt.Unix())
|
|
|
|
|
return &pbcaptcha.GenerateCaptchaResp{
|
|
|
|
|
CaptchaID: id,
|
|
|
|
|
MasterImage: masterImage,
|
|
|
|
|
TileImage: tileImage,
|
|
|
|
|
TileY: int32(block.DY),
|
|
|
|
|
ThumbImage: thumbImage,
|
|
|
|
|
ExpireAt: expiredAt.Unix(),
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *server) VerifyCaptcha(ctx context.Context, req *pbcaptcha.VerifyCaptchaReq) (*pbcaptcha.VerifyCaptchaResp, error) {
|
|
|
|
|
log.ZDebug(ctx, "captcha verify request", "captchaID", req.CaptchaID, "clickCount", len(req.ClickPoints))
|
|
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
filter := bson.M{
|
|
|
|
|
"captcha_id": req.CaptchaID,
|
|
|
|
|
"verify_time": bson.M{"$exists": false},
|
|
|
|
|
}
|
|
|
|
|
update := bson.M{
|
|
|
|
|
"$set": bson.M{
|
|
|
|
|
"verify_time": now,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
update := bson.M{"$set": bson.M{"verify_time": now}}
|
|
|
|
|
|
|
|
|
|
var doc captchaDoc
|
|
|
|
|
err := s.collection.FindOneAndUpdate(
|
|
|
|
|
ctx,
|
|
|
|
|
@ -159,9 +172,41 @@ func (s *server) VerifyCaptcha(ctx context.Context, req *pbcaptcha.VerifyCaptcha
|
|
|
|
|
log.ZWarn(ctx, "captcha expired", nil, "captchaID", req.CaptchaID, "expiredAt", doc.ExpiredAt.Unix())
|
|
|
|
|
return nil, servererrs.ErrFileUploadedExpired.WrapMsg("captcha expired", "captchaID", req.CaptchaID)
|
|
|
|
|
}
|
|
|
|
|
success := slide.Validate(int(req.X), int(req.Y), doc.X, doc.Y, s.conf.VerifyPadding)
|
|
|
|
|
|
|
|
|
|
// Unmarshal the stored answer dots.
|
|
|
|
|
var answerDots map[int]*click.Dot
|
|
|
|
|
if err := json.Unmarshal([]byte(doc.DotsJSON), &answerDots); err != nil {
|
|
|
|
|
log.ZError(ctx, "captcha unmarshal dots failed", err, "captchaID", req.CaptchaID)
|
|
|
|
|
return nil, servererrs.ErrDatabase.WrapMsg("internal captcha data error")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
success := validateClickPoints(req.ClickPoints, answerDots, s.conf.VerifyPadding)
|
|
|
|
|
if !success {
|
|
|
|
|
log.ZError(ctx, "captcha validate failed", nil, "captchaID", req.CaptchaID, "x", req.X, "y", req.Y, "docX", doc.X, "docY", doc.Y)
|
|
|
|
|
log.ZError(ctx, "captcha validate failed", nil,
|
|
|
|
|
"captchaID", req.CaptchaID,
|
|
|
|
|
"clickCount", len(req.ClickPoints),
|
|
|
|
|
"answerCount", len(answerDots),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
log.ZDebug(ctx, "captcha validate success", "captchaID", req.CaptchaID)
|
|
|
|
|
}
|
|
|
|
|
return &pbcaptcha.VerifyCaptchaResp{Success: success}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// validateClickPoints checks that each user click point falls within the
|
|
|
|
|
// bounding box of the corresponding answer dot (in order).
|
|
|
|
|
func validateClickPoints(points []*pbcaptcha.ClickPoint, dots map[int]*click.Dot, padding int) bool {
|
|
|
|
|
if len(points) != len(dots) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for i, pt := range points {
|
|
|
|
|
dot, ok := dots[i]
|
|
|
|
|
if !ok {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if !click.Validate(int(pt.X), int(pt.Y), dot.X, dot.Y, dot.Width, dot.Height, padding) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|