Backend/Flask

Flask 기초 I - RDB와 Flask 상호작용, 간단한 게시판, Flask JWT

yxemsy 2022. 2. 11. 16:09

 

1) RDB와 Flask 상호작용

RDB은 간단하게 설명하자면 관계형 데이터베이스로 NoSQL과는 다르게 정형화된 데이터베이스이다.
키와 값들의 간단한 관계를 테이블화 시킨 데이터베이스이다.
  • 파이썬은 오픈소스와 상용 데이터베이스에 대한 대부분의 데이터베이스 엔진을 위한 패키지를 가지고 있다.
  • RDB와 Flask의 상호작용을 통해 Flask에서 입력받은 내용을 DB에 저장할 수 있다.

 

출처: 엘리스 AI트랙

상호작용을 그림으로 표현한 것이다.

클라이언트가 서버에 데이터를 요청하면 flask는 데이터베이스에 그 데이터가 있는지 확인하고 처리한다.

 


2) 간단한 게시판 만들기

DB에 저장되는 데이터를 활용하여 사용자를 검색해보기
DB에 저장되는 데이터를 활용해서 사용자를 추가해보기 (중복 사용자 방지)
게시판 내용을 생성, 조회, 수정, 삭제해보기

 

  • 게시판의 기능을 DB에 적용하기 위해서는 리소스를 다뤄야한다.
  • RDB를 사용해서 구현하니까 Create, Read, Update, Delete를 이용하면 된다.

** 아래에 작성된 코드는 HTML 파일이 존재하고 있다는 가정하에 작성된 것 **

 

(1) 사용자 정보 검색

from flask import Flask, render_template, request, url_for, redirect
import sqlite3  # DB는 sqlite3을 이용

app = Flask(__name__)  # 플라스크 선언

@app.route('/search', methods = ['GET', 'POST'])
def search():
    if request.method == 'POST':
        result = request.form['search'] # html파일에서 name이 search인 태그의 form 값을 받아옴
        con = sqlite3.connect("database.db") # DB를 연결
        cur = con.cursor() # cursor 객체를 생성
        cur.execute(f"SELECT * FROM Board WHERE name='{result}' or context='{result}'")
        # Board 테이블에서 요청받은 result가 있는지 찾는 쿼리
        # f를 넣으면 중괄호 안에 변수 사용가능
  
        rows = cur.fetchall()  # 쿼리 실행 결과를 rows 변수에 저장
        con.close() # DB연결 해제
         
        print("DB:")
        for i in range(len(rows)):
            print(rows[i][0] + ':' + rows[i][1])
        return render_template('search.html', rows = rows)
    else:
        return render_template('search.html')

if __name__ == '__main__':
    app.run(debug=True)

 

result 변수에는 html 파일에서 받아온 form의 value값을 담는다.

그리고 이 result를 SQL문 안에 변수로 찾고자하는 값으로 넣어줌!!!!

db에 연결을 한 변수를 cursor() 객체로 생성해주어야 excute 함수를 사용할 수 있다.

excute()는 SQL 쿼리문을 작성하여 원하는 데이터를 찾을 수 있다.

DB에서 원하는 데이터를 불러온 뒤에는 close()를 사용하여 연결 해제를 해주어야한다.

 

 


(2) DB 사용자 추가 : name과 context 라는 값을 저장해보기

from flask import Flask, render_template, request, url_for, redirect
import sqlite3

@app.route('/add', methods = ['GET', 'POST'])
def add():
    if request.method == 'POST':
        try:
            name = request.form['name'] # html의 form에서 name이 'name'인 입력의 값 받아옴 
            context = request.form['context'] # html의 form에서 name이 'context'인 입력의 값 받아옴 
            with sqlite3.connect("database.db") as con: # with를 사용하여 DB 연결이 가능
                cur = con.cursor() # 가져온 값을 cursor 객체에 저장
                
                cur.excute(f"INSERT INTO Board (name, context) VALUES ('{name}','{context}')")
                
                con.commit() # commit하여 DB에 반영한다.
        except:
            con.rollback() # 이전 실행상태로 되돌린다.
        finally : 
            return redirect(url_for('board')) # board url로 redirect
    else:
        return render_template('add.html')

