tokenFactory/service/captcha.go

204 lines
4.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}