在 Golang 中发送手机验证码并进行验证,通常包含以下几个关键步骤:
1. 生成验证码: 生成一个随机的、通常是数字的验证码。
2. 存储验证码: 将生成的验证码与用户的手机号关联并临时存储起来,以便后续验证。
3. 发送验证码: 通过短信服务商将验证码发送到用户的手机号。
4. 用户输入验证码: 用户在前端界面接收到短信后,将验证码输入到表单中。
5. 后端验证: 用户提交验证码后,后端接收到用户输入的验证码,并与之前存储的验证码进行比对。
下面我将详细介绍实现过程和提供相应的 Golang 代码示例。
核心概念:
* 随机数生成: 使用 crypto/rand 或 math/rand 生成随机数字。crypto/rand 更适合生成用于安全目的的随机数。
* 存储:
* 内存 (不推荐用于生产环境): 对于简单的测试或小型应用,可以使用 map 存储,但重启后数据会丢失。
* Redis (推荐): Redis 是一个高性能的内存数据结构存储,非常适合用于存储临时数据,如验证码,并支持设置过期时间。
* 数据库: 也可以存储到数据库,但对于临时验证码来说,Redis 更为高效。
* 短信服务商: 需要接入第三方短信服务提供商(如阿里云短信服务、腾讯云短信服务、Twilio 等)。本文将以一个模拟的短信发送函数为例,你需要替换成实际的短信服务商 SDK 调用。
* 过期时间: 验证码必须有过期时间,通常是几分钟(如 2-5 分钟),以增加安全性。
—
go
package utils
import (
"crypto/rand"
"fmt"
"math/big"
)
// GenerateSMSCode 生成指定位数的数字验证码
func GenerateSMSCode(length int) (string, error) {
if length <= 0 {
return "", fmt.Errorf("length must be positive")
}
var code string
for i := 0; i < length; i++ {
// 生成一个0-9的随机数
num, err := rand.Int(rand.Reader, big.NewInt(10))
if err != nil {
return "", fmt.Errorf("failed to generate random digit: %w", err)
}
code += num.String()
}
return code, nil
}
// Example usage in main package or another file:
/*
func main() {
code, err := utils.GenerateSMSCode(6)
if err != nil {
fmt.Println("Error generating code:", err)
return
}
fmt.Println("Generated code:", code) // e.g., Generated code: 123456
}
*/
Explanation:
* crypto/rand.Int(rand.Reader, big.NewInt(10)): 使用 crypto/rand 包生成一个安全的随机整数,范围在 [0, 10)。
* big.NewInt(10): 创建一个 big.Int 表示数字 10。
* num.String(): 将生成的随机整数转换为字符串。
* 循环 length 次,拼接成指定位数的验证码。
—
这里假设你已经安装并启动了 Redis,并且在 Go 项目中使用了 go-redis/redis/v8 库。
安装 go-redis:bash
go get github.com/go-redis/redis/v8
Redis 存储和获取验证码的函数:go
package storage
import (
"context"
"fmt"
"time"
"github.com/go-redis/redis/v8"
)
var rdb *redis.Client // Global Redis client
// InitRedis 初始化 Redis 客户端
func InitRedis(addr, password string, db int) {
rdb = redis.NewClient(&redis.Options{
Addr: addr, // e.g., "localhost:6379"
Password: password, // "" if no password
DB: db, // 0 by default
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := rdb.Ping(ctx).Result()
if err != nil {
fmt.Printf("Error connecting to Redis: %v\n", err)
// Handle error appropriately in a real application (e.g., exit, retry)
} else {
fmt.Println("Connected to Redis successfully!")
}
}
// StoreSMSCode 存储验证码到 Redis
func StoreSMSCode(ctx context.Context, phone string, code string, expiration time.Duration) error {
if rdb == nil {
return fmt.Errorf("redis client not initialized")
}
key := fmt.Sprintf("sms_code:%s", phone) // Key format: sms_code:<phone_number>
err := rdb.Set(ctx, key, code, expiration).Err()
if err != nil {
return fmt.Errorf("failed to store SMS code in Redis: %w", err)
}
return nil
}
// GetSMSCode 从 Redis 获取验证码
func GetSMSCode(ctx context.Context, phone string) (string, error) {
if rdb == nil {
return "", fmt.Errorf("redis client not initialized")
}
key := fmt.Sprintf("sms_code:%s", phone)
code, err := rdb.Get(ctx, key).Result()
if err != nil {
if err == redis.Nil {
return "", fmt.Errorf("SMS code not found or expired for phone: %s", phone)
}
return "", fmt.Errorf("failed to get SMS code from Redis: %w", err)
}
return code, nil
}
// DeleteSMSCode 删除 Redis 中的验证码 (例如,验证成功后)
func DeleteSMSCode(ctx context.Context, phone string) error {
if rdb == nil {
return fmt.Errorf("redis client not initialized")
}
key := fmt.Sprintf("sms_code:%s", phone)
_, err := rdb.Del(ctx, key).Result()
if err != nil {
return fmt.Errorf("failed to delete SMS code from Redis: %w", err)
}
return nil
}
// Example usage in main package or another file:
/*
func main() {
storage.InitRedis("localhost:6379", "", 0) // Initialize Redis
ctx := context.Background()
phone := "13800138000"
code := "123456"
expiration := 5 * time.Minute
err := storage.StoreSMSCode(ctx, phone, code, expiration)
if err != nil {
fmt.Println("Error storing code:", err)
} else {
fmt.Println("Code stored successfully.")
}
retrievedCode, err := storage.GetSMSCode(ctx, phone)
if err != nil {
fmt.Println("Error retrieving code:", err)
} else {
fmt.Println("Retrieved code:", retrievedCode)
}
// Simulate expiration
// time.Sleep(6 * time.Minute)
// retrievedCode, err = storage.GetSMSCode(ctx, phone)
// ...
// err = storage.DeleteSMSCode(ctx, phone)
// ...
}
*/
Explanation:
* InitRedis: 连接到 Redis 服务器。
* StoreSMSCode:
* 使用 sms_code:<phone> 作为 Redis key,将手机号与验证码关联。rdb.Set(ctx, key, code, expiration)
*: 将验证码code存储在key下,并设置过期时间expiration。GetSMSCode
*:redis.Nil
* 通过手机号获取对应的验证码。
*表示 key 不存在(可能未设置或已过期)。DeleteSMSCode`: 在验证成功后,删除 Redis 中的验证码。
*
—
在实际应用中,你需要集成一个短信服务商的 SDK。这里提供一个模拟函数,用于演示流程。go
package sms
import (
"fmt"
"log"
)
// SendSMS 模拟发送短信验证码
// In a real application, replace this with actual SMS provider SDK calls (e.g., Aliyun, Tencent Cloud, Twilio)
func SendSMS(phone string, code string) error {
log.Printf("Simulating sending SMS to %s with code: %s", phone, code)
// --- Replace with actual SMS API call ---
// Example for Aliyun SMS:
// client, err := dysmsapi.NewClientWithAccessKey("your_region_id", "your_access_key_id", "your_access_key_secret")
// if err != nil {
// return fmt.Errorf("failed to create SMS client: %w", err)
// }
// request := dysmsapi.CreateSendSmsRequest()
// request.Scheme = "https"
// request.PhoneNumbers = phone
// request.SignName = "YourSignName" // e.g., "YourCompany"
// request.TemplateCode = "YourTemplateCode" // e.g., "SMS_1234567"
// request.TemplateParam = fmt.Sprintf(`{"code": "%s"}`, code)
//
// response, err := client.SendSms(request)
// if err != nil {
// return fmt.Errorf("failed to send SMS: %w", err)
// }
// if response.IsSuccess() {
// log.Printf("SMS sent successfully to %s, request ID: %s", phone, response.RequestId)
// return nil
// }
// return fmt.Errorf("SMS sending failed: %s", response.Message)
// ----------------------------------------
// For simulation, assume success
fmt.Printf("Successfully simulated sending SMS to %s with code: %s\n", phone, code)
return nil
}
// Example usage in main package or another file:
/*
func main() {
phone := "13800138000"
code := "987654"
err := sms.SendSMS(phone, code)
if err != nil {
fmt.Println("Error sending SMS:", err)
} else {
fmt.Println("SMS simulation successful.")
}
}
*/
Explanation:
* This function is a placeholder. In a real-world scenario, you’d integrate with a specific SMS provider’s SDK (e.g., Aliyun, Tencent Cloud, Twilio).
* The log.Printf statement shows what would happen.
—
这里使用 gin 框架来创建一个简单的 HTTP API,用于请求发送验证码和验证验证码。
安装 Gin:bash
go get github.com/gin-gonic/gin
main.go:go
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
"your_module_path/sms" // Replace with your actual module path
"your_module_path/storage" // Replace with your actual module path
"your_module_path/utils" // Replace with your actual module path
"github.com/gin-gonic/gin"
)
// Config for your application
const (
RedisAddr = "localhost:6379"
RedisPassword = ""
RedisDB = 0
SMSCodeLength = 6
SMSCodeExpiration = 5 * time.Minute // 5 minutes
)
type SendSMSRequest struct {
Phone string `json:"phone" binding:"required"`
}
type VerifySMSRequest struct {
Phone string `json:"phone" binding:"required"`
Code string `json:"code" binding:"required"`
}
func main() {
// Initialize Redis
storage.InitRedis(RedisAddr, RedisPassword, RedisDB)
// Initialize Gin router
r := gin.Default()
// API Endpoints
r.POST("/send-sms-code", sendSMSCodeHandler)
r.POST("/verify-sms-code", verifySMSCodeHandler)
// Run the server
port := "8080"
log.Printf("Starting server on :%s", port)
if err := r.Run(":" + port); err != nil {
log.Fatalf("Failed to run server: %v", err)
}
}
// sendSMSCodeHandler handles requests to send an SMS verification code
func sendSMSCodeHandler(c *gin.Context) {
var req SendSMSRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 1. Generate SMS code
code, err := utils.GenerateSMSCode(SMSCodeLength)
if err != nil {
log.Printf("Error generating SMS code: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate verification code"})
return
}
// 2. Store SMS code in Redis with expiration
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) // Redis timeout
defer cancel()
err = storage.StoreSMSCode(ctx, req.Phone, code, SMSCodeExpiration)
if err != nil {
log.Printf("Error storing SMS code in Redis for phone %s: %v", req.Phone, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to store verification code"})
return
}
// 3. Send SMS code (using the simulated function)
err = sms.SendSMS(req.Phone, code)
if err != nil {
log.Printf("Error sending SMS to phone %s: %v", req.Phone, err)
// In a real app, you might want to clean up the stored code if sending fails,
// or implement a retry mechanism. For now, we'll just report the error.
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send verification code"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Verification code sent successfully"})
}
// verifySMSCodeHandler handles requests to verify an SMS verification code
func verifySMSCodeHandler(c *gin.Context) {
var req VerifySMSRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 1. Get stored SMS code from Redis
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) // Redis timeout
defer cancel()
storedCode, err := storage.GetSMSCode(ctx, req.Phone)
if err != nil {
log.Printf("Error retrieving SMS code from Redis for phone %s: %v", req.Phone, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve verification code"})
return
}
// 2. Compare entered code with stored code
if req.Code != storedCode {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid verification code"})
return
}
// 3. If codes match, delete the stored code (important for security)
err = storage.DeleteSMSCode(ctx, req.Phone)
if err != nil {
log.Printf("Error deleting SMS code from Redis for phone %s: %v", req.Phone, err)
// This is a critical step. If it fails, log it and potentially alert an admin.
// For this example, we still consider the verification successful if codes match.
}
c.JSON(http.StatusOK, gin.H{"message": "Verification successful"})
}
Directory Structure:
your_module_path/
├── main.go
├── sms/
│ └── sms.go
├── storage/
│ └── redis.go
└── utils/
└── code_generator.go
How to Run:
1. Create the files as shown in the directory structure.
2. Replace your_module_path with your actual Go module path (e.g., github.com/yourusername/yourproject). You can initialize a Go module with go mod init your_module_path in your project’s root directory.
3. Ensure Redis is running on localhost:6379.
4. Run the main application: go run main.go
5. Test the API:
* Send SMS Code:
bash
curl -X POST http://localhost:8080/send-sms-code \
-H "Content-Type: application/json" \
-d '{"phone": "13800138000"}'
(You should see a log message about sending the SMS.)
* Verify SMS Code:
Assume the code sent was 123456.
bash
curl -X POST http://localhost:8080/verify-sms-code \
-H "Content-Type: application/json" \
-d '{"phone": "13800138000", "code": "123456"}'
If correct, you’ll get {"message": "Verification successful"}.
If incorrect, you’ll get {"error": "Invalid verification code"}.
—
1. HTTPS: 务必在生产环境中使用 HTTPS 来加密客户端和服务器之间的通信。
2. Rate Limiting: 对 /send-sms-code 和 /verify-sms-code API 端点实施严格的速率限制,以防止滥用(如暴力破解验证码或大量发送短信)。
3. 验证码强度: 确保生成的验证码足够随机且难以猜测。6位数字是一个常见的选择。
4. 过期时间: 验证码的过期时间应足够短,以限制其有效期。
5. 一次性使用: 验证成功后,务必删除 Redis 中的验证码,确保其只能使用一次。
6. 错误处理: 妥善处理 Redis 连接失败、短信发送失败等情况,并记录日志。
7. 设备指纹/IP 限制: 考虑为单个设备或 IP 地址添加额外的限制,以防止同一账户在多个设备上被滥用。
8. 用户登录状态: 验证码通常用于用户注册、找回密码或登录过程。在验证成功后,你的应用程序逻辑应该相应地更新用户的登录状态。
这个例子提供了一个完整的 Golang 手机验证码发送和验证的流程,你可以根据你的具体需求进行调整和扩展。