비밀번호 재설정 기능: Golang에서 이메일 보내기

Mary-Kate Olsen
풀어 주다: 2024-10-01 06:12:30
원래의
1129명이 탐색했습니다.

Password Reset Feature: Sending Email in Golang

이 글을 작성하는 동안 앱 Task-inator 3000에서 사용자의 비밀번호를 재설정하는 기능을 구현하고 있습니다. 그냥 내 생각과 행동을 기록해 보세요


계획

저는 이런 흐름을 생각하고 있습니다.

  1. 사용자가 '비밀번호를 잊으셨나요?'를 클릭합니다. 버튼
  2. 이메일을 요청하는 사용자에게 모달 표시
  3. 이메일이 있는지 확인하고 10자 길이의 OTP를 이메일로 보냅니다
  4. 모달이 이제 OTP와 새 비밀번호를 요청합니다
  5. 사용자의 비밀번호가 해시되고 업데이트됩니다

우려사항의 분리

프런트엔드

  • 이메일 입력을 위한 모달 만들기
  • 동일한 모달이 OTP와 새 비밀번호를 가져옵니다

백엔드

  • 이메일 전송을 위한 API 만들기
  • 비밀번호 재설정 API 생성

백엔드부터 시작하겠습니다

백엔드

위에 언급한 것처럼 두 개의 API가 필요합니다

1. 이메일 보내기

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 파일을 생성하겠습니다. 현재 이 파일은 다음과 같은 기능을 갖습니다:

  • OTP 생성: 이메일로 보낼 고유한 영숫자 10자리 OTP를 생성하려면
  • AddOTPtoRedis: 키 값 형식으로 Redis에 OTP를 추가합니다.
key -> password-reset:<email>
value -> hashed otp
expiry -> 10 mins
로그인 후 복사

보안상의 이유로 OTP 자체가 아닌 OTP의 해시를 저장하고 있습니다

  • SendOTP: 생성된 OTP를 사용자의 이메일로 전송합니다

코드를 작성하는 동안 여기에 5개의 상수가 필요하다는 것을 알았습니다.

  • OTP용 redis 키 접두사
  • OTP 유효기간
  • OTP 생성을 위한 문자셋
  • 이메일 템플릿
  • OTP 길이

즉시 /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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

원천:dev.to
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
저자별 최신 기사
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