205 lines
6.5 KiB
Go
205 lines
6.5 KiB
Go
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()
|
||
}
|