본문 바로가기
Flask

Flask 회원가입 API : email체크, 비밀번호 길이체크 암호화, jwt(user_id), 로그인, 로그아웃, 로그인 유저의 리퀘스트 처리, Headers(Authorization & Bearer) , 토큰의 특성

by leopard4 2023. 1. 5.

 

jwt를 사용하는 이유는 보안때문

클라이언트는 사용자의 고유한 id를 보관하고 있어야 

그유저가 어떤 행동을 했을때 그 id를 서버로 보내서 처리를 해야하는데.

그냥 숫자로 보관한다면 해킹등의 문제가 발생한다

때문에 회원가입시에 유저정보를 db에 저장할때

유저의 고유한  id는 토큰으로 저장하고 

클라이언트에서  토큰형태로된 고유한id값을 리퀘스트 하면

 db에 저장되어있는 id 값을 가져와서 체크한다.

 

비밀번호도 db에 저장할때 미리 작성해 두었던

from passlib.hash import pbkdf2_sha256 을 이용한 함수를

써서 암호화하여 저장.

 

email은 진위성 체크 라이브러리를 사용한다.

from email_validator import validate_email, EmailNotValidError

 

비밀번호 길이는 if문을 사용한다. 

 

큰맥락에서 살펴보았으니 자세한 사항은 아래 코드를 참고.

로그인
로그아웃
로그인 유저의 리퀘스트 처리 
Headers(Authorization & Bearer)
토큰의 특성
등은 나중에 설명할것임.
 

어플 폴더, 파일 구성

편리성을 위해 파일을 나눠둔다. 

완성된것은 아니므로 참고만 바람.

 

app == 메인서버를 실행하는 파일

 
from flask import Flask # Flask 객체를 만들기 위한 라이브러리
from flask_restful import Api # API를 만들기 위한 라이브러리
from config import Config # 환경변수를 가져온다.
from flask_jwt_extended import JWTManager

from resources.recipe import MyRecipeListResource, RecipeListResource, RecipePublish, RecipeResource
from resources.user import UserLoginResource, UserLogoutResource, UserRegisterResource
from resources.user import jwt_blocklist

app = Flask(__name__) # Flask 객체 생성
# 환경변수 셋팅
app.config.from_object(Config) 

# JWT 매니저 초기화
jwt = JWTManager(app)

# 로그아웃된 토큰으로 요청하는 경우 처리하는 코드작성.
@jwt.token_in_blocklist_loader # 토큰이 만료되었는지 확인하는 함수를 등록한다.
def check_if_token_is_revoked(jwt_header, jwt_payload): # 토큰이 만료되었는지 확인하는 함수
    jti = jwt_payload['jti'] # jti는 JWT 토큰의 고유 식별자
    return jti in jwt_blocklist 


api = Api(app) # API 객체 생성

# 경로와 리소스(API코드)를 연결한다.
api.add_resource(RecipeListResource, '/recipes') 
api.add_resource(RecipeResource, '/recipes/<int:recipe_id>') # <int:recipe_id> 변경되는 부분을 변수로 받는다.
api.add_resource(RecipePublish, '/recipes/<int:recipe_id>/publish') 
api.add_resource(UserRegisterResource, '/user/register')
api.add_resource(UserLoginResource, '/user/login')
api.add_resource(MyRecipeListResource, '/recipes/me')
api.add_resource(UserLogoutResource, '/user/logout')


if __name__ == '__main__': # 현재 파일이 실행되는지 확인
    app.run() # 서버 실행

user 테이블 == 회원가입 로그인 정보등의 api

from datetime import datetime
from flask import request 
from flask_restful import Resource 
from mysql.connector import Error
from flask_jwt_extended import create_access_token, get_jwt, jwt_required

from mysql_connection import get_connection
from email_validator import validate_email, EmailNotValidError # 이메일 형식을 체크하기 위한 라이브러리

from utils import check_password, hash_password




