代碼結構
相關代碼
captcha/internal/captcha/generator.go
package captchaimport ( _ "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"
)
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 , }
}
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) } 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 - dyfor { img. Set ( x1, y1, c) if x1 == x2 && y1 == y2 { break } e2 := 2 * errif e2 > - dy { err -= dyx1 += sx} if e2 < dx { err += dxy1 += 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 := xfor _ , 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) ) bounds, _ := font. BoundString ( face, string ( char) ) advance := ( bounds. Max. X - bounds. Min. X) . Ceil ( ) currentX += advance + 2 } return nil
} func abs ( x int ) int { if x < 0 { return - x} return x
}
internal/captcha/logic.go
package captchaimport ( "math/rand" "strconv" "strings"
) type CaptchaResult struct { Question string Answer int Token string
} func GenerateCaptcha ( ) * CaptchaResult { initRand ( ) a := rand. Intn ( 21 ) 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
package captchaimport ( "math/rand" "sync" "time"
) var ( randInit sync. Once
) func initRand ( ) { randInit. Do ( func ( ) { rand. Seed ( time. Now ( ) . UnixNano ( ) ) } )
}
pkg/redis/redis.go
package redisimport ( "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
package mainimport ( "context" "fmt" "go_collect/captcha/internal/captcha" "go_collect/captcha/pkg/redis" "log" "net/http" "time" "github.com/gin-gonic/gin"
) var redisClient * redis. Clientfunc 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 ( ) 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) 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 VerifyRequestif 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
}