개발일기
flask#2 본문
from flask import Flask, render_template, request, redirect, url_for, session
from functools import wraps
import time
import sqlite3
import bcrypt
from collections import defaultdict
app = Flask(__name__)
app.secret_key = 'supersecretkey'
# 로그인 시도 기록
login_attempts = defaultdict(list)
# 로그인 시도 횟수 제한
MAX_ATTEMPTS = 5
LOCK_TIME = 60 * 5 # 5분
###############################################################################
# 기타 함수들 ##################################################################
# login_required라는 데코레이터를 정의
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated_function
# SQLite 연결을 위한 함수
def get_db_connection():
conn = sqlite3.connect('requests.db')
conn.row_factory = sqlite3.Row # 결과를 딕셔너리 형식으로 반환
return conn
# SQLite에 데이터를 저장하는 함수
def save_to_db(request_data):
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('''
INSERT INTO requests (name, chk_info, language, sub_time, user_id)
VALUES (?, ?, ?, ?, ?)
''', (request_data['name'], request_data['chk_info'], request_data['language'], request_data['sub_time'], request_data['user_id']))
conn.commit()
conn.close()
# SQLite에서 데이터를 불러오는 함수
def load_from_db(user_id):
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('SELECT * FROM requests WHERE user_id = ?', (user_id,))
requests = cursor.fetchall()
conn.close()
return requests
# 요청을 삭제하는 함수
def delete_request_from_db(user_id, sub_time):
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('DELETE FROM requests WHERE user_id = ? AND sub_time = ?', (user_id, sub_time))
conn.commit()
conn.close()
# 비밀번호 해싱 함수
def hash_password(password):
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
# 비밀번호 검증 함수
def check_password(stored_hash, password):
return bcrypt.checkpw(password.encode('utf-8'), stored_hash)
users = {
"admin": hash_password("password123"),
"user": hash_password("userpass")
}
###############################################################################
# render 함수들 ###############################################################
# 메인 화면
@login_required
@app.route('/', methods=['GET', 'POST'])
def home():
if request.method == 'POST':
# 전송된 데이터
request_data = {
'name': request.form.get('name'),
'chk_info': request.form.get('chk_info'),
'language': request.form.get('language'),
'sub_time': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
}
# 계정 id와 함께 요청 데이터를 세션에 저장
request_data['user_id'] = session.get('user_id', 'anonymous') # 로그인된 사용자 id가 없으면 'anonymous'
# db에 저장
save_to_db(request_data)
# 세션에 전송 완료 메시지 추가
session['message'] = f'전송이 완료되었습니다!\n{str(request_data)}'
return redirect(url_for('home'))
# 메시지 확인 및 반환
message = session.pop('message', None)
return render_template('index.html', session=session, message=message)
# 지금까지 보낸 요청 확인하기
@app.route('/history', methods=['GET', 'POST'])
@login_required
def history():
user_id = session['user_id'] # 로그인된 사용자 아이디
# SQLite에서 해당 사용자의 요청 데이터를 읽어옴
conn = sqlite3.connect('requests.db')
cursor = conn.cursor()
cursor.execute('''SELECT * FROM requests WHERE user_id = ?''', (user_id,))
requests = cursor.fetchall() # 모든 요청 데이터 가져오기
# 데이터는 튜플 형태로 반환되므로, 이를 딕셔너리로 변환
requests = [{'name': row[1], 'chk_info': row[2], 'language': row[3], 'sub_time': row[4], 'user_id': row[5]} for row in requests]
conn.close() # 연결 종료
return render_template('history.html', requests=requests)
# 회원가입
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if username in users:
return "이미 존재하는 사용자입니다."
# 비밀번호 해싱
hashed_password = hash_password(password)
users[username] = hashed_password # 사용자 이름과 해시된 비밀번호를 저장 (bytes로 저장)
return redirect(url_for('login'))
return render_template('register.html')
# 로그인
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
# 로그인 시도 기록
if username in login_attempts:
attempts = login_attempts[username]
attempts = [t for t in attempts if t > time.time() - LOCK_TIME] # 유효한 시도만 남김
login_attempts[username] = attempts
if len(attempts) >= MAX_ATTEMPTS:
return "로그인 시도가 너무 많습니다. 잠시 후 다시 시도해주세요."
# 사용자 검증
if username in users and check_password(users[username], password):
session['user_id'] = username
login_attempts[username] = [] # 로그인 성공 시 시도 기록 초기화
return redirect(url_for('home'))
# 로그인 실패 기록
login_attempts[username].append(time.time())
return "로그인 실패! 다시 시도하세요."
return render_template('login.html')
# 로그아웃
@app.route('/logout', methods=['GET', 'POST'])
def logout():
session.pop('user_id', None)
return redirect(url_for('home'))
###############################################################################
# 기능 함수들 ##################################################################
# 요청 취소
@app.route('/cancel_request/<sub_time>', methods=['POST'])
@login_required
def cancel_request(sub_time):
user_id = session['user_id'] # 로그인된 사용자 아이디
# SQLite에서 해당 사용자와 요청 시간이 일치하는 데이터를 삭제
conn = sqlite3.connect('requests.db')
cursor = conn.cursor()
# 삭제 쿼리 작성
cursor.execute('''DELETE FROM requests WHERE user_id = ? AND sub_time = ?''', (user_id, sub_time))
conn.commit() # 변경 사항 저장
conn.close() # 연결 종료
return redirect(url_for('history'))
if __name__ == '__main__':
app.run(debug=True)
요새는 GPT만 다룰줄 알아도 대부분의 코드를 쓸 수 있으니 정말 편하다.
작성 과정은 천천히 로그인/로그아웃 구현, session으로 로그인 여부 확인, 요청 기록(history) 확인 구현, csv로 데이터 저장->sqlite로 데이터 저장, 부트스트랩 적용.
template은 base.html을 만들고, 그걸 확장해서 index.html, login.html, register.html, history.hmtl을 만드는 식으로 구성했다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Website</title>
<!-- 부트스트랩 CSS 링크 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<!-- 상단바 (NavBar) -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('home') }}">My Website</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
{% if 'user_id' in session %}
<li class="nav-item">
<span class="nav-link">{{ session['user_id'] }}</span>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('history') }}">요청 내역</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('logout') }}">로그아웃</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('login') }}">로그인</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('register') }}">회원가입</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<!-- 페이지 콘텐츠 -->
<div class="container mt-4">
{% block content %}{% endblock %}
</div>
<!-- 부트스트랩 JS 링크 -->
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.min.js"></script>
</body>
</html>
base.html에는 navbar, 즉 상단바와 관련된 내용을 넣었고.
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<h2 class="mb-4">로그인</h2>
<!-- 로그인 실패 메시지 출력 -->
{% if message %}
<div class="alert alert-danger">
{{ message | e }} <!-- XSS 방어를 위해 자동으로 이스케이프 처리 -->
</div>
{% endif %}
<form method="POST">
<div class="mb-3">
<label for="username" class="form-label">아이디:</label>
<input type="text" id="username" name="username" class="form-control" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">비밀번호:</label>
<input type="password" id="password" name="password" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary">로그인</button>
</form>
<p class="mt-3">아직 계정이 없으신가요? <a href="{{ url_for('register') }}">회원가입 페이지로 이동</a></p>
</div>
{% endblock %}
거기에 extends 해서 넣을 내용을 정해놓는 방식.
그러면 이렇게 훌륭하게 만들어진다.



