wisePocket

[Flask] Team Project(GameInsight) 04 기능 구현시 발생한 문제와 트러블슈팅 회고, 아쉬운점 본문

Python&Flask Tutorials, AWS EB/Flask_Team_Project_GameInsight

[Flask] Team Project(GameInsight) 04 기능 구현시 발생한 문제와 트러블슈팅 회고, 아쉬운점

ohnyong 2023. 8. 12. 21:24

내가 팀에서 맡은 기능 부분은 회원 가입 및 로그인이다.

기능 구현 기능들 나열보다는 기능 구현에서 발생한 오류와 새롭게 알게된 부분에 대해서 정리해보고자 한다.

 


1. 모듈화 미흡에 대한 아쉬움

우선 app.py가 정신 없다.
4명이 작성한 기능이 모두 app.py안에서 route를 통해 맵핑되어 있다. 보기 정신이없고 찾는데도, 보수하는데도 오래걸린다. 주석으로 대체하려했지만 생각보다 많은 코드들이 사용되면서 시인성이 좋지 않았다. 이것과 관련되서 저번에 한번 찾았던 기능인 블루프린트 객체 활용하는 것이 있었는데, 기획 과정에서 이 부분을 잊고 시작해서 아쉽게도 모듈화 하지 못했다. flask에서도 blueprint를 통해 객체화 시키는 것이 가능한 것으로 보여진다. 다음 프로젝트에서는 spring으로 객체화 시키는 것 위주로 진행하겠지만, flask도 충분히 가능하다는 것을 인지하고 flask 프로젝트에서는 모듈화를 적용시켜봐야겠다 생각했다.

이 부분에 대해서 적용된 예제를 찾아봤다. 우선적으로 프로젝트 구조는 다음과 같다.


.
|ㅡ main
   |ㅡ templates
          |ㅡ*.html 파일들
   |ㅡ __init__.py
   |ㅡ board.py
   |ㅡ member.py
|ㅡ run.py

run.py에서는 main 폴더에서 app 객체를 가져와 실행만 하는 역할을 한다.
run.py
from main import app

if __name__ == "__main__":
    app.run(host='0.0.0.0', debug=True, port=5000)
[출처] [Python Flask] #20 플라스크 모듈화, 블루프린트 Blueprint|작성자 넬티아


main 폴더의 __init__.py 코드에서 app 객체를 선언하고 각종 모듈, 데이터베이스, 블루프린트 등 값을 설정한다.

# __init__.py
from flask import Flask
from flask import request
from flask import render_template
from bson.objectid import ObjectId
from flask_pymongo import PyMongo
from datetime import datetime
# 기타 필요한 모듈 선언

app = Flask(__name__) # app 객체 선언
app.config["MONGO_URI"] = "mongodb://localhost:27017/WhisperTalk" # pymongo DB 경로 설정
salt = 'neltia' # secret key
now = str(datetime.now())
myHash = hashlib.sha512(str(now + salt).encode('utf-8')).hexdigest()
app.config['SECRET_KEY'] = myHash
mongo = PyMongo(app)


from . import board  # board.py 내용 호출
from . import member # member.py 내용 호출


app.register_blueprint(board.blueprint) # 각 블루프린트에 내용 등록
app.register_blueprint(member.blueprint)
[출처] [Python Flask] #20 플라스크 모듈화, 블루프린트 Blueprint|작성자 넬티아

board.py는 게시판의 내용을 보고, 글을 쓰고, 글 목록을 확인하고, 수정하고, 삭제하는 등 게시판과 관련된 기능만 작성한다. main에서 import *로 모든 것을 불러왔기 때문에 app 객체를 불러오게 되어 __init__.py에서 초기기화한 값들을 가져올 수 있다. /board/[라우팅 경로]로 이동하면 각 기능이 수행된다. /board/view로 가면 글의 내용을 확인한다.

# board.py
from main import *
from flask import Blueprint


blueprint = Blueprint("board", __name__, url_prefix='/board')


@blueprint.route("/view/<idx>")
def board_view(idx):
    <후략>
[출처] [Python Flask] #20 플라스크 모듈화, 블루프린트 Blueprint|작성자 넬티아
member.py는 회원가입, 로그인 같은 회원과 관련된 기능만 작성한다. 마찬가지로 main에서 *를 불러오고 Blueprint로 /member에 라우팅해 /member/join으로 가면 회원가입이 동작한다.
[출처] [Python Flask] #20 플라스크 모듈화, 블루프린트 Blueprint|작성자 넬티아
from main import *
from flask import Blueprint

blueprint = Blueprint("member", __name__, url_prefix='/member')

@blueprint.route("/join", methods=["GET", "POST"])
def member_join():
    if request.method == "GET":
        <후략>
[출처] [Python Flask] #20 플라스크 모듈화, 블루프린트 Blueprint|작성자 넬티아​

