수학과의 좌충우돌 프로그래밍

[smileGate] 2차 과제 (2) Auth 서버 구현 본문

스마일게이트

[smileGate] 2차 과제 (2) Auth 서버 구현

ssung.k 2020. 2. 4. 23:48

2차 과제는 1차 과제에 비해 규모가 좀 있어서 여러 포스팅으로 나누어서 올릴려고 했지만 실패했습니다.

1차 과제 때와 마찬가지로 구현한 방법과 그에 대한 코드 리뷰를 중심으로 포스팅 하도록 하겠습니다.

2차 과제는 Auth 서버였고 필요한 기능은 다음과 같습니다.

  • 가입, 로그인 페이지
  • 유저 관리 페이지
  • 인증 서버 (API)
  • MySQL DB 사용
  • Password Encryption
  • 캐시
  • E-Mail 인증
  • 비밀번호 찾기

 

가입, 로그인 페이지

가입, 로그인 페이지는 html,css,js 를 통해 구현하였습니다.

API 서버와의 통신은 fetch 를 통해 진행하였습니다.

항상 API 서버만 개발하고 클라이언트 쪽은 팀원과 협업을 통해 진행하였어서 놓치는 부분이 많았습니다.

특히 request headers 부분을 설정해주는 것이 중요하다는 걸 깨달았습니다.

다음과 같이 보내는 데이터 타입, 받는 데이터 타입, csrf_token 등을 함께 보내주었습니다.

// myauth/js/find_password.js

fetch('http://localhost:8000/find_password/', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'X-CSRFToken': inputcsrfToken.value
    },
    body: JSON.stringify({
        username: inputUsername.value,
    })
})

 

유저 관리 페이지

유저 관리 페이지는 django 의 기본 admin 을 사용했습니다.

# myauth/admin.py

from django.contrib import admin
from .models import MyUser, Salt


@admin.register(MyUser)
class MyUserAmin(admin.ModelAdmin):
    list_display = ['username', 'email', 'is_active']

 

인증 서버 API

인증 서버는 django restframework 를 사용했습니다.

자세한 코드는 github 링크를 공유하도록 하겠습니다.

renderer

개발 중 생긴 가장 큰 이슈는 renderer 처리였습니다.

일반적으로 get 요청에 대해서는 html 을 render 해주고 post 요청에서는 json 응답을 해주었습니다.

renderer_classes 를 다음과 같이 설정해주었지만 계속 에러가 발생했습니다.

# myauth/views.py

class CreateUserView(APIView):
    permission_classes = [permissions.AllowAny]
    renderer_classes = [JSONRenderer, TemplateHTMLRenderer]
    # 생략

가입, 로그인 페이지 에서도 이야기 했지만 headers 에 Accept 를 지정해줌으로서 상황에 맞는 데이터를 response 로 받을 수 있었습니다.

 

error handling

다른 지적 사항으로는 에러 핸들링이었습니다.

나름 이곳저곳에서 처리를 해주었지만 DB 와 관련된 곳에서는 무조건 에러 핸들링이 필요했지만 그러지 못했습니다.

# myauth/views.py

def post(self, request, *args, **kargs):
    user = request.data.get('user')
    if not user:
        return Response({
            'response': 'error',
            'message': 'No data found'
        })

 

DB 의 문제로 에러가 발생할 수 있기 때문에 django ORM 에 대해서 create , get , update 등 여러 부분에서 에러 핸들링을 추가해야 할 듯 합니다.

예를 들어 다음과 같이 create 하는 곳에서도 핸들링이 필요합니다.

# myauth/serializers.py

def create(self, validated_data):
    user = MyUser.objects.create(
        username=validated_data['username'],
        email=validated_data['email'],
    )
    user.set_password(validated_data['password'])
    user.is_active = False
    user.save()
    return user

 

response

이 부분은 그래도 칭찬받은 부분입니다.

다음 예시는 에러 핸들링 후에 그에 따른 response 를 나타낸 겁니다.

해당 코드에서 사용하는 Response 를 타고 계속 올라가면 status 가 default로 200입니다.

물론 따로 명시해줄 수도 있고요.

이 같이 서버와의 통신은 잘 되었지만 에러가 났다고 해서 response status 를 변경해서는 안됩니다.

비록 에러가 났지만 통신은 잘 되었기에 status 200으로 return 해야합니다.

def post(self, request, *args, **kargs):
    user = request.data.get('user')
    if not user:
        return Response({
            'response': 'error',
            'message': 'No data found'
        })

 

MySQL DB

DB 로는 MySQL 을 사용하였습니다.

아래 이미지는 user model 의 스키마입니다.

django 기본 user 를 커스텀하였기 때문에 익숙한 필드들이 많이 보입니다.

이제 대해서 많은 지적을 받았습니다.

db_table

 

 

is_active, is_admin, is_superuser, is_staff

django는 활성화 유저, admin 페에지 접속 가능 유저, 최종관리자 등 여러 부분을 각각 Boolean Field 로 관리하고 있습니다.

그에 따라 같은 방식으로 구현하였는데 이 부분에서 지적을 받았습니다.

이럴 경우 추가적인 상황에 대해 대처를 하지 못하므로 level 로서 구분하는 방식을 택하는게 더 선호된다고 합니다.

예를 들면 이메일 인증을 못받은 유저는 0, 이메일 인증을 받은 유저는 1, 관리자는 2 이런 방식으로 말이죠.

이렇게 하면 django 에서 커스튬해야 할 부분이 정말 많아 질 것 같지만 차차 해보도록 하겠습니다.

 

