Curriculum/AI웹개발자_내일배움캠프

Story 4. TIL_(새로운 팀, 와이어프레임, JWT)

작은코딩 2021. 12. 28. 21:26

매주 몇 시간을 들여 WIL을 쓰다 보니 '그냥 매일 간단하게 TIL 쓰면 안돼?'라는 생각이 들기 시작했다.

 

그래서 TIL을 시작하려 하는데,, 이것도 시간 많이들거 같다는 불길한 생각이,,,(최대한 간결하게 적어보겠다)

 

그럼 Let's go!

 


 

1. 새로운 팀

2주동안 정들었던 제로투원을 떠나보내고 새로운 팀인 9velopment에 들어갔다. 

 

(스파르타 양반,, 팀프로젝트가 처음 시작하는데 정말 이러기야? 최소한 첫 프로젝트는 정든 팀에서 진행을 할 수 있게 해주지 ㅠ)

 

첫 만남이라 어색함이 가득했지만 그래도 어쩔 수 없다!

 

자기소개 및 간단한 tmi를 진행하고 바로 프로젝트 구상에 돌입!!

 

앞으로 3주간 함께할 9velopment 화이팅~

 


 

2. 와이어프레임

그렇게 정해진 프로젝트명은 9k밥.

 

간단하게 설명하면 9천 원 미만의 맛집을 공유할 수 있는 sns를 구현하는 프로젝트로 국밥, 구 케이 밥, 등으로 읽을 수 있다.

 

내가 맡은 역할은 백엔드 쪽인데,, 솔직히 아직 뭘 해야 될지 구체적이지가 않다. 일단 로그인 기능 flask 등을 중점으로 공부해서 프로젝트에 도움이 되도록 힘써보자!

 

소제목으로 와이어프레임을 적어놓고 프로젝트 설명만 한 거 같은데 처음으로 와이어프레임을 그려봐서 제목으로 적어봤다. (사실 이런 프로젝트 자체가 처음이잖어,,)

 

부끄럽지만 수기로 적은 내 첫 와이어프레임

로그인 / 회원가입
메인 페이지
마이 페이지
글쓰기

 


 

3. JWT

프로젝트를 하나하나 구상해 나가던 중 발견한 이상한 녀석,, (얘는 또 뭐야 ㅠ)

 

알아보니 사용자를 인증하는 방법 중 하나로 엄청 긴 이상한 토큰을 생성해서 이 사용자가 맞는 사용자인지 확인하는 절차였다. 

 

인증 과정은 사용자가 로그인을 하면 토큰을 생성하여 보내주고 사용자가 서버에 요청을 할 때마다 토큰을 같이 보내서 "나 로그인한 사용자야!" 어필을 하면 서버는 이게 맞는 토큰인지 확인하고, 맞다면 요청에 응답한다. (맞는 건가,,?)

 

사용자 로그인 -> 로그인 성공 -> 서버에서 토큰 생성 -> 사용자에게 토큰 보내주기 -> 사용자의 요청 -> 요청과 함께 토큰도 서버로 전달 -> 토큰 진위여부 판별 -> 판별 성공 -> 사용자 요청에 응답

 

대략적인 이론(?)을 머리에 넣고 예제 코드를 보러 갔는데 일주일 만에 보게 된 flask,, (너 뭔가 낯설다,,)

 

from flask import Flask, render_template, jsonify, request, session, redirect, url_for

app = Flask(__name__)

from pymongo import MongoClient

client = MongoClient('')
db = client.dbsparta_plus_week4

# JWT 토큰을 만들 때 필요한 비밀문자열입니다. 아무거나 입력해도 괜찮습니다.
# 이 문자열은 서버만 알고있기 때문에, 내 서버에서만 토큰을 인코딩(=만들기)/디코딩(=풀기) 할 수 있습니다.
SECRET_KEY = ''

# JWT 패키지를 사용합니다. (설치해야할 패키지 이름: PyJWT)
import jwt

# 토큰에 만료시간을 줘야하기 때문에, datetime 모듈도 사용합니다.
import datetime

# 회원가입 시엔, 비밀번호를 암호화하여 DB에 저장해두는 게 좋습니다.
# 그렇지 않으면, 개발자(=나)가 회원들의 비밀번호를 볼 수 있으니까요.^^;
import hashlib


#################################
##  HTML을 주는 부분             ##
#################################
@app.route('/')
def home():
		# 현재 이용자의 컴퓨터에 저장된 cookie 에서 mytoken 을 가져옵니다.
    token_receive = request.cookies.get('mytoken')
    try:
				# 암호화되어있는 token의 값을 우리가 사용할 수 있도록 디코딩(암호화 풀기)해줍니다!
        payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
        user_info = db.user.find_one({"id": payload['id']})
        return render_template('index.html', nickname=user_info["nick"])
		# 만약 해당 token의 로그인 시간이 만료되었다면, 아래와 같은 코드를 실행합니다.
    except jwt.ExpiredSignatureError:
        return redirect(url_for("login", msg="로그인 시간이 만료되었습니다."))
    except jwt.exceptions.DecodeError:
		# 만약 해당 token이 올바르게 디코딩되지 않는다면, 아래와 같은 코드를 실행합니다.
        return redirect(url_for("login", msg="로그인 정보가 존재하지 않습니다."))


