Golang实现通过邮箱重置密码功能
Golang实现通过邮箱重置密码功能 大家好, 能否请你们告诉我如何为我的Web应用程序实现通过电子邮件重置密码的功能?我完全不知道该如何开发这个功能。 谢谢。
5 回复
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)
}
该实现包含以下关键组件:
-
生成安全的重置令牌:使用
crypto/rand生成32字节的随机令牌 -
令牌存储和验证:内存存储令牌并设置15分钟有效期
-
邮件发送功能:使用SMTP协议发送包含重置链接的邮件
-
密码哈希:使用
bcrypt安全地哈希新密码 -
HTTP端点:
/request-reset:接收重置请求并发送邮件/reset-password-page:显示重置密码表单/reset-password:处理密码重置
-
JWT替代方案:提供了使用JWT令牌的备选实现
使用前需要:
- 配置SMTP邮件服务器信息
- 将内存存储替换为数据库存储
- 添加适当的错误处理和日志记录
- 实现用户认证中间件
- 添加HTTPS支持
生产环境注意事项:
- 使用数据库存储用户和令牌
- 实现令牌清理机制
- 添加速率限制防止滥用
- 使用环境变量存储敏感信息
- 实现完整的错误处理
- 添加邮件发送队列

