go语言,彩色验证码生成,加减法验证,

发布于:2025-09-13 ⋅ 阅读:(21) ⋅ 点赞:(0)

代码结构

在这里插入图片描述

相关代码

captcha/internal/captcha/generator.go

package captcha

import (
	_ "embed" // 👈 启用 embed
	"image"
	"image/color"
	"image/draw"
	"image/png"
	"io"
	"math/rand"

	"golang.org/x/image/font"
	"golang.org/x/image/font/opentype"
	"golang.org/x/image/math/fixed"
)

// 👇 嵌入字体文件
//
//go:embed font/font.ttf
var fontBytes []byte

var (
	white = color.RGBA{255, 255, 255, 255}
	black = color.RGBA{0, 0, 0, 255}
)

func randomColor() color.Color {
	return color.RGBA{
		R: uint8(rand.Intn(256)),
		G: uint8(rand.Intn(256)),
		B: uint8(rand.Intn(256)),
		A: 255,
	}
}

// CaptchaImage 生成验证码图片并写入 io.Writer
func CaptchaImage(question string, w io.Writer) error {
	initRand()

	const (
		width  = 150
		height = 70
	)

	img := image.NewRGBA(image.Rect(0, 0, width, height))
	draw.Draw(img, img.Bounds(), &image.Uniform{white}, image.Point{}, draw.Src)

	// 画干扰线
	for i := 0; i < 5; i++ {
		drawLine(img, rand.Intn(width), rand.Intn(height), rand.Intn(width), rand.Intn(height), randomColor())
	}

	// 画噪点
	for i := 0; i < 50; i++ {
		img.Set(rand.Intn(width), rand.Intn(height), black)
	}

	// 画文字(使用 freetype 渲染)
	if err := drawTextWithFreetype(img, question, 10, 60, randomColor()); err != nil {
		return err
	}

	return png.Encode(w, img)
}