@app.route('/login')
def login():
    msg = request.args.get("msg")
    return render_template('login.html', msg=msg)


@app.route('/register')
def register():
    return render_template('register.html')


#################################
##  로그인을 위한 API            ##
#################################

# [회원가입 API]
# id, pw, nickname을 받아서, mongoDB에 저장합니다.
# 저장하기 전에, pw를 sha256 방법(=단방향 암호화. 풀어볼 수 없음)으로 암호화해서 저장합니다.
@app.route('/api/register', methods=['POST'])
def api_register():
    id_receive = request.form['id_give']
    pw_receive = request.form['pw_give']
    nickname_receive = request.form['nickname_give']

    pw_hash = hashlib.sha256(pw_receive.encode('utf-8')).hexdigest()

    db.user.insert_one({'id': id_receive, 'pw': pw_hash, 'nick': nickname_receive})

    return jsonify({'result': 'success'})


# [로그인 API]
# id, pw를 받아서 맞춰보고, 토큰을 만들어 발급합니다.
@app.route('/api/login', methods=['POST'])
def api_login():
    id_receive = request.form['id_give']
    pw_receive = request.form['pw_give']

    # 회원가입 때와 같은 방법으로 pw를 암호화합니다.
    pw_hash = hashlib.sha256(pw_receive.encode('utf-8')).hexdigest()

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

    # 찾으면 JWT 토큰을 만들어 발급합니다.
    if result is not None:
        # JWT 토큰에는, payload와 시크릿키가 필요합니다.
        # 시크릿키가 있어야 토큰을 디코딩(=암호화 풀기)해서 payload 값을 볼 수 있습니다.
        # 아래에선 id와 exp를 담았습니다. 즉, JWT 토큰을 풀면 유저ID 값을 알 수 있습니다.
        # exp에는 만료시간을 넣어줍니다. 만료시간이 지나면, 시크릿키로 토큰을 풀 때 만료되었다고 에러가 납니다.
        payload = {
            'id': id_receive,
            'exp': datetime.datetime.utcnow() + datetime.timedelta(seconds=5)
        }
        token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')

        # token을 줍니다.
        return jsonify({'result': 'success', 'token': token})
    # 찾지 못하면
    else:
        return jsonify({'result': 'fail', 'msg': '아이디/비밀번호가 일치하지 않습니다.'})


# [유저 정보 확인 API]
# 로그인된 유저만 call 할 수 있는 API입니다.
# 유효한 토큰을 줘야 올바른 결과를 얻어갈 수 있습니다.
# (그렇지 않으면 남의 장바구니라든가, 정보를 누구나 볼 수 있겠죠?)
@app.route('/api/nick', methods=['GET'])
def api_valid():
    token_receive = request.cookies.get('mytoken')

    # try / catch 문?
    # try 아래를 실행했다가, 에러가 있으면 except 구분으로 가란 얘기입니다.

    try:
        # token을 시크릿키로 디코딩합니다.
        # 보실 수 있도록 payload를 print 해두었습니다. 우리가 로그인 시 넣은 그 payload와 같은 것이 나옵니다.
        payload = jwt.decode(token_receive, SECRET_KEY, algorithms=['HS256'])
        print(payload)

        # payload 안에 id가 들어있습니다. 이 id로 유저정보를 찾습니다.
        # 여기에선 그 예로 닉네임을 보내주겠습니다.
        userinfo = db.user.find_one({'id': payload['id']}, {'_id': 0})
        return jsonify({'result': 'success', 'nickname': userinfo['nick']})
    except jwt.ExpiredSignatureError:
        # 위를 실행했는데 만료시간이 지났으면 에러가 납니다.
        return jsonify({'result': 'fail', 'msg': '로그인 시간이 만료되었습니다.'})
    except jwt.exceptions.DecodeError:
        return jsonify({'result': 'fail', 'msg': '로그인 정보가 존재하지 않습니다.'})


if __name__ == '__main__':
    app.run('0.0.0.0', port=5000, debug=True)

 

구글링을 해보니 주소에 아이디랑 비번이 노출되면 보안에 좋지 않다고 하여 몽고 db 주소와 시크릿키는 삭제했다. 

(주소 노출 안되게 공유하는 방법도 찾아봤는데 환경변수와 파이썬을 이용하는 방법이 있었다.)

 

코드를 쭉 읽어봤는데 당연하게도 한번에 이해하진 못했다.

 

내일은 코드를 html과 비교하며 두세 번 반복해서 읽어보고 어려운 부분은 구글님에게 물어보는 걸로~

 

(코드를 돌려서 실제 구현해봤는데 이게 신기하게 된다,, 왜 되는지는 모르겠지만,,)

 

 

- 끝 -