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) } } } } }