func drawLine(img *image.RGBA, x1, y1, x2, y2 int, c color.Color) {
	dx := abs(x2 - x1)
	dy := abs(y2 - y1)
	var sx, sy int
	if x1 < x2 {
		sx = 1
	} else {
		sx = -1
	}
	if y1 < y2 {
		sy = 1
	} else {
		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 drawTextWithFreetype(img *image.RGBA, text string, x, y int, _ color.Color) error {
	fontParsed, err := opentype.Parse(fontBytes)
	if err != nil {
		return err
	}

	face, err := opentype.NewFace(fontParsed, &opentype.FaceOptions{
		Size:    32,
		DPI:     72,
		Hinting: font.HintingNone,
	})
	if err != nil {
		return err
	}

	currentX := x
	for _, char := range text {
		// 为每个字符生成随机颜色
		charColor := randomColor()

		d := &font.Drawer{
			Dst:  img,
			Src:  image.NewUniform(charColor), // 👈 每个字符独立颜色
			Face: face,
			Dot:  fixed.P(currentX, y),
		}
		d.DrawString(string(char))

		// 手动计算字符宽度(简单估算,或使用 font.Measure)
		bounds, _ := font.BoundString(face, string(char))
		advance := (bounds.Max.X - bounds.Min.X).Ceil()
		currentX += advance + 2 // +2 为字符间距微调
	}

	return nil
}

func abs(x int) int {
	if x < 0 {
		return -x
	}
	return x
}

internal/captcha/logic.go

// internal/captcha/logic.go
package captcha

import (
	"math/rand"
	"strconv"
	"strings"
)

type CaptchaResult struct {
	Question string
	Answer   int
	Token    string
}

func GenerateCaptcha() *CaptchaResult {
	initRand()

	a := rand.Intn(21) // 0-20
	b := rand.Intn(21)

	var op string
	var result int

	if rand.Intn(2) == 0 {
		op = "+"
		result = a + b
	} else {
		op = "-"
		if a < b {
			a, b = b, a
		}
		result = a - b
	}

	question := strconv.Itoa(a) + " " + op + " " + strconv.Itoa(b) + " = ?"

	return &CaptchaResult{
		Question: question,
		Answer:   result,
		Token:    randString(32),
	}
}

func randString(n int) string {
	const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
	b := make([]byte, n)
	for i := range b {
		b[i] = letters[rand.Intn(len(letters))]
	}
	return string(b)
}

func ValidateAnswer(token string, userInput string, getAnswer func(string) (string, error)) bool {
	userInput = strings.TrimSpace(userInput)
	ansStr, err := getAnswer(token)
	if err != nil {
		return false
	}
	expected, err := strconv.Atoi(ansStr)
	if err != nil {
		return false
	}
	given, err := strconv.Atoi(userInput)
	return err == nil && given == expected
}

internal/captcha/rand.go

// internal/captcha/rand.go
package captcha

import (
	"math/rand"
	"sync"
	"time"
)

var (
	randInit sync.Once
)

func initRand() {
	randInit.Do(func() {
		rand.Seed(time.Now().UnixNano())
	})
}

pkg/redis/redis.go

// pkg/redis/redis.go
package redis

import (
	"context"
	"time"

	"github.com/go-redis/redis/v8"
)

type Client struct {
	*redis.Client
}

func NewClient(addr, password string, db int) *Client {
	rdb := redis.NewClient(&redis.Options{
		Addr:     addr,
		Password: password,
		DB:       db,
	})

	return &Client{rdb}
}

func (c *Client) SetCaptcha(ctx context.Context, key string, answer int, expiration time.Duration) error {
	return c.Set(ctx, key, answer, expiration).Err()
}

func (c *Client) GetCaptcha(ctx context.Context, key string) (string, error) {
	return c.Get(ctx, key).Result()
}

func (c *Client) DeleteCaptcha(ctx context.Context, key string) error {
	return c.Del(ctx, key).Err()
}

main.go

// main.go
package main

import (
	"context"
	"fmt"
	"go_collect/captcha/internal/captcha"
	"go_collect/captcha/pkg/redis"
	"log"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
)

var redisClient *redis.Client

func init() {
	// 生产环境应从配置文件或环境变量读取
	redisClient = redis.NewClient("localhost:6377", "", 0)
	// 测试连接
	if err := redisClient.Ping(context.Background()).Err(); err != nil {
		log.Fatal("❌ Redis 连接失败:", err)
	}
	fmt.Println("✅ Redis 连接成功")
}

func main() {
	r := gin.Default()

	// 获取验证码图片
	r.GET("/captcha", getCaptchaHandler)

	// 验证答案
	r.POST("/captcha/verify", verifyCaptchaHandler)

	r.Run(":8088")
}

func getCaptchaHandler(c *gin.Context) {
	// 生成逻辑
	cap := captcha.GenerateCaptcha()

	// 缓存答案到 Redis,5分钟过期
	ctx := context.Background()
	err := redisClient.SetCaptcha(ctx, cap.Token, cap.Answer, 5*time.Minute)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "生成验证码失败"})
		return
	}

	// 设置响应头
	c.Header("Content-Type", "image/png")
	c.Header("X-Captcha-Token", cap.Token) // 前端需读取此 Header 或返回 JSON

	// 生成图片
	err = captcha.CaptchaImage(cap.Question, c.Writer)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "渲染图片失败"})
		return
	}
}

type VerifyRequest struct {
	Token  string `json:"token" binding:"required"`
	Answer string `json:"answer" binding:"required"`
}

func verifyCaptchaHandler(c *gin.Context) {
	var req VerifyRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// 校验答案
	isValid := captcha.ValidateAnswer(req.Token, req.Answer, func(token string) (string, error) {
		ans, err := redisClient.GetCaptcha(context.Background(), token)
		if err != nil {
			return "", err
		}
		// 验证后删除,防止重复使用
		redisClient.DeleteCaptcha(context.Background(), token)
		return ans, nil
	})

	if isValid {
		c.JSON(http.StatusOK, gin.H{
			"success": true,
			"message": "验证通过",
		})
	} else {
		c.JSON(http.StatusOK, gin.H{
			"success": false,
			"message": "验证失败",
		})
	}
}

调用

http://localhost:8088/captcha

在这里插入图片描述

验证

http://localhost:8088/captcha/verify
{
    "token":"sWmHAreIaA5jC7WqshKHXOjDMTH4I9kV",
     "answer":"7"
}
{
	"message": "验证通过",
	"success": true
}

在这里插入图片描述