Files
jiang13-bbs/backend/service/captcha.go

237 lines
6.4 KiB
Go
Raw 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 (
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"image"
"image/color"
"image/draw"
"image/png"
"math/big"
"strings"
"sync"
"time"
)
const (
captchaTTL = 5 * time.Minute
captchaLen = 5
captchaCharset = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" // 去掉易混 I/O/0/1
)
type captchaEntry struct {
answer string
expiresAt time.Time
}
// CaptchaStore 内存图形验证码(单实例足够;重启后未用完的码失效)
type CaptchaStore struct {
mu sync.Mutex
data map[string]captchaEntry
}
func NewCaptchaStore() *CaptchaStore {
s := &CaptchaStore{data: make(map[string]captchaEntry)}
go s.loopPurge()
return s
}
func (s *CaptchaStore) loopPurge() {
t := time.NewTicker(2 * time.Minute)
defer t.Stop()
for range t.C {
s.mu.Lock()
now := time.Now()
for id, e := range s.data {
if now.After(e.expiresAt) {
delete(s.data, id)
}
}
s.mu.Unlock()
}
}
// Issue 生成验证码,返回 id 与 PNG data URL
func (s *CaptchaStore) Issue() (id, dataURL string, err error) {
answer, err := randomCaptchaText(captchaLen)
if err != nil {
return "", "", err
}
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return "", "", err
}
id = hex.EncodeToString(b)
img, err := renderCaptchaPNG(answer)
if err != nil {
return "", "", err
}
s.mu.Lock()
s.data[id] = captchaEntry{answer: strings.ToUpper(answer), expiresAt: time.Now().Add(captchaTTL)}
s.mu.Unlock()
dataURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(img)
return id, dataURL, nil
}
// Verify 校验并立即作废(一次性)
func (s *CaptchaStore) Verify(id, answer string) bool {
id = strings.TrimSpace(id)
answer = strings.ToUpper(strings.TrimSpace(answer))
if id == "" || answer == "" {
return false
}
s.mu.Lock()
defer s.mu.Unlock()
e, ok := s.data[id]
if !ok {
return false
}
delete(s.data, id)
if time.Now().After(e.expiresAt) {
return false
}
return e.answer == answer
}
func randomCaptchaText(n int) (string, error) {
var b strings.Builder
max := big.NewInt(int64(len(captchaCharset)))
for i := 0; i < n; i++ {
v, err := rand.Int(rand.Reader, max)
if err != nil {
return "", err
}
b.WriteByte(captchaCharset[v.Int64()])
}
return b.String(), nil
}
func renderCaptchaPNG(text string) ([]byte, error) {
const w, h = 140, 44
img := image.NewRGBA(image.Rect(0, 0, w, h))
bg := color.RGBA{R: 248, G: 250, B: 252, A: 255}
draw.Draw(img, img.Bounds(), &image.Uniform{C: bg}, image.Point{}, draw.Src)
// 噪点
for i := 0; i < 180; i++ {
x, _ := rand.Int(rand.Reader, big.NewInt(w))
y, _ := rand.Int(rand.Reader, big.NewInt(h))
img.Set(int(x.Int64()), int(y.Int64()), color.RGBA{R: 180, G: 190, B: 200, A: 255})
}
// 干扰线
for i := 0; i < 4; i++ {
x0, _ := rand.Int(rand.Reader, big.NewInt(w))
y0, _ := rand.Int(rand.Reader, big.NewInt(h))
x1, _ := rand.Int(rand.Reader, big.NewInt(w))
y1, _ := rand.Int(rand.Reader, big.NewInt(h))
drawLine(img, int(x0.Int64()), int(y0.Int64()), int(x1.Int64()), int(y1.Int64()), color.RGBA{R: 160, G: 170, B: 185, A: 255})
}
fg := color.RGBA{R: 30, G: 41, B: 59, A: 255}
startX := 12
for i, ch := range text {
ox := startX + i*24
oy := 10 + int(i%2)*2
drawGlyph(img, ch, ox, oy, fg)
}
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func drawLine(img *image.RGBA, x0, y0, x1, y1 int, c color.Color) {
dx := abs(x1 - x0)
dy := -abs(y1 - y0)
sx, sy := 1, 1
if x0 >= x1 {
sx = -1
}
if y0 >= y1 {
sy = -1
}
err := dx + dy
for {
img.Set(x0, y0, c)
if x0 == x1 && y0 == y1 {
break
}
e2 := 2 * err
if e2 >= dy {
err += dy
x0 += sx
}
if e2 <= dx {
err += dx
y0 += sy
}
}
}
func abs(v int) int {
if v < 0 {
return -v
}
return v
}
// 简易 5×7 点阵(仅验证码字符集)
var glyphBits = map[rune][7]string{
'A': {"01110", "10001", "10001", "11111", "10001", "10001", "10001"},
'B': {"11110", "10001", "10001", "11110", "10001", "10001", "11110"},
'C': {"01111", "10000", "10000", "10000", "10000", "10000", "01111"},
'D': {"11110", "10001", "10001", "10001", "10001", "10001", "11110"},
'E': {"11111", "10000", "10000", "11110", "10000", "10000", "11111"},
'F': {"11111", "10000", "10000", "11110", "10000", "10000", "10000"},
'G': {"01111", "10000", "10000", "10011", "10001", "10001", "01111"},
'H': {"10001", "10001", "10001", "11111", "10001", "10001", "10001"},
'J': {"00111", "00010", "00010", "00010", "00010", "10010", "01100"},
'K': {"10001", "10010", "10100", "11000", "10100", "10010", "10001"},
'L': {"10000", "10000", "10000", "10000", "10000", "10000", "11111"},
'M': {"10001", "11011", "10101", "10001", "10001", "10001", "10001"},
'N': {"10001", "11001", "10101", "10011", "10001", "10001", "10001"},
'P': {"11110", "10001", "10001", "11110", "10000", "10000", "10000"},
'Q': {"01110", "10001", "10001", "10001", "10101", "10010", "01101"},
'R': {"11110", "10001", "10001", "11110", "10100", "10010", "10001"},
'S': {"01111", "10000", "10000", "01110", "00001", "00001", "11110"},
'T': {"11111", "00100", "00100", "00100", "00100", "00100", "00100"},
'U': {"10001", "10001", "10001", "10001", "10001", "10001", "01110"},
'V': {"10001", "10001", "10001", "10001", "10001", "01010", "00100"},
'W': {"10001", "10001", "10001", "10001", "10101", "10101", "01010"},
'X': {"10001", "10001", "01010", "00100", "01010", "10001", "10001"},
'Y': {"10001", "10001", "01010", "00100", "00100", "00100", "00100"},
'Z': {"11111", "00001", "00010", "00100", "01000", "10000", "11111"},
'2': {"01110", "10001", "00001", "00010", "00100", "01000", "11111"},
'3': {"11110", "00001", "00001", "01110", "00001", "00001", "11110"},
'4': {"00010", "00110", "01010", "10010", "11111", "00010", "00010"},
'5': {"11111", "10000", "11110", "00001", "00001", "10001", "01110"},
'6': {"01110", "10000", "10000", "11110", "10001", "10001", "01110"},
'7': {"11111", "00001", "00010", "00100", "01000", "01000", "01000"},
'8': {"01110", "10001", "10001", "01110", "10001", "10001", "01110"},
'9': {"01110", "10001", "10001", "01111", "00001", "00001", "01110"},
}
func drawGlyph(img *image.RGBA, ch rune, ox, oy int, c color.Color) {
bits, ok := glyphBits[ch]
if !ok {
return
}
scale := 2
for row, line := range bits {
for col, cell := range line {
if cell != '1' {
continue
}
for dy := 0; dy < scale; dy++ {
for dx := 0; dx < scale; dx++ {
img.Set(ox+col*scale+dx, oy+row*scale+dy, c)
}
}
}
}
}