블루프린트를 사용해 /board/view, /member/join처럼 라우팅이 되기 때문에 url_for() 함수로 리다이렉트를 넘길 때 다음처럼 함수만 입력해서 라다이렉트 시키면 안 되고 함수 앞에 블루프린트 객체 이름을 붙여줘야 한다.

- redirect(url_for("board.board_view", idx=idx))

- redirect(url_for("member.member_join"))

 

2. 명명 규칙, 컨벤션 지정 미흡 아쉬움

4명의 기능에 대해서 어떻게 명칭을 정할지 정확하게 구분하지 않았다. 특히 이부분은 위 모듈화가 미흡했던 부분과 연결된다.
누구는 index.html에 직접 작업
하기도 했으며 나는 별도 페이지html(ex: register.html은 회원 가입 div만 포함된 페이지)를 작성하고 block을 include하는 방식을 선택했다. 이 부분을 모두 통일시켜서 알려줬으면 좀 더 깔끔한 index.html을 유지하고, 유지보수적으로도 유리하여 어느 부분을 고쳐야 하는지도 빨리 찾을 수 있었을 것이고 다른 개발자가 열어보더라도 빠르게 해당 부분을 찾을 수 있었을 것이라 생각했다. 나는 모듈화시키기를 진행하고자했으나 이 부분을 공지하고 충분히 회의를 했어야 했는데 팀장으로써 역할을 못한 부분이다. html과 script또한 마찬가지이며, style.css까지 모듈화 시켰지만 각 기능마다 개인마다 만들었어야 되는 것을 정확히 인지 하지 못했던 점이 아쉽다. 다음 프로젝트에서는 충돌을 최대한 막을 수 있도록 컨벤션에 신경써야겠다고 생각했다.
파일이 제각각이다..

 

3. 비밀번호 암호화 방법

flask에서는 hashlib이라는 라이브러리를 통해서 비밀번호 암호화를 할 수 있다.
회원 가입시에도 해야되고 로그인 시 검증을 위해서도 동일하게 해쉬로 검색해야 한다. 이 부분에 대해서 추가적으로 찾아보니 해싱은 단방향 암호화 알고리즘이므로 원래의 문자열로 복구할 수는 없다고 한다. 따라서 검증에서도 반드시 해싱된 값끼리 검증을 하도록 설정해야 한다.
  # 회원가입 때와 같은 방법으로 pw를 암호화합니다.
    pw_hash = hashlib.sha256(pw_receive.encode('utf-8')).hexdigest()

https://docs.python.org/ko/3/library/hashlib.html

 

hashlib — Secure hashes and message digests

Source code: Lib/hashlib.py This module implements a common interface to many different secure hash and message digest algorithms. Included are the FIPS secure hash algorithms SHA1, SHA224, SHA256,...

docs.python.org

 

4. JWT 토큰을 이용한 로그인 구현

예전에 세션을 통해서 구현했었는데, 쿠키로 한다고? 어떤 방식인지 궁금했다. 배우기로는 쿠키는 보안성이 취약하다고했는데 그걸 보완한 부분인가 의심되기도했다.

우선 JWT 토큰의 흐름을 내 코드를 분해하며 분류해보았다. 
1. 사용자가 id와 password를 입력하여 로그인을 시도합니다.
@app.route('/api/login', methods=['GET', 'POST'])
def api_login():
    id_receive = request.form['id_give']
    pw_receive = request.form['pw_give']
    pw_hash = hashlib.sha256(pw_receive.encode('utf-8')).hexdigest()

    # id, 암호화된pw을 가지고 해당 유저를 찾습니다.
    result = db.user.find_one({'user_id': id_receive, 'user_password': pw_hash})

2. 서버는 요청을 확인하고 secret key(내가'서버'가 정해둬야 함)를 통해 Access token을 발급합니다.
 if result is not None:
        # JWT 토큰에는, payload와 시크릿키가 필요합니다.
        # 시크릿키가 있어야 토큰을 디코딩(=풀기) 해서 payload 값을 볼 수 있습니다.
        # 아래에선 id와 exp를 담았습니다. 즉, JWT 토큰을 풀면 유저ID 값을 알 수 있습니다.
        # exp에는 만료시간을 넣어줍니다. 만료시간이 지나면, 시크릿키로 토큰을 풀 때 만료되었다고 에러가 납니다.
        payload = {
            'user_id': id_receive,
            'exp': datetime.datetime.utcnow() + datetime.timedelta(seconds=200)
        }
        token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')

3. JWT 토큰을 클라이언트에 전달 합니다.(브라우저) 
        # token을 줍니다.
        return jsonify({'result': 'success', 'token': token})
    else:
	return jsonify({'result': 'fail', 'msg': '아이디/비밀번호가 일치하지 않습니다.'})​

-------로그인 완료-----아래는 로그인 상태인 경우-----------