datetime field

실제로 datetime field 와 date field 는 많이 햇갈린다고 합니다.

어떤 필드로 구성되어 있는지 확인을 하면 되지만 매번 확인할수도 없는 노릇이죠.

그래서 필드명으로 이를 구분해줍니다.

위와 같은 경우에도 date_joined 필드를 joined_dt 와 어떤 자료형인지 명시해줌으로서 혼란을 방지할 수 있습니다.

 

update_dt

회원가입한 시기 말고 정보 수정을 마친 시기도 저장을 해야 합니다.

이를 통해 비밀번호 3개월 주기 변경 등의 알람을 해줄 수 있는데 이는 update_dt 를 비밀번호에만 한정할 것이냐, 모든 필드에 대해 맞출 것이냐의 선택사항이 생깁니다.

원하는 서비스에 맞게 진행하면 될 것 같습니다.

 

user 의 삭제

실제 product 에서는 왠만해서는 유저 정보를 삭제하지 않는다고 합니다.

유저가 탈퇴했다고 정보를 삭제한다면, 나중에 환불처리나 여러 이슈가 생길 수 있기 때문이죠.

따라서 탈퇴한 유저도 level 에 적절히 추가하여 데이터는 남기고 활성화는 시키지 않는다고 합니다.

 

기타

기타 여러 지적을 받았습니다.

id 필드는 의미 상 위 쪽에 존재해야하는데 아래에 있는 점, varchar 의 크기가 필요 이상으로 큰 점 등 수정해야 할 부분이 많습니다.

 

Password Encryption

https://ssungkang.tistory.com/entry/smileGate-2%EC%B0%A8-%EA%B3%BC%EC%A0%9C-1-password-%EC%A0%80%EC%9E%A5%EB%B0%A9%EB%B2%95-hash-function

 

[smileGate] 2차 과제 (1) password 저장방법, hash function

어느 사이트에 회원가입 및 로그인 할 때 아이디와패스워드를 입력합니다. 서버에서는 해당 정보들을 DB 에 저장할텐데 패스워드는 어떻게 저장이 되고 있을까요? 만약패스워드를 평문 그대로 저장하고 있다면, 해..

ssungkang.tistory.com

기본적으로 다음과 같은 방식으로 암호화를 해야합니다.

이미 django 에서 password 하는 과정을 비슷하게 제공해주고 있어 이를 그대로 사용했습니다.

password 를 저장하는 set_password 함수를 보면 salt 값을 사용하여 raw_password 를 여러 번 암호화하게 됩니다.

def set_password(self, raw_password):
    salt = md5(os.urandom(128)).hexdigest()[:9]

    hashed = sha1(
        (salt + sha1(
            (salt + sha1(
                raw_password.encode('utf8')
            ).hexdigest()).encode('utf8')
        ).hexdigest()).encode('utf8')
    ).hexdigest()

    self.salt = salt
    self.password = hashed

 

캐시

auth 서버만 구현하다보니 캐싱할 부분이 많지는 않았습니다.

우선 어느 부분을 캐싱했는지 설명하기 위해서는 인증 방법을 설명해야 하는데 jwt 토큰 방식을 사용하였습니다.

로그인 하는 순간 토큰을 발급하여서 클라이언트 쪽에서는 Cookie 에 저장하고 서버 쪽에서는 redis 에 저장해서 토큰을 캐싱하였습니다.

redis 를 사용하게 되면 expired 시간을 설정할 수 있어서 토큰을 자동으로 만료시킬 수 있습니다.

단순히 캐시의 역할 뿐 아니라 expired 의 역할도 함께 수행할 수 있죠.

request 에는 Cookie가 함께 넘어오기 때문에 넘어오는 순간 인증된 유저인지 확인하기 위해서 redis 의 토큰을 캐싱하여 값을 비교하게 됩니다.

이 부분에 대해서 좀 더 좋은 방법을 모색해 다음 포스팅에서 코드까지 함께 첨부하도록 하겠습니다.

 

E-mail 인증

django smtp

 

[Django] 회원가입 시 이메일 인증, SMTP

SMTP SMTP 는 Simple Mail Transfer Protocol 의 약자로 전자 메일 전송을 위한 표준 프로토콜입니다. 이를 이용해서 인증메일을 보내보도록 하겠습니다. 사전 설정 IMAP 설정 : 링크로 들어가서 IMAP 1단계 설정..

ssungkang.tistory.com

이메일 전송으로는 smtp 를 사용하였습니다.

이에 대한 자세한 사항은 위 링크에서 확인 할 수 있습니다.

email 인증 방식은 다음과 같습니다.

  • 회원가입 시 유저는 is_active 값이 False 입니다.
  • 이 때 redis 에 값을 저장하는데 key 값은 uuid, value 는 user.id 를 저장합니다.
  • 동시 해당 uuid 값을 이메일로 전송합니다.
  • 해당 이메일로 접속할 경우 uuid 값으로 redis 에서 user.id 를 얻어 해당 유저의 is_activeTrue 로 바꿔줍니다.

 

비밀번호 찾기

비밀번호 찾기도 E-mail 인증 과 같은 방식으로 진행됩니다.

이메일을 통해 해당 uuid 를 얻고 이로 접속할 경우에만 비밀번호 변경 화면을 제공합니다.

 

 

Auth 서버를 거의 다 구현했다고 생각했는데 여기저기서 조금씩 수정을 해야할 듯 합니다.

좀 더 다듬어서 완벽한 코드를 만들도록 하겠습니다.

Comments