본문 바로가기

PYTHON-BACK

# 24.10.21_ 마켓컬리 클론코딩3

728x90

이번에는 로그인 페이지에서 아이디 찾기, 비밀번호 찾기를 구현해보려고 했다.

Django에서 이메일을 보내기 위해서는 다양한 방법이 있는데, 저는 Google SMTP서버를 이용해서 이메일을 전송하는 방식을 선택했습니다.

 

 

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'  # Gmail SMTP 사용
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = '***'  # 여기서 본인의 이메일 주소 입력
EMAIL_HOST_PASSWORD = '***'  # 여기서 본인의 이메일 비밀번호 입력
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER

 

장고에서 이메일 보내기 위한 이메일 설정 (settings.py)에 작성하면 되는 것으로
저같은 경우는 이메일과 비밀번호 넣어두면 되는데 보안 문제로 2차 인증 활성화 해서 패스키 새로 만들어서 이용
현재 ***로 가려져 등록되어 있어서 확인해보시고 싶으시면 자신 구글 아이디랑 비밀번호 넣으면 됩니다.

 

이제 위에 내용을 정리하면 아래와 같다.

  • EMAIL_BACKEND: Django가 사용할 이메일 백엔드. 여기서는 SMTP를 사용.
  • EMAIL_HOST: Gmail SMTP 서버.
  • EMAIL_PORT: SMTP 서버에서 사용하는 포트 (587번).
  • EMAIL_USE_TLS: 보안 연결(TLS)을 사용할 것인지 여부.
  • EMAIL_HOST_USER: 보낼 이메일 주소.
  • EMAIL_HOST_PASSWORD: 이메일 비밀번호.
  • DEFAULT_FROM_EMAIL: 이메일을 보낼 때 사용할 기본 발신자 이메일 주소.

다음으로 아이디 뷰 (find_username)

이 함수는 사용자가 입력한 이메일을 기준으로 아이디를 찾아 이메일로 보내는 기능을 구현한 것으로 

def find_username(request):
    User = get_user_model()  # CustomUser 모델 가져오기
    if request.method == 'POST':
        email = request.POST.get('email')
        try:
            user = User.objects.get(email=email)  # 해당 이메일을 가진 사용자를 찾음
            # 아이디를 이메일로 전송
            send_mail(
                'Your Username',
                f'Your username is: {user.username}',
                settings.DEFAULT_FROM_EMAIL,
                [email],
                fail_silently=False,
            )
            # 이메일 전송 후 로그인 페이지로 리다이렉트
            return redirect('login')
        except User.DoesNotExist:
            # 해당 이메일이 존재하지 않으면 에러 메시지 출력
            return render(request, 'users/find_username.html', {'error': 'Email not found.'})

    return render(request, 'users/find_username.html')

 

  • User: get_user_model()로 커스텀 사용자 모델을 가져오는 역할
  • 사용자가 이메일을 제출하면, 해당 이메일로 등록된 사용자를 찾아 이메일로 아이디를 전송.
  • 이메일이 존재하지 않으면, 에러 메시지를 표시.
<h2>아이디찾기</h2>
<form method="POST">
    {% csrf_token %}
    <input type="email" name="email" placeholder="Enter your email" required>
    <button type="submit">아이디찾기</button>
</form>
{% if message %}
    <p>{{ message }}</p>
{% elif error %}
    <p>{{ error }}</p>
{% endif %}
  • 아이디 찾기 템플릿으로 사용자가 이메일을 입력하여 아이디를 찾을 수 있는 폼이다.

아이디 찾기 구현한것으로 해당 칸에 이메일을 입력하고 아이디 찾기를 누르게 되면 

 

해당 사진처럼 메일이 오게 되고 해당 형광펜 부분에 있는것이 이메일 주소를 등록했던 아이디 이다.

비밀번호뷰 (reset_password)

이 함수는 사용자가 비밀번호를 재설정할 수 있는 링크를 이메일로 보내는 역할을 하는 것으로

def reset_password(request):
    User = get_user_model()  # CustomUser 모델 가져오기
    if request.method == 'POST':
        email = request.POST.get('email')
        try:
            user = User.objects.get(email=email)
            token = default_token_generator.make_token(user)
            uid = urlsafe_base64_encode(force_bytes(user.pk))
            link = request.build_absolute_uri(f'/users/reset-password-confirm/{uid}/{token}/')

            # 비밀번호 재설정 링크를 이메일로 전송
            send_mail(
                'Reset your password',
                f'Click the link to reset your password: {link}',
                settings.DEFAULT_FROM_EMAIL,
                [email],
                fail_silently=False,
            )
            return redirect('login')
        except User.DoesNotExist:
            return render(request, 'users/reset_password.html', {'error': 'Email not found.'})
        except Exception as e:
            return render(request, 'users/reset_password.html', {'error': 'Failed to send email. Please try again.'})

    return render(request, 'users/reset_password.html')

 

 

 

  • token: 비밀번호 재설정에 사용될 보안 토큰을 생성.
  • uid: 사용자의 ID를 안전하게 인코딩한 값.
  • link: 비밀번호 재설정 페이지의 링크를 생성.
  • 해당 링크가 사용자의 이메일로 전송되며, 이메일이 없으면 에러 메시지를 표시.
<h2>비밀번호 재설정</h2>
<form method="POST">
    {% csrf_token %}
    <input type="text" name="username" placeholder="Enter your username" required>
    <input type="email" name="email" placeholder="Enter your email" required>
    <button type="submit">비밀번호 재설정</button>
</form>

{% if message %}
    <p>{{ message }}</p>
{% elif error %}
    <p>{{ error }}</p>
{% endif %}

 

  • 비밀번호를 재설정 하기 위해 요청할 수 있도록 구성된 폼으로, 사용자 이름과 이메일을 입력받고, 요청이 성공 또는 실패할 경우 메시지를 표시한다.

 

