Golang实现通过邮箱重置密码功能

Golang实现通过邮箱重置密码功能 大家好, 能否请你们告诉我如何为我的Web应用程序实现通过电子邮件重置密码的功能?我完全不知道该如何开发这个功能。 谢谢。

5 回复

非常感谢 ermanimer

更多关于Golang实现通过邮箱重置密码功能的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


ermanimer:

如果你需要,我可以分享示例代码。

请分享。

当用户请求重置密码时,生成一个安全密钥,将其分配给用户。创建链接并通过电子邮件发送给用户。当用户点击链接时,检查密钥是否正确,然后允许用户更改密码。据我所知,这是最简单的方法。如果您需要,我可以分享示例代码。

func main() {
    fmt.Println("hello world")
}

我使用 MailJet 作为电子邮件服务。所有消息和邮件内容均为土耳其语。

func requestResetPasswordEmail(c *gin.Context) {
	var rrpef RequestResetPasswordEmailForm
	err := c.BindJSON(&rrpef)
	if err != nil {
		errorMessage := "Form bilgileri alınamadı!"
		l.Error(errorMessage)
		c.JSON(http.StatusInternalServerError, gin.H{
			"error_message": errorMessage,
		})
		return
	}
	emailAddress := rrpef.EmailAddress
	u, err := db.GetUserByEmailAddress(emailAddress)
	if err != nil {
		errorMessage := err.Error()
		l.Error(errorMessage)
		c.JSON(http.StatusInternalServerError, gin.H{
			"error_message": errorMessage,
		})
		return
	}
	userId := u.Id
	name := u.Name
	rpr, err := db.GetResetPasswordRequestByUserId(userId)
	if err != nil {
		errorMessage := err.Error()
		l.Error(errorMessage)
		c.JSON(http.StatusInternalServerError, gin.H{
			"error_message": errorMessage,
		})
		return
	}
	if rpr != nil {
		resetPasswordRequestInterval := co.ResetPasswordRequestInterval
		availableAt := rpr.CreatedAt.Add(time.Duration(resetPasswordRequestInterval) * time.Minute)
		leftDuration := int(availableAt.Sub(time.Now().UTC()).Seconds())
		if leftDuration > 0 {
			errorMessage := fmt.Sprintf("Tekrar şifre sıfırlama talebinde bulunmadan önce %v saniye bekleyin!", leftDuration)
			l.Error(errorMessage)
			c.JSON(http.StatusInternalServerError, gin.H{
				"error_message": errorMessage,
			})
			return
		}
	}
	key := kg.GenerateStringKey(64)
	ipAddress := c.ClientIP()
	rpr = &db.ResetPasswordRequest{
		UserId:    userId,
		Key:       key,
		IpAddress: ipAddress,
		CreatedAt: time.Now().UTC(),
	}
	err = db.InsertResetPasswordRequest(rpr)
	if err != nil {
		errorMessage := err.Error()
		l.Error(errorMessage)
		c.JSON(http.StatusInternalServerError, gin.H{
			"error_message": errorMessage,
		})
		return
	}
	m := ec.Message{
		EmailAddress: emailAddress,
		Name:         name,
		Subject:      "Şifre Sıfırlama Talebi",
		HtmlPart: `<p>Şifrenizi değiştirmek için aşağıdaki bağlantıya tıklayın:</p>
			<a href="http://market.atiaup.com/change_password/` + key + `">http://market.atiaup.com/change_password/` + key + `</a>
			<p>Eğer şifre sıfırlama talebinde bulunmaduysanız bu maile cevap vererek bizim ile irtibata geçebilirsiniz.</p>`,
	}
	err = ec.Send(&m)
	if err != nil {
		errorMessage := err.Error()
		l.Error(errorMessage)
		c.JSON(http.StatusInternalServerError, gin.H{
			"error_message": errorMessage,
		})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"success_message": "Şifre değiştirme bağlantısı e-posta adresinize gönderildi. Kullanıcı girişi sayfasına yönlendiriliyorsunuz.",
	})
}

func publicChangePassword(c *gin.Context) {
	key := c.Param("key")
	_, err := db.GetResetPasswordRequestByKey(key)
	if err != nil {
		errorMessage := err.Error()
		l.Error(errorMessage)
		renderTemplateWithMessages(c, "public_change_password.html", "", "", errorMessage)
		return
	}
	renderTemplateWithKey(c, "public_change_password.html", key)
}

