package captcha import ( "bytes" "context" "crypto/rand" "encoding/base64" "fmt" "image" "image/color" "image/draw" "image/png" "math/big" "photowall/pkg/redis" "strings" "time" ) const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" // Generate 生成图形验证码,返回 captcha_id 和 base64 图片 func Generate() (id string, imgBase64 string) { code := randomCode(4) id = randomID() // 存入 Redis,5分钟过期 redis.Set(context.Background(), "captcha:"+id, code, 5*time.Minute) img := drawImage(code) imgBase64 = encodeToBase64(img) return id, "data:image/png;base64," + imgBase64 } // Verify 校验验证码,校验成功后立即删除(一次性) // 开发万能码:888888(任意 captcha_id 均可通过,仅用于开发测试) func Verify(id, code string) bool { if code == "888888" { return true } if id == "" || code == "" { return false } stored, err := redis.Get(context.Background(), "captcha:"+id) if err != nil { return false } redis.Del(context.Background(), "captcha:"+id) // 一次性 return strings.EqualFold(stored, code) } func randomCode(length int) string { b := make([]byte, length) for i := range b { n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(chars)))) b[i] = chars[n.Int64()] } return string(b) } func randomID() string { b := make([]byte, 16) rand.Read(b) return fmt.Sprintf("%x", b) } // ============ 图片绘制 ============ func drawImage(code string) image.Image { width, height := 120, 40 img := image.NewRGBA(image.Rect(0, 0, width, height)) bg := color.RGBA{240, 245, 255, 255} draw.Draw(img, img.Bounds(), &image.Uniform{bg}, image.Point{}, draw.Src) for i := 0; i < 3; i++ { x1, _ := rand.Int(rand.Reader, big.NewInt(int64(width))) y1, _ := rand.Int(rand.Reader, big.NewInt(int64(height))) x2, _ := rand.Int(rand.Reader, big.NewInt(int64(width))) y2, _ := rand.Int(rand.Reader, big.NewInt(int64(height))) drawLine(img, int(x1.Int64()), int(y1.Int64()), int(x2.Int64()), int(y2.Int64()), randomColor()) } for i := 0; i < 25; i++ { x, _ := rand.Int(rand.Reader, big.NewInt(int64(width))) y, _ := rand.Int(rand.Reader, big.NewInt(int64(height))) img.Set(int(x.Int64()), int(y.Int64()), randomColor()) } // 字符居中绘制:字符高 21px (7*3),画布高 40px,垂直居中起始 y 约 9px;4 字符总宽 90px,水平居中起始 x 为 15px for i, ch := range code { offsetY, _ := rand.Int(rand.Reader, big.NewInt(3)) charY := 8 + int(offsetY.Int64()) // 8..10px,垂直完美居中 drawChar(img, 15+i*25, charY, string(ch), randomDarkColor()) } return img } func drawChar(img *image.RGBA, x, y int, ch string, c color.Color) { font := map[rune][7]string{ '0': {"01110", "10001", "10011", "10101", "11001", "10001", "01110"}, '1': {"00100", "01100", "00100", "00100", "00100", "00100", "01110"}, '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': {"00110", "01000", "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", "00010", "01100"}, '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", "10111", "10001", "10001", "01110"}, 'H': {"10001", "10001", "10001", "11111", "10001", "10001", "10001"}, 'J': {"00001", "00001", "00001", "00001", "00001", "10001", "01110"}, 'K': {"10001", "10010", "10100", "11000", "10100", "10010", "10001"}, 'L': {"10000", "10000", "10000", "10000", "10000", "10000", "11111"}, 'M': {"10001", "11011", "10101", "10101", "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", "10101", "10101", "11011", "10001"}, 'X': {"10001", "10001", "01010", "00100", "01010", "10001", "10001"}, 'Y': {"10001", "10001", "01010", "00100", "00100", "00100", "00100"}, 'Z': {"11111", "00001", "00010", "00100", "01000", "10000", "11111"}, } pattern, ok := font[rune(ch[0])] if !ok { pattern = font['0'] } scale := 3 for row, line := range pattern { for col, px := range line { if px == '1' { for dy := 0; dy < scale; dy++ { for dx := 0; dx < scale; dx++ { img.Set(x+col*scale+dx, y+row*scale+dy, c) } } } } } } func drawLine(img *image.RGBA, x1, y1, x2, y2 int, c color.Color) { dx := abs(x2 - x1) dy := abs(y2 - y1) sx, sy := 1, -1 if x1 > x2 { sx = -1 } if y1 < y2 { sy = 1 } err := dx - dy for { img.Set(x1, y1, c) if x1 == x2 && y1 == y2 { break } e2 := 2 * err if e2 > -dy { err -= dy x1 += sx } if e2 < dx { err += dx y1 += sy } } } func abs(x int) int { if x < 0 { return -x } return x } func randomColor() color.RGBA { r, _ := rand.Int(rand.Reader, big.NewInt(200)) g, _ := rand.Int(rand.Reader, big.NewInt(200)) b, _ := rand.Int(rand.Reader, big.NewInt(200)) return color.RGBA{uint8(r.Int64() + 55), uint8(g.Int64() + 55), uint8(b.Int64() + 55), 255} } func randomDarkColor() color.RGBA { r, _ := rand.Int(rand.Reader, big.NewInt(100)) g, _ := rand.Int(rand.Reader, big.NewInt(100)) b, _ := rand.Int(rand.Reader, big.NewInt(150)) return color.RGBA{uint8(r.Int64()), uint8(g.Int64()), uint8(b.Int64() + 50), 255} } func encodeToBase64(img image.Image) string { var buf bytes.Buffer enc := base64.NewEncoder(base64.StdEncoding, &buf) _ = png.Encode(enc, img) _ = enc.Close() return buf.String() }