if __name__ == '__main__':
    app.run(debug=True)

with sqlite3.connect로 db에 연결해주면 close()과정이 따로 없어도 된다.

위 코드같은 경우는 파이썬의 try, except, finally 문을 사용하여 구현한 것이다.

입력받은 값을 SQL문을 이용하여 DB에 입력해주고 commit해 저장한다.

근데 만약에 그 과정 도중 문제가 발생하면 except: 문에서 rollback 처리 해준다.

성공하면 'board' url로 redirect! 

 


(3) 중복 사용자 방지 - 추가하고자 하는 값이 이미 DB에 있는 값일 때

app.route('/add', methods = ['GET', 'POST'])
def add():
    if request.method == 'POST':
        name = request.form['name']
        context = request.form['context']
        with sqlite3.connect("database.db") as con:
            cur = con.cursor()
             # name이 DB에 있는지 확인 후 사용자를 DB에 추가 ▽
            cur.execute(f"SELECT count('name') FROM Board WHERE name='{name}'") # 입력된 name이 Board 테이블에 이미 있는지 조회하는 쿼리
            if cur.fetchall()[0][0] == 0: #만약 데이터가 0개라면 중복 x라는 뜻
                cur.execute(f"INSERT INTO Board (name, context) VALUES ('{name}', '{context}')")
                con.commit()
            else: # 중복 사용자인 경우 안내 메시지를 넘긴 후 add.html을 렌더링
                return render_template('add.html', msg="중복 사용자입니다. 이름을 바꿔주세요.")
                
        return redirect(url_for('board'))
    else:
        return render_template('add.html')

 

html이 존재하고있다고 가정하고 코드를 작성했다고 했지만 마지막 (3) 예시의 html 코드를 간단히 리뷰해보면

<form action = "/add" method = "POST">
		이름<br>
		<input type = "text" name = "name" /><br>
		내용<br>
		<input type = "text" name = "context" style="text-align:center; width:400px; height:100px;"/>
</form>

action 은 /add url과 연결해주는 프로퍼티이다. 값을 추가하고자하는 거니까 당연히 POST 메서드를 사용하고

name 프로퍼티에 적힌 값을 통해 flask로 값을 받아올 수 있다. request.form['name'] 이런식~

 


(4) 게시판 내용 수정 및 삭제

########## 수정 #############

@app.route('/update/<uid>', methods=['GET','POST']) # <uid>는 변경할 값을 받아오는 변수이자 주소
def update(uid):
    if request.method =='POST':
        name = request.form['name'] # uid의 name을 바꾸고싶은 name으로 입력
        context = request.form['context'] # 마찬가지 
        with sqlite3.connect("database.db") as con:
            cur = con.cursor()
            # UPDATE 문을 이용해 name과 context를 수정하는 쿼리를 실행
            cur.execute(f"UPDATE Board Set name='{name}',context='{context}' WHERE name='{uid}'") 
            con.commit()
        return redirect(url_for('board'))
    else:
        con = sqlite3.connect("database.db")
        cur = con.cursor()
        cur.execute(f"SELECT * FROM Board WHERE name ='{uid}'")
        row = cur.fetchall()
        return render_template('update.html',row=row)

########## 삭제 #############

@app.route('/delete/<uid>')
def delete(uid):
    with sqlite3.connect("database.db") as con:
            cur = con.cursor()
            #  DELETE 문을 이용해 데이터를 삭제하는 쿼리를 실행
            cur.execute(f"DELETE FROM Board WHERE name='{uid}'") 
            con.commit()
    return redirect(url_for('board')) # "/"로 돌아가겟다.

with 구문을 사용해 close()를 해줄 필요가 없이 구현한 것이다.

