Flask

[Flask] 웹페이지 회원가입, 로그인 기능 구현하기(원리) - hashlib, JWT, Token, Cookie

민돌v 2021. 10. 8. 13:27
728x90

오늘은 파이썬 웹페이지 회원가입 로그인 기능을 만들어보고자 한다.

로그인, 회원가입의 원리는 아래와 같다.

 

[회원가입]

  1. 회원정보를 받아온다.(ID, PW, Nickname)
  2. 회원정보 비밀번호를 해시함수로 암호화해서 데이터베이스에 저장한다. - hashlib

 

[로그인]

  1. 사용자의 아이디, 비밀번호를 받아온다.
  2. 사용자의 비빌번호를 똑같이 해시함수 값으로 암호화 한다. (암호화한 값이 저장되어있기 때문에)
  3. id, 암호화된  pw로 db내에 해당 유저를 찾는다
  4. 해당 유저를 찾으면 JWT 토튼을 만들어 발급한다. (JWT 토큰 안에는 id, 토큰만료시간 등이 담겨있다)
  5. JWT 토큰을 암호하한다음 브라우저에 보낸다.
  6. 브라우저에서 jwt토큰값을 성공적으로 받았으면, 브라우저 쿠키(Cookie)에 이 토큰값을 저장한다.
  7. 이 쿠키값이 유지되는 동안, 회원이 브라우저에 API를 요청할 때마다 회원임을 확인받는다. (쿠키 값은 항상 브라우저에서 서버에 무언가 요청을 보낼 떄 같이 보내진다. -> 일종의 프리패스권)
  8. 로그아웃 시 해당 토큰을 삭제한다.

 

 


 

 

1. 회원가입


회원가입은 비교적 간단하게 작성

register-html

 

  1. 회원정보를 받아온다.(ID, PW, Nickname)
  2. 회원정보 비밀번호를 해시함수로 암호화해서 데이터베이스에 저장한다. - hashlib
# [회원가입 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'})

 

Python hashlib - 문자열 해싱 라이브러리

  • hashlib은 파이썬 라이브러리가 제공해주는 hash 함수이다.
  • hashlib을 사용해주기위해서는 먼저 import hashlib으로 라이브러리를 포함시키고 
  • 어떤 방식으로 문자열을 해싱(암호화)시킬지 설정한다. - md5, sha256등 (문자열을 암호화시키는 알고리즘의 종류)
import hashlib

m = hashlib.sha256()
m.update('Life is too short'.encode('utf-8'))

m.update(', you need python.'.encode('utf-8'))

 

 

digest, hexdigest - 해싱코드 문자열 리턴

문자열을 해싱시켰다면 digest(바이트 문자열) 또는 hexdigest(바이트 -> 16진수 문자열) 함수를 사용해 해싱된 문자열을 얻어올 수 있다.

>>> m.digest()
b"\x9d\x05'^\xcaK\xf8\xf2\x02w!\xce?\xd7\xe6\xf0\xaa\x06\xdc\xc3\x81 N\xd8G[\xe3B\\,S\x84"
>>> m.hexdigest()
'9d05275eca4bf8f2027721ce3fd7e6f0aa06dcc381204ed8475be3425c2c5384'

 

암호화된 값이 저장된 디비


 

 

2. 로그인


로그인은 JWT 토큰과 Cookie의 개념을 같이 설명하겠다.

 

로그인 페이지

 

1. 사용자에게 id와 pw를 받아오고, 디비에 저장된 암호화된 값과 비교하기위해 똑같이 비밀번호를 해싱한다.

2. 디비에 사용자의 정보가 있으면  JWT 토큰을 발급한다.

3. jwt 토큰은 Json 형식이며, Playload 키 안에 사용자 정보가 담겨잇다.

4. JWT 토큰을 암호화에서 브라우저에게 보낸다.