4. 클라이언트에서 API 을 요청할때(로그인 상태) 클라이언트가 Authorization header에 Access token을 담아서 보냅니다.
@app.route('/api/logined', methods=['GET'])
def api_valid():
    token_receive = request.cookies.get('mytoken')​

5. 서버는 JWT Signature를 체크하고 Payload로부터 사용자 정보를 확인해 데이터를 반환합니다.
   try:
        # token을 시크릿키로 디코딩합니다.
        # 보실 수 있도록 payload를 print 해두었습니다. 우리가 로그인 시 넣은 그 payload와 같은 것이 나옵니다.
        payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
        print(payload)​

6. 클라이언트의 로그인 정보를 서버 메모리에 저장하지 않기 때문에 토큰기반 인증 메커니즘을 제공합니다.
       # 여기에선 그 예로 닉네임을 보내주겠습니다.
        userinfo = db.user.find_one({'user_id': payload['user_id']}, {'_id': 0})
        return jsonify({'result': 'success', 'user_id': userinfo['user_id'], 'user_name': userinfo['user_name'],  'user_type': userinfo['user_type']})
    except jwt.ExpiredSignatureError:
        # 위를 실행했는데 만료시간이 지났으면 에러가 납니다.
        return jsonify({'result': 'fail', 'msg': '로그인 시간이 만료되었습니다.'})
    except jwt.exceptions.DecodeError:
        return jsonify({'result': 'fail', 'msg': '로그인 정보가 존재하지 않습니다.'})

JWT 의 주요한 이점은 사용자 인증에 필요한 모든 정보는 토큰 자체에 포함하기 때문에 별도의 인증 저장소가 필요없다는 것입니다.
분산 마이크로 서비스 환경에서 중앙 집중식 인증 서버와 데이터베이스에 의존하지 않는 쉬운 인증 및 인가 방법을 제공합니다.
개별 마이크로 서비스에는 토큰 검증과 검증에 필요한 비밀 키를 처리하기위한 미들웨어가 필요합니다. 검증은 서명 및 클레임과 같은 몇 가지 매개 변수를 검사하는 것과 토큰이 만료되는 경우로 구성됩니다.
토큰이 올바르게 서명되었는지 확인하는 것은 CPU 사이클을 필요로하며 IO 또는 네트워크 액세스가 필요하지 않으며 최신 웹 서버 하드웨어에서 확장하기가 쉽습니다.

JSON 웹 토큰의 사용을 권장하는 몇 가지 이유는 다음과 같다.

JWT 장점

  • URL  파라미터와 헤더로 사용
  • 수평 스케일이 용이
  • 디버깅 및 관리가 용이
  • 트래픽 대한 부담이 낮음
  • REST 서비스로 제공 가능
  • 내장된 만료
  • 독립적인 JWT

JWT 단점

  • 토큰은 클라이언트에 저장되어 데이터베이스에서 사용자 정보를 조작하더라도 토큰에 직접 적용할 수 없습니다.
  • 더 많은 필드가 추가되면 토큰이 커질 수 있습니다. ==> 클라이언트에서 부담이 커진다는 이야기 같다. = 
  • 비상태 애플리케이션에서 토큰은 거의 모든 요청에 대해 전송되므로 데이터 트래픽 크기에 영향을 미칠 수 있습니다.



 

5. JWT, 그럼 Cookie와 Session과 또 뭐가 다른건가?

https://velog.io/@znftm97/JWT-Session-Cookie-%EB%B9%84%EA%B5%90-sphsi9yh

나와 정확히 같은 고민을하는 블로그를 찾았다. 나는 JWT가 결국 Session인가 했지만 생각해보니 Cookie처럼 클라이언트에 데이터를 남겼었다. 여기서 의문이 되었던 것이 이건 Session과 Cookie 두방식이 합쳐진 어떤 라이브러리인가? 생각했지만 또 생각해보니 

현재 프로젝트에서 session은 사용 할 줄 알고 import했었지만 사용하지도않았었고, 패키지매니저를 통해 설치조차 안한 상태였다. 그럼 JWT는 뭔가? 쿠키인가? 그거 위험한거 아닌가? 라는 생각을 했었다. 따라서 JWT가 뭔지, Session, Cookie와 다른 점이 무엇인지 잘설명되있는 페이지를 찾아야 할 필요성을 느꼈다. 위 참고 링크는 내가 충분히 이해하고 내 생각대로 그려가며 정리를 하고 나의 글로 다시 작성하고자 한다. 생각해보니 전에도 카카오 로그인을 구현하는데 Oauth토큰을 받아서 진행되었다. 이것과 관련있지 않을까 생각해본다.

우선, 정리해야 될 내용, 아래 내용들이 무엇을 의미하는지, 차이점, 장단점 등은 공부해야된다.

1. Cookie

2. Cookie + Session

3. JWT