이 글을 작성하는 동안 앱 Task-inator 3000에서 사용자의 비밀번호를 재설정하는 기능을 구현하고 있습니다. 그냥 내 생각과 행동을 기록해 보세요
저는 이런 흐름을 생각하고 있습니다.
프런트엔드
백엔드
백엔드부터 시작하겠습니다
위에 언급한 것처럼 두 개의 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에 비밀번호Reset.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 중국어 웹사이트의 기타 관련 기사를 참조하세요!