# [로그인 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)    #언제까지 유효한지
        }
        #jwt를 암호화
        # token = jwt.encode(payload, SECRET_KEY, algorithm='HS256').decode('utf-8')
        token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')

        # token을 줍니다.
        return jsonify({'result': 'success', 'token': token})
    # 찾지 못하면
    else:
        return jsonify({'result': 'fail', 'msg': '아이디/비밀번호가 일치하지 않습니다.'})
  • 위 코드를 실행해 보다가 str decode 에러가 나오면 아래의 글 링크를 참고
  • jwt 토큰 utf-8 에러 해결 : https://thalals.tistory.com/165

 

 

 


 

 

JWT - 토큰 기반 인증 방식

Json Web Token


서버인증 방식에는 크게 세션/쿠기 방식과 토큰 인증 방식이 있습니다.

그 중에서 JWT는 토큰 인증방식에 포함합니다.

 

 

JWT Token

  • JWT는 세션/쿠키와 함께 모바일과 웹의 인증을 책임지는 대표주자입니다.
  • JWT는 Json Web Token의 약자로 인증에 필요한 정보들을 암호화시킨 토큰을 뜻합니다.
  •  사용자는 Access Token(JWT 토큰)을 HTTP 헤더에 실어 서버로 보내게 됩니다.

 

 

 

JWT 토큰의 구조는 3가지로 이루어집니다. - Header,Payload, Verify Signature

  1. Header : 위 3가지 정보를 암호화할 방식(alg), 타입(type) 등이 들어갑니다.
  2. Payload : 서버에서 보낼 데이터가 들어갑니다. 일반적으로 유저의 고유 ID값, 유효기간이 들어갑니다.
  3. Verify Signature :  Base64 방식으로 인코딩한 Header,payload 그리고 SECRET KEY를 더한 후 서명됩니다.

 

 

여기서 우리는 playload를 설정해준 후 Token을 생성해서 암호화를 시켜주는 겁니다.

 

 


 

 

브라우저 쿠키 세션 - Cookie [로그인 정보를 받은 후]


 서버에서 로그인을 하고 JWT토큰을 발급받으면, 브라우저에서는 그 값을 쿠키에 저장합니다

 

로그인 완료

 

 

['쿠키'라는 개념에 대해 알아봅시다]

쿠키 : 브라우저에 저장되어있는 정보 

로그인을 구현하면, 반드시 쿠키라는 개념을 사용합니다.
페이지에 관계없이 브라우저에 임시로 저장되는 정보입니다. 키:밸류 형태(딕셔너리 형태)로 저장됩니다.
쿠키가 있기 때문에, 한번 로그인하면 네이버에서 다시 로그인할 필요가 없는 것입니다.
브라우저를 닫으면 자동 삭제되게 하거나, 일정 시간이 지나면 삭제되게 할 수 있습니다.

 

브라우저에서 서버에 API를 요청할 때마다 쿠키를 함께 전송합니다.

따라서 유저정보가 필요한 API를 실행할 때, 쿠키에 정보가 있다면, 다시 인증하지 않아도 쿠키의 정보를 가지고  API요청을 받아들입니다.

 

쿠키는 일종의 자유이용권!!

 

// ['쿠키'라는 개념에 대해 알아봅시다] -> 브라우저에 저장되어있는 정보
// 로그인을 구현하면, 반드시 쿠키라는 개념을 사용합니다.
// 페이지에 관계없이 브라우저에 임시로 저장되는 정보입니다. 키:밸류 형태(딕셔너리 형태)로 저장됩니다.
// 쿠키가 있기 때문에, 한번 로그인하면 네이버에서 다시 로그인할 필요가 없는 것입니다.
// 브라우저를 닫으면 자동 삭제되게 하거나, 일정 시간이 지나면 삭제되게 할 수 있습니다.

