flask 서버에서 회원가입 및 로그인 구현하기

임찬수·2021년 11월 2일
0

해시함수란 무엇인가?

  • 알고리즘의 한 종류로서 임의의 데이터를 입력 받아 항상 고정된 길이의 임의의 값으로 변환해주는 함수를 말한다.

특징

  • 해시함수 SHA256은 어떤 길이의 입력값을 넣어도 항상 256바이트의 결과값이 나온다.
  • 동일한 입력값은 항상 같은 결과값이 나온다.
  • 입력값이 조금이라도 다르다면 완전히 다른 값이 나온다.
  • 결과값을 통해 입력값을 알아내는것이 불가능하다.

어디에 사용할까?

  • 회원가입 시 비밀번호를 암호화하여 패스워드의 정보를 보안 할 수 있다.

회원가입 구현하기

필요 라이브러리

  • hashlib
  • pymongo

코드

# [회원가입 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'}) 

JWT (Json Web Token) 이란?


JSON Web Token (JWT) 은 웹표준 (RFC 7519) 으로서 두 개체에서 JSON 객체를 사용하여 가볍고 자가수용적인 (self-contained) 방식으로 정보를 안전성 있게 전달해줍니다.

JWT 는 필요한 모든 정보를 자체적으로 지니고 있습니다. JWT 시스템에서 발급된 토큰은, 토큰에 대한 기본정보, 전달 할 정보 (로그인시스템에서는 유저 정보를 나타내겠죠?) 그리고 토큰이 검증됐다는것을 증명해주는 signature 를 포함하고있습니다

JWT는 Header, Payload, Signature로 이루어져있다.
각각의 정보에 담겨있는 내용은 아래와 같다.

# Header
{ 
	"alg": "HS256",  // Signature를 해싱하기 위한 알고리즘을 지정
	"typ": JWT 	// 토큰의 타입을 지정
}

Payload

토큰에 담을 정보가 들어있으며, 담겨있는 정보의 조각을 Claim이라고 부르며 key:value의 쌍으로 이뤄져있다.
클레임의 종류는 크게 세 분류로 나뉘어져있다.

#등록된 클레임 (registered claim)
등록된 클레임들은 서비스에서 필요한 정보들이 아닌, 토큰에 대한 정보들을 담기위하여 이름이 이미 정해진 클레임들이며, 등록된 클레임의 사용은 모두 선택적 (optional)이다.

#공개 클레임 (public claim)
공개 클레임들은 충돌이 방지된 (collision-resistant) 이름을 가지고 있어야 합니다. 충돌을 방지하기 위해서는, 클레임 이름을 URI 형식으로 짓습니다.

#비공개 클레임 (private claim)
등록된 클레임도아니고, 공개된 클레임들도 아닙니다. 양 측간에 (보통 클라이언트 <->서버) 협의하에 사용되는 클레임 이름들입니다. 공개 클레임과는 달리 이름이 중복되어 충돌이 될 수 있으니 사용할때에 유의해야합니다.

# Payload

{
	# 등록된 클레임
    
    "iss" : 토큰 발급자 (issuer)
    "sub" : 토큰 제목 (subject)
    "aud" : 토큰 대상자 (audience)
    "exp" : 토큰 만료시간 (expiration)
    "nbf" : Not Before 를 의미하며, 토큰의 활성 날짜와 비슷한 개념입니다. 여기에도 NumericDate 형식으로 날짜를 지정하며, 이 날짜가 지나기 전까지는 토큰이 처리되지 않습니다.
    "iat" : 토큰이 발급된 시간 (issued at)
    "jti" : JWT의 고유 식별자로서, 주로 중복적인 처리를 방지하기 위하여 사용됩니다. 일회용 토큰에 사용하면 유용합니다.
    
    	# 공개 클레임
    "https://velopert.com/jwt_claims/is_admin": true
    
    # 비공개 클레임
    "username": "tester"
}

Signature

Signature(서명)은 Header의 인코딩값과 Payload의 인코딩값을 합친 후 주어진 비밀키로 해시를 하여 생성한다.

로그인 구현하기

필요 라이브러리

  • pyjwt
  • datetime

로직

  1. 유저가 ID, PW를 입력 후 로그인 시도
  2. 입력받은 PW값을 해시값으로 변환
  3. 유저의 ID, 변환된 해시값으로 DB에 저장된 유저 정보를 조회
    (회원가입 시 PW를 해시값으로 암호화하여 저장하였기 때문에 해시값으로 조회한다.)
  4. 조회한 유저 정보가 있으면 -> 토큰 발급
    (토큰 발급시 payload를 작성 한 후 jwt 라이브러리를 활용하여 token값을 만든다. )
    그렇지 않으면 -> 로그인 실패
  5. 발급된 토큰 값을 클라이언트단에 전달하여 클라이언트 쿠키값으로 저장
  6. 유저가 로그인을 통과하면 저장된 쿠키값을 통해 토큰을 다시 복호화하여 payload에 담겨있는
    유저정보를 확인
# [로그인 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').decode('utf-8')

        # token을 줍니다.
        return jsonify({'result': 'success', 'token': token})
    # 찾지 못하면
    else:
        return jsonify({'result': 'fail', 'msg': '아이디/비밀번호가 일치하지 않습니다.'})
@app.route('/')
def home():
    token_receive = request.cookies.get('mytoken')
    try:
        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"])
    except jwt.ExpiredSignatureError:
        return redirect(url_for("login", msg="로그인 시간이 만료되었습니다."))
    except jwt.exceptions.DecodeError:
        return redirect(url_for("login", msg="로그인 정보가 존재하지 않습니다."))
profile
프론트엔드 개발자가 되기 위한 정보를 정리합니다.

0개의 댓글

관련 채용 정보