class UserRegisterResource(Resource): # Resource 클래스를 상속받는다. # 회원가입 API
    
    def post(self): # POST 요청을 받는다.

        # {"username" : "홍길동",   # 실무에선 이렇게 보면서 한다.
        # "email":"abc@naver.com",
        # "password" : "1234" }

        # 1. 클라이언트가 보낸 데이터를 받는다.
        data = request.get_json() 

        # 2. 이메일 주소형식이 올바른지 확인한다.
        try: 
            validate_email(data['email']) # 이메일 형식이 아니면 에러가 발생한다.
        except EmailNotValidError as e: # 에러가 발생하면
            print(str(e)) # 디버깅용 출력
            return {'message': str(e)}, 400 # 에러 메시지와 400 에러를 리턴한다. # 400 에러는 클라이언트가 잘못된 요청을 했을 때 리턴하는 에러코드

        # 3. 비밀번호의 길이가 유효한지 체크한다.
        # 만약, 비번이 4자리 이상, 12자리 이하다라면,
        
        if len(data['password']) < 4 or len(data['password']) > 12: # 비밀번호가 4자리 미만이거나 12자리 초과이면
            return {'message': '비밀번호는 4자리 이상, 12자리 이하로 입력해주세요.'}, 400 # 에러 메시지와 400 에러를 리턴한다.

        # 4. 비밀번호를 암호화 한다.
        hashed_password = hash_password( data['password'] ) 

        print(hashed_password)

        # 5. DB에 회원정보를 저장한다.
        try:
            connection = get_connection() # DB 커넥션을 가져온다.

            query = """
                INSERT INTO user (username, email, password) 
                VALUES (%s, %s, %s);
            """
            record = (data['username'], data['email'], hashed_password)
            cursor = connection.cursor() # 커서를 가져온다.
            cursor.execute(query, record) # 쿼리를 실행한다.
            connection.commit() # 커밋한다.
            
            ### DB에 회원가입하여, insert 된 후에
            ### user 테이블의 id 값을 가져오는 코드!
            user_id = cursor.lastrowid # 마지막에 추가된 row의 id를 가져온다.

            cursor.close() # 커서를 닫는다.
            connection.close() # 커넥션을 닫는다.

        except Error as e:
            print(e)
            cursor.close() 
            connection.close()
            return {'message': str(e) }, 500 # 500은 서버에러를 리턴하는 에러코드
       
        # user_id를 바로 클라이언트에게 보내면 안되고,
        # jwt로 암호화 해서, 인증토큰을 보낸다.

        acces_token = create_access_token(identity=user_id ) # identity는 토큰에 담길 내용이다. # 담을게 여러개면 딕셔너리 형태로담는다.  
        # expires_delta는 토큰의 유효기간이다. # timedelta로 지정한다. # timedelta는 datetime에서 가져온다.

        return {'access_token': acces_token}, 200 # 200은 성공했다는 의미의 코드

class UserLoginResource(Resource):
   
    def post(self) :
        # {"email":"zzez@naver.com",
        # "password" : "1234" } # 클라이언트가 보낸 데이터
        
        # 1. 클라이언트가 보낸 데이터를 받는다.
        data = request.get_json()

        # 2. DB 로부터 해당 유저의 데이터를 가져온다.
        try :
            connection = get_connection()
            query = """
                select * 
                from user
                where email = %s;
            """
            record = (data['email'],) 
            
            cursor = connection.cursor(dictionary=True) # dictionary=True를 하면, 컬럼명을 key로 가지는 딕셔너리를 리턴한다.
            cursor.execute(query, record)
            
            result_list = cursor.fetchall() # 튜플의 리스트를 리턴한다. 
            
            if len(result_list) == 0 :
                return {'message' : '존재하지 않는 이메일입니다.'}, 400
                
            print("리설트리스트", result_list[0]) # 디버깅용
            i = 0
            for row in result_list :
                result_list[i]['created_at'] = row['created_at'].isoformat() 
                result_list[i]['updated_at'] = row['updated_at'].isoformat()
                i += 1 

            cursor.close()
            connection.close()

        except Error as e :
            print(e)
            cursor.close()
            connection.close()
            return {'message' : str(e)}, 500 # 500은 서버에러를 리턴하는 에러코드

        print(result_list)

        # 3. 비밀번호를 비교한다.

        print("data['password']",data['password'])
        print("result_list[0]['password']",result_list[0]['password'])
        
        check = check_password(data['password'], result_list[0]['password']) 

        if check == False :
            return {'message' : '비밀번호가 틀렸습니다.'}, 400

        # 4. jwt 토큰을 만들어서 클라이언트에게 보낸다.
        acces_token = create_access_token(identity=result_list[0]['id'] ) # identity는 토큰에 담길 내용이다. # 담을게 여러개면 딕셔너리 형태로담는다.

        return {'access_token': acces_token}, 200 # 200은 성공했다는 의미의 코드

### 로그 아웃 ####

# 로그아웃된 토큰을 저장할 set 만든다.

jwt_blocklist = set()