update 쿼리는 다른 쿼리사용법보다 복잡한 것 같아 살펴보고 가자면

UPDATE Board Set name='{name}',context='{context}' WHERE name='{uid}'

Board 테이블에서 name이 {uid}인 것의 name과 context를 Set을 사용하여 재설정 해준다.

 


3) Flask JWT

JWT는 JSON web Token의 약자로, JSON 포맷을 이용하여 사용자에 대한 속성을 저장하는 Web Token이다.
토큰 자체를 정보로 사용하는 Self-Contained 방식으로 정보를 안정하게 전달한다.
이는 웹 표준으로서 두 개체에서 JSON 객체를 사용하여 통신한다.

 

  • JWT의 구성
Header: 토큰의 타입과 알고리즘 저장
Payload: 토큰에 담을 정보를 저장
Signature: 헤더와 정보의 인코딩 값들과 관련된 비밀키 저장

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImVsaWNlIiwiaWF0IjoxNTE2MjM5MDIyfQ.KeQ_ZRON0VcycCQ6YtOIoNfKPvUjn8gNlR6HdE_xsis

 

이런 식으로 생겼다고 한다ㄷ ㄷ.. 중간에 점(빨간색)을 구분자로 사용하며 순서대로 Header, Payload, Signature인데

JSON 형태인 각 부분은 Base64로 인코딩되어 표현된다.

 

  • Header

헤더는 typ와 alg 두 가지 정보로 구성된다.

- typ: 토큰의 타입 지정
- alg: 알고리즘 방식을 지정 (서명 및 토큰 검증에 사용)
EX) { "typ": "JWT", "alg": "HS256" }

 

  • Payload

토큰에서 사용할 정보의 조각들인 클레임(Claim)이 담겨있다.

- 등록된 클레임: 토큰 정보를 표현하기 위해 이미 정해진 종류의 데이터

- 공개 클레임: 충돌이 방지된 이름을 가지고 있어야 한다. 이를 위해 이름을 URI 형식으로 짓는다.
URL이 아니라 URI다!!! EX) { https://hayan.io: true }

- 비공개 클레임: 서버와 클라이언트 사이에 임의로 지장한 정보를 저장 (사용자 정의 클레임)

 

  • Signature

토큰을 인코딩하거나 유효성 검증을 할 때 사용하는 고유한 암호화 코드이다.

서명은, 헤더의 인코딩 값과 정보의 인코딩 값을 합친 후 주어진 비밀키로 해시를 하여 생성한다.

 


(1) JWT를 이용한 로그인 토큰 발급 구현

from flask import Flask, request, render_template, jsonify
import jwt # jwt 모듈을 import

app = Flask(__name__)
encryption_secret = "secret" # 인코드와 디코드에 사용되는 변수
algorithm = "HS256" # 인코드와 디코드에 사용되는 변수

origin = {"name":"hayan", "password":"1234"}


@app.route("/", methods=["GET","POST"])
def jwt_route():
    # 조건문을 이용해 API 요청을 구분
    method = request.method
    if method == 'POST':
        id = request.form['username']
        pw = request.form['password']
        
        # origin에 저장된 name, password와 비교
        if origin['name'] == id and origin['password'] == pw:
            # 정보가 일치하는 경우 사용자 변수를 만들기 위한 딕셔너리
            data_to_encode = {'name':id, 'password':pw}
            # 인증이 완료되면 전송할 encode, decode 정보 저장
            encoded = jwt.encode(data_to_encode, encryption_secret, algorithm=algorithm).decode
            decoded = jwt.decode(encoded, encryption_secret, algorithm =[algorithm]) # decoded의 알고리즘은 리스트에
            # 저장한 정보를 json 형태로 전송
            data = {"encode":encoded, "decode":decoded}
            return jsonify(data)
      
        else:
            return jsonify("User Not Found")
    else:
        return render_template("index.html")
    
    
    
if __name__ == "__main__":
    app.run(debug = True)