この投稿を書いている私は、アプリ Task-inator 3000 にユーザーのパスワードをリセットする機能を実装しています。私の思考プロセスと実行した手順を記録するだけです
次のようなフローを考えています:
フロントエンド
バックエンド
バックエンドから始めます
上で述べたように、2 つの API が必要です
API はユーザーからの電子メールのみを取得する必要があり、成功した場合にはコンテンツを返しません。したがって、次のようにコントローラーを作成します。
// controllers/passwordReset.go func SendPasswordResetEmail(c *fiber.Ctx) error { type Input struct { Email string `json:"email"` } var input Input err := c.BodyParser(&input) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "invalid data", }) } // TODO: send email with otp to user return c.SendStatus(fiber.StatusNoContent) }
次にルートを追加します:
// routes/routes.go // password reset api.Post("/send-otp", controllers.SendPasswordResetEmail)
Golang の標準ライブラリの net/smtp を使用します。
ドキュメントを読むと、プロジェクトの初期化時に SMTPClient を作成するのが最善だと思います。したがって、/config ディレクトリにファイル smtpConnection.go を作成します。
その前に、次の環境変数を .env または運用サーバーに追加します。
SMTP_HOST="smtp.zoho.in" SMTP_PORT="587" SMTP_EMAIL="<myemail>" SMTP_PASSWORD="<mypassword>"
私はzohomailを使用しているため、ここに記載されているようにそのsmtpホストとポート(TLS用)を使用しています。
// config/smtpConnection.go package config import ( "crypto/tls" "fmt" "net/smtp" "os" ) var SMTPClient *smtp.Client func SMTPConnect() { host := os.Getenv("SMTP_HOST") port := os.Getenv("SMTP_PORT") email := os.Getenv("SMTP_EMAIL") password := os.Getenv("SMTP_PASSWORD") smtpAuth := smtp.PlainAuth("", email, password, host) // connect to smtp server client, err := smtp.Dial(host + ":" + port) if err != nil { panic(err) } SMTPClient = client client = nil // initiate TLS handshake if ok, _ := SMTPClient.Extension("STARTTLS"); ok { config := &tls.Config{ServerName: host} if err = SMTPClient.StartTLS(config); err != nil { panic(err) } } // authenticate err = SMTPClient.Auth(smtpAuth) if err != nil { panic(err) } fmt.Println("SMTP Connected") }
抽象化のために、/utils にpasswordReset.go ファイルを作成します。現時点では、このファイルには次の機能があります:
key -> password-reset:<email> value -> hashed otp expiry -> 10 mins
セキュリティ上の理由から、OTP 自体ではなく OTP のハッシュを保存しています
コードを書いていると、ここで 5 つの定数が必要であることがわかりました。
すぐに /utils/constants.go に追加します
// utils/constants.go package utils import "time" const ( authTokenExp = time.Minute * 10 refreshTokenExp = time.Hour * 24 * 30 // 1 month blacklistKeyPrefix = "blacklisted:" otpKeyPrefix = "password-reset:" otpExp = time.Minute * 10 otpCharSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" emailTemplate = "To: %s\r\n" + "Subject: Task-inator 3000 Password Reset\r\n" + "\r\n" + "Your OTP for password reset is %s\r\n" // public because needed for testing OTPLength = 10 )
(真のランダム性を提供するため、 math/rand ではなく、 crypto/rand からインポートすることに注意してください)
// utils/passwordReset.go package utils import ( "context" "crypto/rand" "fmt" "math/big" "os" "task-inator3000/config" "golang.org/x/crypto/bcrypt" ) func GenerateOTP() string { result := make([]byte, OTPLength) charsetLength := big.NewInt(int64(len(otpCharSet))) for i := range result { // generate a secure random number in the range of the charset length num, _ := rand.Int(rand.Reader, charsetLength) result[i] = otpCharSet[num.Int64()] } return string(result) } func AddOTPtoRedis(otp string, email string, c context.Context) error { key := otpKeyPrefix + email // hashing the OTP data, _ := bcrypt.GenerateFromPassword([]byte(otp), 10) // storing otp with expiry err := config.RedisClient.Set(c, key, data, otpExp).Err() if err != nil { return err } return nil } func SendOTP(otp string, recipient string) error { sender := os.Getenv("SMTP_EMAIL") client := config.SMTPClient // setting the sender err := client.Mail(sender) if err != nil { return err } // set recipient err = client.Rcpt(recipient) if err != nil { return err } // start writing email writeCloser, err := client.Data() if err != nil { return err } // contents of the email msg := fmt.Sprintf(emailTemplate, recipient, otp) // write the email _, err = writeCloser.Write([]byte(msg)) if err != nil { return err } // close writecloser and send email err = writeCloser.Close() if err != nil { return err } return nil }
関数 GenerateOTP() はモック (単体テスト) なしでテストできるため、簡単なテストを作成しました
package utils_test import ( "task-inator3000/utils" "testing" ) func TestGenerateOTP(t *testing.T) { result := utils.GenerateOTP() if len(result) != utils.OTPLength { t.Errorf("Length of OTP was not %v. OTP: %v", utils.OTPLength, result) } }
次に、これらをすべてコントローラー内にまとめる必要があります。その前に、指定された電子メール アドレスがデータベースに存在することを確認する必要があります。
コントローラーの完全なコードは次のとおりです:
func SendPasswordResetEmail(c *fiber.Ctx) error { type Input struct { Email string `json:"email"` } var input Input err := c.BodyParser(&input) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "invalid data", }) } // check if user with email exists users := config.DB.Collection("users") filter := bson.M{"_id": input.Email} err = users.FindOne(c.Context(), filter).Err() if err != nil { if err == mongo.ErrNoDocuments { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ "error": "user with given email not found", }) } return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": "error while finding in the database:\n" + err.Error(), }) } // generate otp and add it to redis otp := utils.GenerateOTP() err = utils.AddOTPtoRedis(otp, input.Email, c.Context()) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": err.Error(), }) } // send the otp to user through email err = utils.SendOTP(otp, input.Email) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": err.Error(), }) } return c.SendStatus(fiber.StatusNoContent) }
正しい URL に POST リクエストを送信することで API をテストできます。 cURL の例は次のとおりです:
curl --location 'localhost:3000/api/send-otp' \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "yashjaiswal.cse@gmail.com" }'
シリーズの次のパートでは、パスワードをリセットするための次の API を作成します
以上がパスワード リセット機能: Golang でのメール送信の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。