class UserLogoutResource(Resource) :

    @jwt_required()
    def post(self) :
        
        jti = get_jwt()['jti'] # jti는 jwt 토큰의 고유한 식별자이다.
        print(jti) # 디버깅용
        jwt_blocklist.add(jti) # 로그아웃된 토큰을 저장한다.

        return {'message' : '로그아웃 성공'}, 200

config == db접근에 필요한 시크릿 정보와 jwt 토큰정보

class Config :
    HOST = "여기는비밀로할것1"
    DATABASE = '여기는비밀로할것2'
    DB_USER = '여기는비밀로할것3'
    DB_PASSWORD = '여기는비밀로할것4'
    SALT = '여기는비밀로할것5'

    # JWT 관련 변수 셋팅
    JWT_SECRET_KEY = '여기는비밀로할것6' # JWT 토큰을 만들때 사용하는 비밀키 # 관리할 문자열
    JWT_ACCESS_TOKEN_EXPIRES = False # False 가아니고 만약, 60 * 60 * 24 # JWT 토큰의 유효기간 # 1일
    PROPAGATE_EXCEPTIONS = True # JWT 관련 에러를 발생시키는지 여부

utils == 패스워드 단방향 암호화와 비교

# 비밀번호 암호화 관련 파일임
from passlib.hash import pbkdf2_sha256

from config import Config 

# 원문 비밀번호를, 단방향 암호화 하는 함수
def hash_password(original_password): 
     # salt는 랜덤한 문자열 # 해킹이 어렵도록 ## 반드시 config에 분리저장
    password = original_password + Config.SALT # 원문 비밀번호에 salt를 붙여서 암호화
    password = pbkdf2_sha256.hash(password) # 암호화된 비밀번호를 리턴
    return password

# 유저가 로그인 할때, 입력한 비밀번호와, DB에 저장된 비밀번호를 비교하는 함수
def check_password(original_password, hashed_password): # 원문 비밀번호, 암호화된 비밀번호
    # 위에서 사용한 salt 와 똑같은 문자열
    password = original_password + Config.SALT
    check = pbkdf2_sha256.verify(password, hashed_password)
    return check # True or False
    # verify가 뭐야? -> 원문 비밀번호와, 암호화된 비밀번호를 비교해주는 함수

mysql_connection == DB 연결을 위한 사전작업

import mysql.connector # DB에 연결하기 위한 라이브러리

from config import Config  

def get_connection(): 

    connection = mysql.connector.connect(
        host = Config.HOST ,
        database = Config.DATABASE,
        user = Config.DB_USER,
        password = Config.DB_PASSWORD
    )
    return connection

recipe == 레시피 파일

from flask import request # 클라이언트가 보낸 데이터를 받기 위한 라이브러리
from flask_restful import Resource # API를 만들기 위한 라이브러리
from mysql.connector import Error # DB에 연결할 때, 에러가 발생할 수 있으므로, 에러처리를 위한 라이브러리
from flask_jwt_extended import jwt_required, get_jwt_identity # JWT를 사용하기 위한 라이브러리

from mysql_connection import get_connection # DB에 연결하기 위한 함수

# API 를 만들기 위한 클래스 작성.
# class란 ?? 변수와 함수로 구성된 묶음
# 클래스는 상속이 가능하다!
# 상속이란 ?? 기존 클래스를 확장하여 새로운 클래스를 만드는 것
# API를 만들기 위해서는, flask_restful 라이브러리의 
# Resource 클래스를 상속해서 만들어야 한다.