비밀번호 변경을 위해 메일을 보내기 위한 창으로 해당 유저 아이디와, 메일 주소를 적으면 해당 메일로 비밀번호 재설정 폼이 전송되어진다.

 

  • 재설정 링크에서 reset-password-confirm뒤에 부분에 오는 토큰은, 비밀번호 재설정 과정에서 보안을 위해 사용되는 두 가지 요소를 포함한 토큰으로
  • 첫번째 가린 부분은 사용자 ID를 인코딩한 값으로, django에서는 보안을 위해 사용자의 데이터 베이스 기본 키(PK)를 그대로 사용하지 않고 이를 URL에 안전하게 사용할 수 있도록 base64로 인코딩합니다.
    • 이 값은 urlsafe_base64_encode 함수에 의해 인코딩된 사용자 ID로, 이를 통해 비밀번호 재설정 요청이 어떤 사용자와 관련되어 있는지를 확인할 수 있다.
  • cf9reh-*** 부분은 비밀번호 재설정 토큰으로 django의 default_token_generator가 생성한 고유한 토큰으로 사용자와 요청에 대한 유효성을 확인하는 데 사용되며
  • 이 토큰이 사용자가 요청한 비밀번호 재설정이 유효한지 확인하는 보안장치로, 만약 이 토큰이 잘못되었거나 만료되었다면 비밀번호 재설정이 실패되게 되는 것입니다.

 

  • 즉, /reset-password-confirm/<uidb64>/<token>/ 형식은 django의 비밀번호 재설정에 자주 사용되는  URL 패턴으로 
  • uidb64: 사용자의 기본 키(PK)를 안전하게 base64로 인코딩한 값.
  • token: 비밀번호 재설정을 위한 고유한 보안 토큰.
  • 공통적으로 이 값들은 django의 PasswordResetConfirmView 에서 처리되게 되고, 토큰이 유효한지 검증하고, 사용자가 제공한 새 비밀번호를 저장하는 역할을 하는 것 입니다.

 

해당 링크를 들어가게 되면 아래 재설정 확인 뷰가 나오게 됩니다.

비밀번호 재설정 확인 뷰 ( reset_password_confirm)

이 함수에서는 사용자가 재설정 링크를 클릭하게 되었을때 해당 링크가 유효한지 확인하고, 새 비밀번호를 설정할 수 있는 페이지를 랜더링하는 기능을 한다.

def reset_password_confirm(request, uidb64, token):
    logger.info("Attempting to render reset_password_confirm.html")
    User = get_user_model()
    try:
        uid = urlsafe_base64_decode(uidb64).decode()
        user = User.objects.get(pk=uid)
    except (TypeError, ValueError, OverflowError, User.DoesNotExist):
        logger.warning(f"Invalid UID: {uidb64}, Token: {token}")
        user = None

    if user is not None and default_token_generator.check_token(user, token):
        if request.method == 'POST':
            form = SetPasswordForm(user, request.POST)
            if form.is_valid():
                form.save()
                return redirect('login')
        else:
            form = SetPasswordForm(user)
    else:
        form = None
    logger.info("Rendering reset_password_confirm.html")
    return render(request, 'users/reset_password_confirm.html', {'form': form})

 

 

 

  • uidb64: 인코딩된 사용자 ID.
  • token: 비밀번호 재설정에 필요한 보안 토큰.
  • 사용자가 토큰과 ID가 유효하면 비밀번호를 재설정할 수 있는 폼을 보여줌.
  • 유효하지 않은 경우 폼을 보여주지 않음.
<h2>비밀번호 재설정</h2>
<form method="POST">
    {% csrf_token %}
    {{ form.new_password1.label_tag }}
    {{ form.new_password1 }}
    {% if forhttp://m.new_password1.errors %}
      <div class="error">{{ forhttp://m.new_password1.errors }}</div>
    {% endif %}
    {{ form.new_password2.label_tag }}
    {{ form.new_password2 }}
    {% if forhttp://m.new_password2.errors %}
      <div class="error">{{ forhttp://m.new_password2.errors }}</div>
    {% endif %}
    <button type="submit">비밀번호 재설정</button>
</form>

 

  • 비밀번호 재설정 템플릿으로
  • 사용자가 새로운 비밀번호를 입력할 수 있도록 하는 폼
  • 폼 검증에 실패할 경우 에러 메시지가 출력된다.

 

해당 화면에 나오는 재설정 창에서 새 비밀번호를 입력하고 재설정 버튼을 누르게 되면 해당 아이디의 비밀번호가 재설정하게 되는 것입니다.

 

 

오늘은 Django에서 Google SMTP서버를 활용해서 아이디, 비밀번호를 찾는 방법을 마켓컬리 클론코딩에 반영을 해보았는데, 깃허브에 해당 내용을 추가하고 올리는 과정에서 맨 위쪽에서 설명했던 settings.py 부분에 아이디 비밀번호를 그대로 올리는 실수를 했었다. 

 

사실은 올린지 모르고 당연히 ** 처리 했겠지 생각을 했었는데, 메일로 GitGuardian <security@getgitguardian.com> 로 부터 메일이 하나 날라왔다.

 

SMTP 자격 증명이 해당 킷허브 레퍼지토리에서 노출됐다는 것이다.

이에 급하게 아이디와 비밀번호를 **처리 했었다.

 

해당 사건을 겪으면서 생각보다 깃허브에 코드를 올릴때 고려하고, 많이 생각해야한다는 것을 느꼈다. (진짜 놀랐다... 2차인증으로 토큰 키 받아서 비밀번호 대체한거 아니였음 큰일날 뻔 했다.)

728x90