Back-End/Fastapi

11. 이메일 인증을 통안 회원가입 및 관리(Email Verification)

sd4beatles 2024. 11. 10. 17:10

1. Introduction

몇 웹 사이트 및 앱을 보면, 회원가입 후 이메일 인증을 하는 것을 종종 경험해본적이 있습니다. 즉, 이처럼 이메일 인증을 완료함으로서, '완전한 접근'을 허용하는 코드를 구현해보겠습니다. 일단, database에 user라는 table을 생성했다고 가정하면, column 정보는 아래와 같습니다. 이미, 이전 포스트에서 다뤘던 내용이기에 간략하게 자세한 설명은 생략합니다.

"앞으로 살펴볼 코드를 토앻서, 아래의 column중에 'is_verified'가 회원 가입시에 False 초기값이지만, 이 메일 인증 후에 True값으로 전환됩니다. Section 10 의 보충내용으로 꼭 이전 포스트를 읽어주세요 "


class User(SQLModel,table=True):
  __tablename__="users"


  user_id:str=Field(
    sa_column=Column(pg.VARCHAR,
                     nullable=False,
                     primary_key=True))

  email:str=Field(unique=True,nullable=False)
  first_name:str
  last_name:str
  passowrd_hash:str=Field(default=False)
  is_verified:bool=Field(default=False)
  updated_at:datetime=Field(sa_column=Column(pg.TIMESTAMP,default=datetime.now,nullable=False))

  # #add user's role to our database table
  role:str=Field(sa_column=Column(pg.VARCHAR,nullable=False,server_default="user"))

  like:Optional['Likes']=Relationship(
    back_populates="users")



  def __repr(self):
    return f"<User {self.user_id}>"

2. Code

2.1 Set up configurations for SMTP

대부분 post는 gmail에 관련된 내용을 많이 담고 있기 때문에, 이번 poist는 네이버를 사용해보독 하겠습니다. 네이버의 SMTP를 사용하려면, 일단 네이버 로그인 후, 메일을 클릭하여 내 메일함을 접근합니다.

네이버 환경설정 1

내 메일함의 톱니바퀴를 클릭하면, 아래와 같은 화면을 마주할 수 있습니다. 환경 설정 부분에서 POP3/IMAP 설정을 클릭한 후,

네이버 환경설정 92

IMAP/SMTP 설정을 클릭하여, 사용함으로 재설정합니다.

네이버 환경설정 03

그리고 아래의 계정정보와 SMTP 서버명등 SMTP네이버를 사용하하기 위한 정보를 찾으실 수 있습니다.(참고로, 전 개인정보인 아이디를 그림판으로 지웠습니다.)

네이버 환경설정 04

위와 같은 정보를 우리 .env 파일에 적어줍니다.


# email
NAVER_USERNAME=<네이버아이디>
NAVER_PASSWORD=<네이버비밀번호>
NAVER_SERVER=smtp.naver.com
NAVER_PORT=587
NAVER_FROM_EMAIL=<이메일 주소>
#name appears on the receiver's header
NAVER_FROM_NAME=<수신자상단 이메일에 적힐 송신자 이름>

이와 같이 .env 환경변수를 적어놨다면, config.py에 윗 정보를 추가해줍니다.


from dotenv import load_dotenv
from pydantic_settings import BaseSettings, SettingsConfigDict

load_dotenv()
class Settings(BaseSettings):
  #infomraiton on mail 
  naver_username:str
  naver_password:str
  naver_server:str
  naver_port:int
  naver_from_email:str
  naver_from_name:str
  mail_starttls:bool=False
  mail_ssl_tls:bool=False
  use_credentails:bool=False
  validate_certs:bool=False



  class Config:
    env_file="./project/project-stock/.env"


settings=Settings()

2.2 User Verification through Token

회원등록을 마친 '준회원'에게 token이 path-parameter로 있는 link를 이메일로 보내줍니다. 말이 복잡해보이지만, 아래와 같은 단계를 거치게 됩니다.

token이 포함된 link 만들기

위와 같이 query token을 만들고, 이메일로 link로 보내주고 클릭을 하면, 'is_verified'가 True로 전달됩니다. 이것을 위해선, 아래와 같은 단계를 걸쳐야 합니다.

2.2.1 create message function

-auth    ** user계정 관리를 담은 폴더 
  |-schema.py
  |-routes.py
  |-service.py
  |-mail.py **새로 생성될 파일 **

새로 생성될 'mail.py'에 송신자의 정보와 내용들을 적을 수 있는 함수를 만들어 줍니다. 이는 아래와 같습니다.(ConnectionConfig는 우리가 이미 앞에서 설정했던 정보들을 가지고 오는 것이니, 명칭을 매칭하는데 주의하세요)

from fastapi_mail import FastMail,ConnectionConfig,MessageSchema,MessageType
from src.config import settings
from pathlib import Path

BASE_DIR=Path(__file__).resolve().parent