class RecipeListResource(Resource):
    
    # APi를 처리하는 함수 개발
    # HTTP Method 를 보고 ! 똑같이 만들어준다.

    # @jwt_required() == jwt 토큰이 필수라는 뜻! : 토큰이 없으면, 이 api는 실행할수 없다.
    @jwt_required() # 헤더에 Authorization 토큰이 있어야만, API를 사용할 수 있다. 
    def post(self):
        # 1. 클라이언트가 보내준 데이터가 있으면
        #    그 데이터를 받아준다.
        data = request.get_json()
        
        # 1.1 헤더에 jwt 토큰이 있으면, 토큰 정보를 받아준다.
        user_id = get_jwt_identity() # 유저 id 토큰 정보를 받아준다. # 복호화하는 함수

        # 2. 받은 데이터(레시피정보)를 DB에 저장한다.         

        try :
            ### 1 . DB 연결
            # mysql_connection.py 에서 만든 함수를 호출
            connection = get_connection()

            ### 2. 쿼리문 만들기
            query = '''
                    insert into recipe
                    (name, description, num_of_servings, cook_time, directions, user_id)
                    values
                    ( %s, %s, %s, %s, %s, %s );'''
            ### 3. 쿼리에 매칭되는 변수 처리 해준다. 
            # # 딕셔너리의 키값으로 접근
            record = (data['name'], data['description'], data['num_of_servings'], data['cook_time'], data['directions'], user_id) 
            
            ### 4. 커서를 가져온다.
            cursor = connection.cursor() # 커서는 DB에 쿼리문을 실행시키는 역할을 한다.

            ### 5. 쿼리문을, 커서로 실행한다.
            cursor.execute(query, record) # 쿼리문과, 쿼리에 매칭되는 변수를 넣어준다.

            ### 6. 커밋 해줘야, DB에 실제 반영한다.
            connection.commit() 

            ### 7. 자원 해제
            cursor.close()
            connection.close()

        except Error as e:          
            print(e)
            cursor.close()
            connection.close()

            return {"result": "fail", "error" : str(e)}, 500


        # API 를 끝낼때는
        # 클라이언트에 보내줄 정보(json)와 http상태코드를
        # 리턴한다(보내준다)
        return {"result": "success" }, 200

    # get 메소드를 처리하는 함수를 만든다.
    
    def get(self):
        # 1. 클라이언트로부터 데이터를 받아온다.
        # 없다.

        # 2. db에 저장된 데이터를 가져온다.
        try :
            ### 3 . DB 연결
            # mysql_connection.py 에서 만든 함수를 호출
            connection = get_connection()

            ### 4. 쿼리문 만들기
            query = '''
                    SELECT * FROM recipe;
                    '''

            ## 중요 : select 문은!!!!!!
            ## 커서 가져올때, dictionary = True 로 해준다.
            ### 5. 커서를 가져온다.
            cursor = connection.cursor(dictionary=True)

            ### 6. 쿼리문을, 커서로 실행한다.
            cursor.execute(query)

            ### 7. 결과를 가져온다.
            result_list = cursor.fetchall()
            
            print(result_list)

            # 중요! 디비에서 가져온 timestamp 는
            # 파이썬에서 datetime 으로 자동 변환된다.
            # 그래서, json 으로 변환해서 클라이언트한테 보내야하는데 에러가난다.
            # 따라서, 시간을 문자열로 변환해서 보내준다.
           
            
            i = 0
            for row in result_list :
                result_list[i]['created_at'] = row['created_at'].isoformat()
                result_list[i]['updated_at'] = row['updated_at'].isoformat()
                i += 1

            ### 8. 자원 해제
            cursor.close()
            connection.close()

        except Error as e:          
            print(e)
            cursor.close()
            connection.close()

            return {"result": "fail", "error" : str(e)}, 500

 # 2. DB에서 가져온 레시피 정보를 클라이언트에 보낸다.
        return {"result": "success", 
            'items' : result_list, 
            'count' : len(result_list) }, 200

