mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 08:03:55 +08:00
refactor: replace freetype captcha with opentype renderer (#13753)
This commit is contained in:
@@ -3,7 +3,6 @@ module github.com/1Panel-dev/1Panel/core
|
||||
go 1.26.1
|
||||
|
||||
require (
|
||||
github.com/1panel-dev/base64Captcha v1.3.8
|
||||
github.com/beevik/etree v1.7.0
|
||||
github.com/coreos/go-oidc/v3 v3.20.0
|
||||
github.com/creack/pty v1.1.24
|
||||
@@ -40,6 +39,7 @@ require (
|
||||
github.com/swaggo/swag v1.16.6
|
||||
github.com/xlzd/gotp v0.1.0
|
||||
golang.org/x/crypto v0.55.0
|
||||
golang.org/x/image v0.41.0
|
||||
golang.org/x/net v0.58.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sys v0.47.0
|
||||
@@ -86,7 +86,6 @@ require (
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/hhrutter/lzw v1.0.0 // indirect
|
||||
github.com/hhrutter/pkcs7 v0.2.0 // indirect
|
||||
@@ -129,7 +128,6 @@ require (
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.1 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/arch v0.26.0 // indirect
|
||||
golang.org/x/image v0.41.0 // indirect
|
||||
golang.org/x/mod v0.38.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/tools v0.48.0 // indirect
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
github.com/1panel-dev/base64Captcha v1.3.8 h1:GbQ2IuGMp4ai4erpwf4BMjm5eLC8Efb+dATVwgpPIII=
|
||||
github.com/1panel-dev/base64Captcha v1.3.8/go.mod h1:gVpwyGm9+g4rg3pXdYnsFAouP73qMOSBfnT3bxCFzco=
|
||||
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
|
||||
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
@@ -118,8 +116,6 @@ github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXe
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/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.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
|
||||
@@ -1,55 +1,68 @@
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/core/app/dto"
|
||||
"github.com/1panel-dev/base64Captcha"
|
||||
)
|
||||
|
||||
var store = base64Captcha.DefaultMemStore
|
||||
|
||||
const (
|
||||
captchaWidth = 160
|
||||
captchaHeight = 48
|
||||
captchaNoise = 0
|
||||
captchaWidth = 160
|
||||
captchaHeight = 48
|
||||
captchaExpiration = 10 * time.Minute
|
||||
captchaStoreLimit = 10000
|
||||
)
|
||||
|
||||
var captchaFonts = []string{
|
||||
base64Captcha.FontRitaSmith,
|
||||
}
|
||||
var store = newMemoryStore(captchaStoreLimit, captchaExpiration)
|
||||
|
||||
func VerifyCode(codeID string, code string) string {
|
||||
vv := store.Get(codeID, true)
|
||||
vv = strings.TrimSpace(vv)
|
||||
answer := store.consume(codeID)
|
||||
code = strings.TrimSpace(code)
|
||||
if codeID == "" || code == "" {
|
||||
if codeID == "" || code == "" || answer == "" || answer != code {
|
||||
return "ErrCaptchaCode"
|
||||
}
|
||||
if strings.EqualFold(vv, code) {
|
||||
return ""
|
||||
}
|
||||
return "ErrCaptchaCode"
|
||||
return ""
|
||||
}
|
||||
|
||||
func CreateCaptcha() (*dto.CaptchaResponse, error) {
|
||||
driver := base64Captcha.NewDriverMath(
|
||||
captchaHeight,
|
||||
captchaWidth,
|
||||
captchaNoise,
|
||||
0,
|
||||
&color.RGBA{R: 246, G: 248, B: 251, A: 255},
|
||||
nil,
|
||||
captchaFonts,
|
||||
)
|
||||
c := base64Captcha.NewCaptcha(driver, store)
|
||||
id, b64s, _, err := c.Generate()
|
||||
question, answer, err := generateQuestion()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.CaptchaResponse{
|
||||
CaptchaID: id,
|
||||
ImagePath: b64s,
|
||||
}, nil
|
||||
imagePath, err := renderQuestion(question)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id := rand.Text()
|
||||
if err := store.put(id, answer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.CaptchaResponse{CaptchaID: id, ImagePath: imagePath}, nil
|
||||
}
|
||||
|
||||
func generateQuestion() (question, answer string, err error) {
|
||||
limits := [...]int64{3, 80, 20, 80}
|
||||
var values [4]int
|
||||
for i, limit := range limits {
|
||||
n, err := rand.Int(rand.Reader, big.NewInt(limit))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("generate captcha: %w", err)
|
||||
}
|
||||
values[i] = int(n.Int64())
|
||||
}
|
||||
switch values[0] {
|
||||
case 0:
|
||||
a, b := values[1]%20, values[2]
|
||||
return fmt.Sprintf("%d+%d=?", a, b), fmt.Sprint(a + b), nil
|
||||
case 1:
|
||||
a, b := values[1]+values[2], values[3]
|
||||
return fmt.Sprintf("%d-%d=?", a, b), fmt.Sprint(a - b), nil
|
||||
default:
|
||||
a, b := values[1]%10, values[2]%10
|
||||
return fmt.Sprintf("%dx%d=?", a, b), fmt.Sprint(a * b), nil
|
||||
}
|
||||
}
|
||||
|
||||
62
core/utils/captcha/image.go
Normal file
62
core/utils/captcha/image.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"math/rand/v2"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/font/gofont/gobold"
|
||||
"golang.org/x/image/font/opentype"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
var loadFont = sync.OnceValues(func() (*opentype.Font, error) {
|
||||
return opentype.Parse(gobold.TTF)
|
||||
})
|
||||
|
||||
func renderQuestion(question string) (string, error) {
|
||||
f, err := loadFont()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
dst := image.NewRGBA(image.Rect(0, 0, captchaWidth, captchaHeight))
|
||||
background := image.NewUniform(color.RGBA{R: 246, G: 248, B: 251, A: 255})
|
||||
draw.Draw(dst, dst.Bounds(), background, image.Point{}, draw.Src)
|
||||
chars := []rune(question)
|
||||
cellWidth := (captchaWidth - 12) / len(chars)
|
||||
for i, char := range chars {
|
||||
face, err := opentype.NewFace(f, &opentype.FaceOptions{
|
||||
Size: float64(24 + rand.IntN(5)), DPI: 72, Hinting: font.HintingFull,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
bounds, _ := font.BoundString(face, string(char))
|
||||
x := fixed.I(6+i*cellWidth+cellWidth/2) - (bounds.Min.X+bounds.Max.X)/2
|
||||
y := fixed.I(captchaHeight/2+rand.IntN(7)-3) - (bounds.Min.Y+bounds.Max.Y)/2
|
||||
d := font.Drawer{
|
||||
Dst: dst,
|
||||
Src: image.NewUniform(color.RGBA{
|
||||
R: uint8(20 + rand.IntN(90)), G: uint8(20 + rand.IntN(90)),
|
||||
B: uint8(20 + rand.IntN(90)), A: 255,
|
||||
}),
|
||||
Face: face,
|
||||
Dot: fixed.Point26_6{X: x, Y: y},
|
||||
}
|
||||
d.DrawString(string(char))
|
||||
if err := face.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, dst); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes()), nil
|
||||
}
|
||||
76
core/utils/captcha/store.go
Normal file
76
core/utils/captcha/store.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type challenge struct {
|
||||
id string
|
||||
answer string
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
type memoryStore struct {
|
||||
mu sync.Mutex
|
||||
entries map[string]*list.Element
|
||||
order *list.List
|
||||
limit int
|
||||
expiration time.Duration
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func newMemoryStore(limit int, expiration time.Duration) *memoryStore {
|
||||
return &memoryStore{
|
||||
entries: make(map[string]*list.Element), order: list.New(),
|
||||
limit: limit, expiration: expiration, now: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *memoryStore) put(id, answer string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
now := s.now()
|
||||
s.removeExpired(now)
|
||||
if len(s.entries) >= s.limit {
|
||||
return errors.New("captcha store is full")
|
||||
}
|
||||
if _, exists := s.entries[id]; exists {
|
||||
return errors.New("captcha ID already exists")
|
||||
}
|
||||
s.entries[id] = s.order.PushBack(challenge{id: id, answer: answer, expiresAt: now.Add(s.expiration)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *memoryStore) consume(id string) string {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
now := s.now()
|
||||
s.removeExpired(now)
|
||||
e, ok := s.entries[id]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
value := e.Value.(challenge)
|
||||
s.remove(e)
|
||||
if !now.Before(value.expiresAt) {
|
||||
return ""
|
||||
}
|
||||
return value.answer
|
||||
}
|
||||
|
||||
func (s *memoryStore) removeExpired(now time.Time) {
|
||||
for e := s.order.Front(); e != nil; e = s.order.Front() {
|
||||
if now.Before(e.Value.(challenge).expiresAt) {
|
||||
return
|
||||
}
|
||||
s.remove(e)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *memoryStore) remove(e *list.Element) {
|
||||
delete(s.entries, e.Value.(challenge).id)
|
||||
s.order.Remove(e)
|
||||
}
|
||||
Reference in New Issue
Block a user