mail_config=ConnectionConfig(
  MAIL_USERNAME=settings.naver_username,
  MAIL_PASSWORD=settings.naver_password,
  MAIL_FROM=settings.naver_from_email,
  MAIL_PORT=settings.naver_port,
  MAIL_SERVER=settings.naver_server,
  MAIL_FROM_NAME=settings.naver_from_name,
  MAIL_STARTTLS=True,
  MAIL_SSL_TLS=False,
  VALIDATE_CERTS=True,
  #TEMPLATE_FOLDER=Path(BASE_DIR,"templates")
)


mail=FastMail(mail_config)

def create_message(recipients:list[str],subject:str,body:str):
  message=MessageSchema(recipients=recipients,subject=subject,body=body,subtype=MessageType.html)
  return message

```

2.2.2 user authorization service를 위한 class설정

section 10 Authservice 의 멤머변수인 create_user는 단순히 계좌등록만 하고 끝났습니다.이번에는 그 이어서 이메일 추가인증을 하는 코드를 작성하겠습니다.

  async def send_email(self,email:str,subject:str,html:str):
    message=create_message(
              recipients=[email],
              subject=subject,
              body=html
            )

    await mail.send_message(message)


  async def create_user(self,account:UserInput,session:AsyncSession):
      user_dict=account.model_dump()

      email=user_dict['email']
      #check if  the requested email exists in our current database
      result= await self.get_user_by_email(email,session)


      if result:
        raise UserExist()


      #create a string of hex digits in standard form
      new_user=User(
        updated_at=datetime.now(),
        **user_dict
      )

      #hasing the confidential information
      new_user.passowrd_hash=hash(user_dict['password_hash'])
      #set the role as 'user'
      new_user.role="user"
      session.add(new_user)
      await session.commit()


      #create a token for verication via email
      token=create_url_safe_token({"email":email})


      link=f"http://127.0.0.1:8000/users/verify/{token}"

      html = f"""
       <h1>Verify your Email</h1>
       <p>Please click this <a href="{link}">link</a> to verify your email</p>
       """
      emails=[email]

      await self.send_email(
        email=email,
        subject="Finish verification via Email",
        html=html
        )

      return True

이때, token을 만들 함수와 이를 다시 풀어줄 함수를 만들 필요가 있습니다. 이를 위해선 아래와 같으 함수를 따로 빼서 utils.py에 정해줍니다.


def create_url_safe_token(data:dict):
  serializer=URLSafeSerializer(
    secret_key=settings.secret_key,salt="email-configuration"
  )

  token=serializer.dumps(data,salt="email-configuration")  
  return token


def decode_url_safe_token(token:str):
  try:
    token_data=serializer.loads(token)
    return token_data

  except Exception as e:
    logging.error(str(e))

또한, 위에 작성한 link는 여러분들이 원하는 domain과 path 주소로 작성하시면 됩니다.이로써 우리는 user에게 자신의 등록한 이메일을 토대로 한, link를 메일로 보내줄 수 있습니다.

2.2.3 user_verification through email (2)

User가 이메일로 받은 link를 클릭하기 전까지는, 아직 우리는 is_verified를 false로 만들어야 합니다. 만약, user_verification을 위한 link주소를클릭을 하였다면,우리는 인증이 된거라 간주하면 되겠군요. 이를 위해선, 우리는 일단 service.py에 있는 class AuthSertice에 추가적인 함수를 제공합니다.

이 update_user함수는 받아들인 정보를 통해서, account-user의 정보를 수정하는 역할을 합니다.


class AuthService:

  async def update_user(self,user:User,user_data:dict,session:AsyncSession):
    for k,v in user_data.items():
      setattr(user,k,v)


    await session.commit()

    return user

위 코드를 작성했디면, router.py에 아래와 같은 코드를 추가합니다. 이 코드는 path-parmeter가 포함된 token을 디인코딩화하여 유저의 데이터를 업데이트를 완료하는 코드입니다.



@auth_router.get("/verify/{token}")
async def verify_user_account(token:str,session:AsyncSession=Depends(get_session)):

  token_data=decode_url_safe_token(token)

  user_email=token_data.get("email")

  if user_email:
    user=await auth_service.get_user_by_email(user_email,session)

    if not user:
      raise UserNotFound()

    await auth_service.update_user(user,{"is_verified":True},session)

    return JSONResponse(
            content={"message": "Account verified successfully"},
            status_code=status.HTTP_200_OK,
        )



  return JSONResponse(
        content={"message": "Error occured during verification"},
        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
    )

 

 

이메일 인증 예시

 

윗 메일처럼 이메일 인증하라는 메시지가 전달되고, link를 클릭하는 순간 이메일 인증이 완료됩니다. 

 

인증 완료된 결과

 

                                  

이로써 우리는 메일인증을 통해서, 데이터를 업뎃하는 방법을 알아보았습니다.아직 더 추가할 내용이 많기에, 여기서 끝내고 다음 section에서 더 알아보겠습니다.