func publicUpdatePassword(c *gin.Context) {
	var upwkf UpdatePasswordWithKeyForm
	err := c.BindJSON(&upwkf)
	if err != nil {
		errorMessage := "Form bilgileri alınamadı!"
		l.Error(errorMessage)
		c.JSON(http.StatusInternalServerError, gin.H{
			"error_message": errorMessage,
		})
		return
	}
	key := upwkf.Key
	password := upwkf.Password
	rpr, err := db.GetResetPasswordRequestByKey(key)
	if err != nil {
		errorMessage := err.Error()
		l.Error(errorMessage)
		c.JSON(http.StatusInternalServerError, gin.H{
			"error_message": errorMessage,
		})
		return
	}
	userId := rpr.UserId
	u, err := db.GetUser(userId)
	if err != nil {
		errorMessage := err.Error()
		l.Error(errorMessage)
		c.JSON(http.StatusInternalServerError, gin.H{
			"error_message": errorMessage,
		})
		return
	}
	companyId := u.CompanyId
	name := u.Name
	emailAddress := u.EmailAddress
	err = db.UpdateUserPassword(companyId, userId, password)
	if err != nil {
		errorMessage := err.Error()
		l.Error(errorMessage)
		c.JSON(http.StatusInternalServerError, gin.H{
			"error_message": errorMessage,
		})
		return
	}
	m := ec.Message{
		EmailAddress: emailAddress,
		Name:         name,
		Subject:      "Şifre Değiştirme Bilgisi",
		HtmlPart: `<p>Şifreniz değiştirildi.</p>
			<p>Eğer şifrenizi siz değiştirmediyseniz bu maile cevap vererek bizim ile irtibata geçebilirsiniz.</p>`,
	}
	err = ec.Send(&m)
	if err != nil {
		errorMessage := err.Error()
		l.Error(errorMessage)
		c.JSON(http.StatusInternalServerError, gin.H{
			"error_message": errorMessage,
		})
		return
	}
	err = db.DeleteResetPasswordRequest(userId)
	if err != nil {
		errorMessage := err.Error()
		l.Error(errorMessage)
		c.JSON(http.StatusInternalServerError, gin.H{
			"error_message": errorMessage,
		})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"success_message": "Şifreniz değiştirildi. Kullanıcı girişi sayfasına yönlendiriliyorsunuz. Kullanıcı adınız ve yeni şifreniz ile giriş yapabilirsiniz.",
	})
}

以下是使用Go语言实现通过邮箱重置密码功能的完整示例代码。该实现包含用户请求重置密码、发送重置链接、验证重置令牌和更新密码的核心流程。

package main

import (
    "crypto/rand"
    "encoding/hex"
    "fmt"
    "net/http"
    "net/smtp"
    "strings"
    "time"

    "github.com/dgrijalva/jwt-go"
    "golang.org/x/crypto/bcrypt"
)

// 用户模型
type User struct {
    ID       int
    Email    string
    Password string
}

// 内存存储(生产环境请使用数据库)
var users = map[string]User{
    "user@example.com": {ID: 1, Email: "user@example.com", Password: "$2a$10$hashedpassword"},
}

// 重置令牌存储
var resetTokens = make(map[string]ResetToken)

type ResetToken struct {
    Email     string
    ExpiresAt time.Time
}

// 生成重置令牌
func generateResetToken(email string) (string, error) {
    // 生成随机令牌
    tokenBytes := make([]byte, 32)
    if _, err := rand.Read(tokenBytes); err != nil {
        return "", err
    }
    token := hex.EncodeToString(tokenBytes)

    // 存储令牌,设置15分钟有效期
    resetTokens[token] = ResetToken{
        Email:     email,
        ExpiresAt: time.Now().Add(15 * time.Minute),
    }

    return token, nil
}

// 验证重置令牌
func validateResetToken(token string) (string, bool) {
    resetToken, exists := resetTokens[token]
    if !exists {
        return "", false
    }

    if time.Now().After(resetToken.ExpiresAt) {
        delete(resetTokens, token)
        return "", false
    }

    return resetToken.Email, true
}

// 发送重置邮件
func sendResetEmail(email, token string) error {
    // 邮件配置
    smtpHost := "smtp.gmail.com"
    smtpPort := "587"
    smtpUser := "your-email@gmail.com"
    smtpPass := "your-app-password"

    // 重置链接
    resetLink := fmt.Sprintf("https://yourapp.com/reset-password?token=%s", token)

    // 邮件内容
    subject := "密码重置请求"
    body := fmt.Sprintf(`您好,

我们收到了您重置密码的请求。请点击以下链接重置您的密码:

%s

如果这不是您发起的请求,请忽略此邮件。

此链接将在15分钟后失效。

谢谢,
您的应用团队`, resetLink)

    msg := []byte(fmt.Sprintf("To: %s\r\nSubject: %s\r\n\r\n%s", email, subject, body))

    // 发送邮件
    auth := smtp.PlainAuth("", smtpUser, smtpPass, smtpHost)
    err := smtp.SendMail(smtpHost+":"+smtpPort, auth, smtpUser, []string{email}, msg)
    return err
}