class RecipeResource(Resource) : # 상속
    
    def get(self, recipe_id) :
        # 1. 클라이언트로부터 정보를 가져온다.
        # print(recipe_id)

        # 2. 디비로부터 해당 레시피아이디에 맞는 레시피데이터를 
        #    가져온다.
        
        try :
            connection = get_connection()

            query = '''
                    SELECT * FROM recipe
                    where id = %s ; '''   # %s 는 변수처리 할것임
            
            record = (recipe_id, ) # 튜플로 만들어줘야함

            cursor = connection.cursor(dictionary=True) # 셀렉트 문장은 딕셔너리로 가져와야함

            cursor.execute(query, record) # 쿼리문 실행

            result_list = cursor.fetchall() # 리스트로 가져옴

            i = 0 # 인덱스
            for row in result_list : # 리스트에 있는 딕셔너리를 하나씩 가져옴
                result_list[i]['created_at'] = row['created_at'].isoformat() # 시간을 문자열로 변환
                result_list[i]['updated_at'] = row['updated_at'].isoformat()
                i += 1

            cursor.close() # 커서 닫기
            connection.close() # 커넥션 닫기

        except Error as e : # 에러가 나면
            print(e)
            cursor.close() # 에러가 나면 커서와 커넥션을 닫아줌
            connection.close() # 에러가 나면 커서와 커넥션을 닫아줌

            return {"result": "fail", "error" : str(e)}, 500 # 에러가 나면 에러메세지와 500번 에러코드를 보냄

        
        if len(result_list) == 0 :
            return {"result": "fail", "message" : "해당 레시피가 없습니다."}, 400
        
        return {"result": "success",
                "item" : result_list[0]}, 200 # 에러가 안나면 성공과 데이터를 보냄

    @jwt_required() # 토큰이 필요함
    def put(self, recipe_id) : # 수정
       
        data = request.get_json() 

        user_id = get_jwt_identity() # 토큰에 있는 유저아이디를 가져옴
        # 토큰에서 가져온 user_id를 쿼리문과 record에 추가함

        # 2. 클라이언트로부터 받은 데이터를 디비에 업데이트한다.
        #    (레시피 정보를 업데이트한다.)
        try :
            connection = get_connection()

            query = '''
                update recipe
                set
                name = %s,
                description = %s,
                num_of_servings = %s,
                cook_time = %s,
                directions = %s
                where id = %s and user_id = %s; '''   # %s 는 변수처리 할것임 # 
            
            
            record = (data['name'], data['description'], data['num_of_servings'], data['cook_time'], data['directions'], recipe_id, user_id) # 튜플로 만들어줘야함
            
            cursor = connection.cursor() 

            cursor.execute(query, record) 

            connection.commit() 

            cursor.close()
            connection.close()

        except Error as e :
            print(e)
            cursor.close()
            connection.close()

            return {"result": "fail", "error" : str(e)}, 500

        return {"result": "success"}, 200
        
    @jwt_required()
    def delete(self, recipe_id) : # 삭제 

        user_id = get_jwt_identity() 

        try :
            connection = get_connection()

            query = '''
                delete from recipe
                where id = %s and user_id = %s; '''  
            
            record = (recipe_id, user_id) 
            
            cursor = connection.cursor() 

            cursor.execute(query, record) 

            connection.commit() 

            cursor.close()
            connection.close()

        except Error as e :
            print(e)
            cursor.close()
            connection.close()

            return {"result": "fail", "error" : str(e)}, 500

        return {"result": "success"}, 200

    
   
class RecipePublish(Resource) : # 공개, 비공개    # Resource 는 뭐냐면 클래스를 상속받는거임
    
    @jwt_required()
    def put(self, recipe_id) : # 공개
        
        user_id = get_jwt_identity()

        try :
            connection = get_connection()

            query = '''
                update recipe
                set
                is_publish = 1
                where id = %s and user_id = %s; '''   
            
            record = (recipe_id, user_id) 
            
            cursor = connection.cursor() 

            cursor.execute(query, record) 

            connection.commit() 

            cursor.close()
            connection.close()
        
        except Error as e :
            print(e)
            cursor.close()
            connection.close()

            return {"result": "fail", "error" : str(e)}, 500 

        return {"result": "success"}, 200
        
    @jwt_required()
    def delete(self, recipe_id) : # 비공개

        user_id = get_jwt_identity()

        try :
            connection = get_connection()

            query = '''
                update recipe
                set
                is_publish = 0
                where id = %s and user_id = %s; '''   
            
            record = (recipe_id,user_id ) 
            
            cursor = connection.cursor() 

            cursor.execute(query, record) 

            connection.commit() 

            cursor.close()
            connection.close()
        
        except Error as e :
            print(e)
            cursor.close()
            connection.close()

            return {"result": "fail", "error" : str(e)}, 500
        
        return {"result": "success"}, 200
        
class MyRecipeListResource(Resource): # 내가 만든 레시피 리스트

    @jwt_required()
    def get(self): # 조회

        user_id = get_jwt_identity()

        try :
            connection = get_connection()

            query = '''
                select *
                from recipe
                where user_id = %s; '''   
            
            record = (user_id, ) 
            
            cursor = connection.cursor(dictionary=True)

            cursor.execute(query, record) 

            result_list = cursor.fetchall() 

            i = 0
            for row in result_list :
                result_list[i]['created_at'] = row['created_at'].isoformat()
                result_list[i]['updated_at'] = row['updated_at'].isoformat()
                i += 1

            cursor.close()
            connection.close()

        except Error as e :
            print(e)
            cursor.close()
            connection.close()

            return {"error" : str(e)}, 500

        return {"result": "success",
                "items" : result_list, 
                'count' : len(result_list)}, 200

 

'Flask' 카테고리의 다른 글

Flask 영화 추천 API  (0) 2023.01.09
Flask 영화명 검색 API  (0) 2023.01.09
Flask postman mysql  (0) 2023.01.06
Flask 백엔드 개발 큰그림 요약  (0) 2023.01.05
Flask 1단계 Postman, Mysql, Github 기본세팅  (0) 2023.01.04