function login() {
	$.ajax({
		type: "POST",
		url: "/api/login",
		data: {id_give: $('#userid').val(), pw_give: $('#userpw').val()},

		success: function (response) {
			if (response['result'] == 'success') {
				// 로그인이 정상적으로 되면, 토큰을 받아옵니다.
				// 이 토큰을 mytoken이라는 키 값으로 쿠키에 저장합니다.
				
                $.cookie('mytoken', response['token']);

				alert('로그인 완료!')
				window.location.href = '/'
			} else {
				// 로그인이 안되면 에러메시지를 띄웁니다.
				alert(response['msg'])
			}
		}
	})
}

 

 

Jquery를 이용해 쿠키 만들기

$.cookie('mytoken', response['token']);

이 코드는 제이쿼리를 이용해 jwt토큰 value를 가지는 Mytoken이라는 쿠키를생성하는 코드입니다..


1. jquery 쿠키 생성(가이드)

// 세션 쿠키 생성 - 브라우저를 닫으면 없으집니다.
$.cookie('name', 'value');

// 7일 뒤에 만료되는 쿠키 생성
$.cookie('name', 'value', { expires: 7 });

// 전체 사이트에 대해 7일 뒤에 만료되는 쿠키 생성
$.cookie('name', 'value', { expires: 7, path: '/' });

 

2. jquery 쿠키 읽어오기(제어)

// 키로 사용한 이름으로 값을 읽습니다.
// 키로 저장된 값이 없으면 undefined 가 반환됩니다.
$.cookie('name'); // => "value"
$.cookie('nothing'); // => undefined

// 모든 쿠키 읽기
// 모든 쿠키를 { "name": "value" } 형태의 객체로 반환합니다.
$.cookie(); // => { "name": "value" }

 

3, jquery 쿠키 삭제 

 

// 성공적으로 삭제되면 true 를 반환하고, 삭제 못했을때는 false를 반환합니다.
$.removeCookie('name'); // => true
$.removeCookie('nothing'); // => false

// 쿠키 삭제시 생성할때 와 같은 path와 domain 을 사용해야 합니다.
// 아래와 같이 생성했다면,
$.cookie('name', 'value', { path: '/' });

// 이렇게 삭제할 수 없습니다.
$.removeCookie('name'); // => false

// 이렇게 삭제해야 합니다.
$.removeCookie('name', { path: '/' }); // => true

 

 

 


 

 

토큰 유효기간 


 

토큰 유효기간이 지난 후 새로고침을 하면 아래와 같은 경고창이 뜨는데, 이게 왜 이러나면,

메인코드를 봐야한다!

 

거의 다 왔다..

코드를 하나씩 살펴보자

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

@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="로그인 정보가 존재하지 않습니다."))

 

request.cookies.get

클라인트 쪽에서 쿠키값을 보내지 않은 Main 함수임에도 불구하고 이 코드가 작동하는 이유는 

쿠키는 사용자가 별도로 요청하지 않아도 브라우저(Client)에서 서버에 요청(Request) 시에 Request Header에 쿠키 값을 넣어 요청하기 때문이다. (=자동이다.)


jwt.ExpiredSignatureError

jwt 토큰의 시그니쳐가 만료되었다는 의미이다. 즉, 유효기간이 다 되었다는 에러문구이다.

Try - catch- except를 이용해 이 문구 에러가 나오면, 시간 만료 메세지를 클라이언트에게 보낸다.

 

jwt.exceptions.DecodeError

jwt 토큰이 유효하지 않다는 에러문구이다.

 

 

 

끝!!!

 

아주 나이스해! 나 자신!

 

 

 

 

 

 

[참고]

hashlib : https://wikidocs.net/122201

서버 인증 방식 : https://tansfil.tistory.com/58?category=255594

제이쿼리 쿠키 사용법 : https://dullyshin.github.io/2019/09/06/WEB-cookieSession/

서버, 클라이언트에서의 쿠키 프로세스 : https://jeong-pro.tistory.com/80

반응형