204 lines
4.1 KiB
Go
204 lines
4.1 KiB
Go
package service
|
||
|
||
import (
|
||
"image"
|
||
"image/color"
|
||
"image/draw"
|
||
"image/png"
|
||
"math/rand"
|
||
"bytes"
|
||
"encoding/base64"
|
||
"fmt"
|
||
"sync"
|
||
"time"
|
||
|
||
"golang.org/x/image/font"
|
||
"golang.org/x/image/font/basicfont"
|
||
"golang.org/x/image/math/fixed"
|
||
)
|
||
|
||
// CaptchaItem 表示一个验证码条目
|
||
type CaptchaItem struct {
|
||
ID string
|
||
Answer string
|
||
ExpiresAt time.Time
|
||
}
|
||
|
||
var (
|
||
captchaStore = sync.Map{} // map[string]*CaptchaItem
|
||
captchaCleaner sync.Once
|
||
)
|
||
|
||
func initCaptchaCleaner() {
|
||
go func() {
|
||
for {
|
||
time.Sleep(5 * time.Minute)
|
||
now := time.Now()
|
||
captchaStore.Range(func(key, value interface{}) bool {
|
||
item := value.(*CaptchaItem)
|
||
if now.After(item.ExpiresAt) {
|
||
captchaStore.Delete(key)
|
||
}
|
||
return true
|
||
})
|
||
}
|
||
}()
|
||
}
|
||
|
||
// GenerateMathCaptcha 生成一个数学验证码,返回 id、问题表达式、base64 PNG 图片
|
||
func GenerateMathCaptcha() (id string, question string, base64PNG string, err error) {
|
||
captchaCleaner.Do(initCaptchaCleaner)
|
||
|
||
// 生成随机数学题
|
||
a := rand.Intn(20) + 1 // 1-20
|
||
b := rand.Intn(20) + 1 // 1-20
|
||
op := rand.Intn(2) // 0: +, 1: -
|
||
var answer int
|
||
var opStr string
|
||
if op == 0 {
|
||
answer = a + b
|
||
opStr = "+"
|
||
} else {
|
||
// 确保结果非负
|
||
if a < b {
|
||
a, b = b, a
|
||
}
|
||
answer = a - b
|
||
opStr = "-"
|
||
}
|
||
|
||
question = fmt.Sprintf("%d %s %d = ?", a, opStr, b)
|
||
answerStr := fmt.Sprintf("%d", answer)
|
||
|
||
img, err := drawCaptchaImage(question)
|
||
if err != nil {
|
||
return "", "", "", err
|
||
}
|
||
|
||
var buf bytes.Buffer
|
||
if err := png.Encode(&buf, img); err != nil {
|
||
return "", "", "", err
|
||
}
|
||
|
||
base64PNG = "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes())
|
||
|
||
// 生成唯一 ID 并存储
|
||
id = fmt.Sprintf("%d%06d", time.Now().UnixNano(), rand.Intn(1000000))
|
||
captchaStore.Store(id, &CaptchaItem{
|
||
ID: id,
|
||
Answer: answerStr,
|
||
ExpiresAt: time.Now().Add(5 * time.Minute),
|
||
})
|
||
|
||
return id, question, base64PNG, nil
|
||
}
|
||
|
||
// VerifyCaptcha 验证验证码,验证后立即使其失效
|
||
func VerifyCaptcha(id string, answer string) bool {
|
||
if id == "" || answer == "" {
|
||
return false
|
||
}
|
||
val, ok := captchaStore.LoadAndDelete(id)
|
||
if !ok {
|
||
return false
|
||
}
|
||
item := val.(*CaptchaItem)
|
||
if time.Now().After(item.ExpiresAt) {
|
||
return false
|
||
}
|
||
return item.Answer == answer
|
||
}
|
||
|
||
// drawCaptchaImage 将验证码文字绘制为带噪点的图片
|
||
func drawCaptchaImage(text string) (*image.RGBA, error) {
|
||
width := 160
|
||
height := 50
|
||
|
||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||
|
||
// 随机浅色背景
|
||
bgR := uint8(220 + rand.Intn(36))
|
||
bgG := uint8(220 + rand.Intn(36))
|
||
bgB := uint8(220 + rand.Intn(36))
|
||
bgColor := color.RGBA{bgR, bgG, bgB, 255}
|
||
draw.Draw(img, img.Bounds(), &image.Uniform{bgColor}, image.Point{}, draw.Src)
|
||
|
||
// 绘制干扰线
|
||
for i := 0; i < 4; i++ {
|
||
x1 := rand.Intn(width)
|
||
y1 := rand.Intn(height)
|
||
x2 := rand.Intn(width)
|
||
y2 := rand.Intn(height)
|
||
lineColor := color.RGBA{
|
||
uint8(rand.Intn(150)),
|
||
uint8(rand.Intn(150)),
|
||
uint8(rand.Intn(150)),
|
||
255,
|
||
}
|
||
drawLine(img, x1, y1, x2, y2, lineColor)
|
||
}
|
||
|
||
// 绘制干扰点
|
||
for i := 0; i < 60; i++ {
|
||
x := rand.Intn(width)
|
||
y := rand.Intn(height)
|
||
dotColor := color.RGBA{
|
||
uint8(rand.Intn(200)),
|
||
uint8(rand.Intn(200)),
|
||
uint8(rand.Intn(200)),
|
||
255,
|
||
}
|
||
img.Set(x, y, dotColor)
|
||
}
|
||
|
||
// 绘制文字
|
||
face := basicfont.Face7x13
|
||
d := &font.Drawer{
|
||
Dst: img,
|
||
Src: &image.Uniform{color.RGBA{50, 50, 50, 255}},
|
||
Face: face,
|
||
}
|
||
|
||
// 计算文字宽度以居中
|
||
textWidth := font.MeasureString(face, text).Ceil()
|
||
startX := (width - textWidth) / 2
|
||
if startX < 5 {
|
||
startX = 5
|
||
}
|
||
|
||
d.Dot = fixed.P(startX, 30)
|
||
d.DrawString(text)
|
||
|
||
return img, nil
|
||
}
|
||
|
||
// drawLine 在图像上绘制一条线段(Bresenham 算法简化版)
|
||
func drawLine(img *image.RGBA, x1, y1, x2, y2 int, c color.RGBA) {
|
||
dx := abs(x2 - x1)
|
||
dy := abs(y2 - y1)
|
||
steps := dx
|
||
if dy > dx {
|
||
steps = dy
|
||
}
|
||
if steps == 0 {
|
||
img.Set(x1, y1, c)
|
||
return
|
||
}
|
||
for i := 0; i <= steps; i++ {
|
||
x := x1 + (x2-x1)*i/steps
|
||
y := y1 + (y2-y1)*i/steps
|
||
// 线宽为 2
|
||
img.Set(x, y, c)
|
||
if x+1 < img.Bounds().Max.X {
|
||
img.Set(x+1, y, c)
|
||
}
|
||
}
|
||
}
|
||
|
||
func abs(x int) int {
|
||
if x < 0 {
|
||
return -x
|
||
}
|
||
return x
|
||
}
|