// 请求重置密码
func requestPasswordReset(w http.ResponseWriter, r *http.Request) {
    if r.Method != "POST" {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    email := r.FormValue("email")
    if email == "" {
        http.Error(w, "Email is required", http.StatusBadRequest)
        return
    }

    // 检查用户是否存在
    _, exists := users[email]
    if !exists {
        // 出于安全考虑,即使用户不存在也返回成功
        fmt.Fprintf(w, "If the email exists, a reset link will be sent")
        return
    }

    // 生成重置令牌
    token, err := generateResetToken(email)
    if err != nil {
        http.Error(w, "Failed to generate reset token", http.StatusInternalServerError)
        return
    }

    // 发送重置邮件
    if err := sendResetEmail(email, token); err != nil {
        http.Error(w, "Failed to send reset email", http.StatusInternalServerError)
        return
    }

    fmt.Fprintf(w, "Password reset email sent successfully")
}

// 重置密码页面
func resetPasswordPage(w http.ResponseWriter, r *http.Request) {
    token := r.URL.Query().Get("token")
    if token == "" {
        http.Error(w, "Token is required", http.StatusBadRequest)
        return
    }

    // 验证令牌
    _, valid := validateResetToken(token)
    if !valid {
        http.Error(w, "Invalid or expired token", http.StatusBadRequest)
        return
    }

    // 显示重置密码表单
    html := `
    <!DOCTYPE html>
    <html>
    <head>
        <title>重置密码</title>
    </head>
    <body>
        <h2>重置密码</h2>
        <form action="/reset-password" method="POST">
            <input type="hidden" name="token" value="%s">
            <div>
                <label>新密码:</label>
                <input type="password" name="new_password" required>
            </div>
            <div>
                <label>确认密码:</label>
                <input type="password" name="confirm_password" required>
            </div>
            <button type="submit">重置密码</button>
        </form>
    </body>
    </html>`

    fmt.Fprintf(w, html, token)
}

// 处理密码重置
func resetPassword(w http.ResponseWriter, r *http.Request) {
    if r.Method != "POST" {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    token := r.FormValue("token")
    newPassword := r.FormValue("new_password")
    confirmPassword := r.FormValue("confirm_password")

    if token == "" || newPassword == "" || confirmPassword == "" {
        http.Error(w, "All fields are required", http.StatusBadRequest)
        return
    }

    if newPassword != confirmPassword {
        http.Error(w, "Passwords do not match", http.StatusBadRequest)
        return
    }

    // 验证令牌
    email, valid := validateResetToken(token)
    if !valid {
        http.Error(w, "Invalid or expired token", http.StatusBadRequest)
        return
    }

    // 哈希新密码
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
    if err != nil {
        http.Error(w, "Failed to hash password", http.StatusInternalServerError)
        return
    }

    // 更新用户密码(生产环境应更新数据库)
    if user, exists := users[email]; exists {
        user.Password = string(hashedPassword)
        users[email] = user
    }

    // 删除已使用的令牌
    delete(resetTokens, token)

    fmt.Fprintf(w, "Password reset successfully")
}

// 使用JWT的替代实现(可选)
func generateJWTResetToken(email string) (string, error) {
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "email": email,
        "exp":   time.Now().Add(15 * time.Minute).Unix(),
        "type":  "password_reset",
    })

    secretKey := []byte("your-secret-key")
    return token.SignedString(secretKey)
}

func validateJWTResetToken(tokenString string) (string, bool) {
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("unexpected signing method")
        }
        return []byte("your-secret-key"), nil
    })

    if err != nil || !token.Valid {
        return "", false
    }

    claims, ok := token.Claims.(jwt.MapClaims)
    if !ok || claims["type"] != "password_reset" {
        return "", false
    }

    email, ok := claims["email"].(string)
    if !ok {
        return "", false
    }

    return email, true
}

func main() {
    http.HandleFunc("/request-reset", requestPasswordReset)
    http.HandleFunc("/reset-password-page", resetPasswordPage)
    http.HandleFunc("/reset-password", resetPassword)

    fmt.Println("Server starting on :8080")
    http.ListenAndServe(":8080", nil)
}

该实现包含以下关键组件:

  1. 生成安全的重置令牌:使用crypto/rand生成32字节的随机令牌

  2. 令牌存储和验证:内存存储令牌并设置15分钟有效期

  3. 邮件发送功能:使用SMTP协议发送包含重置链接的邮件

  4. 密码哈希:使用bcrypt安全地哈希新密码

  5. HTTP端点

    • /request-reset:接收重置请求并发送邮件
    • /reset-password-page:显示重置密码表单
    • /reset-password:处理密码重置
  6. JWT替代方案:提供了使用JWT令牌的备选实现

使用前需要:

  1. 配置SMTP邮件服务器信息
  2. 将内存存储替换为数据库存储
  3. 添加适当的错误处理和日志记录
  4. 实现用户认证中间件
  5. 添加HTTPS支持

生产环境注意事项:

  • 使用数据库存储用户和令牌
  • 实现令牌清理机制
  • 添加速率限制防止滥用
  • 使用环境变量存储敏感信息
  • 实现完整的错误处理
  • 添加邮件发送队列
回到顶部