Mini Project3: Flask 기반 JWT 인증과 Todo 앱 구축
이 프로젝트는 Flask와 Flask-Smorest를 사용하여 JWT 인증을 통한 사용자 인증과 Todo 애플리케이션을 구축하는 실습입니다.
목표
- JWT 인증을 통한 사용자 인증 및 관리 구현
- Todo 애플리케이션의 RESTful API 구축
- Flask-Smorest를 활용하여 API 문서화 및 관리
🚦 기능 구현
HTTP 메서드Endpoint설명
GET | /users | 모든 사용자 조회 |
POST | /users | 새로운 사용자 생성 |
POST | /users/post/<username> | 특정 사용자의 Todo 항목 추가 |
GET | /users/post/<username> | 특정 사용자의 Todo 목록 조회 |
PUT | /users/post/like/<username>/<title> | 특정 게시물 좋아요 수 증가 |
DELETE | /users/<username> | 특정 사용자 삭제 |
💻 코드 예시 및 흐름 분석
1. 프로젝트 및 데이터베이스 설정 (app.py)
from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy
from flask_jwt_extended import JWTManager
from flask_smorest import Api
from db import db
from flask_migrate import Migrate
app = Flask(__name__)
# 데이터베이스 및 JWT 설정
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db' # local db => 파일 형태
app.config['JWT_SECRET_KEY'] = 'super-secret-key'
app.config['API_TITLE'] = 'Todo API'
app.config['API_VERSION'] = 'v1'
app.config['OPENAPI_VERSION'] = '3.0.2'
# db 초기화
db.init_app(app)
migrate = Migrate(app, db)
# JWT와 Flask-Smorest 설정
jwt = JWTManager(app)
api = Api(app)
# 모델 불러오기
from models import User, Todo
from routes.auth import auth_blp
from routes.todo import todo_blp
# Blueprint 등록
api.register_blueprint(auth_blp)
api.register_blueprint(todo_blp)
@app.route('/')
def index():
return render_template('index.html')
if __name__ == '__main__':
app.run(debug=True)
흐름 설명:
- Flask 애플리케이션 설정:
- Flask, SQLAlchemy, JWTManager, Flask-Smorest를 설정합니다.
- SQLALCHEMY_DATABASE_URI로 SQLite를 사용해 데이터베이스를 설정하고, JWT_SECRET_KEY로 JWT 인증에 필요한 비밀 키를 설정합니다.
- 데이터베이스 초기화:
- db.init_app(app)로 데이터베이스를 초기화하고, migrate는 데이터베이스 마이그레이션을 관리합니다.
- Blueprint 설정:
- 인증과 Todo API를 처리할 블루프린트(auth_blp, todo_blp)를 등록합니다.
- 애플리케이션 실행:
- app.run(debug=True)로 애플리케이션을 실행합니다.
2. 사용자 모델 및 Todo 모델 정의 (models.py)
from db import db
from werkzeug.security import generate_password_hash, check_password_hash
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
class Todo(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
completed = db.Column(db.Boolean, default=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
흐름 설명:
- User 모델:
- 사용자 정보를 저장하는 테이블을 정의합니다.
- username은 고유해야 하며(unique=True), password_hash는 사용자의 암호를 해시한 값을 저장합니다.
- set_password() 메서드는 비밀번호를 해시하여 저장하고, check_password() 메서드는 입력된 비밀번호가 해시된 비밀번호와 일치하는지 확인합니다.
- Todo 모델:
- 각 사용자의 Todo 항목을 저장합니다.
- title은 Todo 항목의 제목, completed는 완료 여부를 나타내며, user_id는 해당 Todo가 어떤 사용자에게 속하는지 나타냅니다.
3. 사용자 인증 및 JWT 구현 (auth.py)
from flask import request, jsonify
from flask_smorest import Blueprint
from flask_jwt_extended import create_access_token
from models import User
from werkzeug.security import check_password_hash
auth_blp = Blueprint('auth', 'auth', url_prefix='/login', description='Operations on todos')
@auth_blp.route('/', methods=['POST'])
def login():
if not request.is_json:
return jsonify({"msg": "Missing JSON in request"}), 400
username = request.json.get('username', None)
password = request.json.get('password', None)
if not username or not password:
return jsonify({"msg": "Missing username or password"}), 400
user = User.query.filter_by(username=username).first()
if user and check_password_hash(user.password_hash, password):
access_token = create_access_token(identity=username)
return jsonify(access_token=access_token)
else:
return jsonify({"msg": "Bad username or password"}), 401
흐름 설명:
- 사용자 인증 요청:
- POST 요청을 통해 username과 password를 받아옵니다.
- JSON 형식 확인:
- request.is_json을 통해 요청이 JSON 형식인지를 확인하고, 아니라면 오류 메시지를 반환합니다.
- 사용자 확인 및 JWT 발급:
- 입력된 username을 사용해 DB에서 사용자 정보를 조회하고, 비밀번호가 일치하면 JWT 액세스 토큰을 생성하여 반환합니다.
- 잘못된 인증 처리:
- 비밀번호가 맞지 않거나 사용자가 존재하지 않으면 401 에러를 반환합니다.
4. Todo API 구현 (todo.py)
from flask import request, jsonify
from flask_smorest import Blueprint
from flask_jwt_extended import jwt_required, get_jwt_identity
from models import Todo, User, db
todo_blp = Blueprint('todo', 'todo', url_prefix='/todo', description='Operations on todos')
# Todo 생성
@todo_blp.route('/', methods=['POST'])
@jwt_required()
def create_todo():
if not request.is_json:
return jsonify({"msg": "Missing JSON in request"}), 400
title = request.json.get('title', None)
if not title:
return jsonify({"msg": "Missing title"}), 400
username = get_jwt_identity()
user = User.query.filter_by(username=username).first()
new_todo = Todo(title=title, user_id=user.id)
db.session.add(new_todo)
db.session.commit()
return jsonify({"msg": "Todo created", "id": new_todo.id}), 201
# Todo 조회
@todo_blp.route('/', methods=['GET'])
@jwt_required()
def get_todos():
username = get_jwt_identity()
user = User.query.filter_by(username=username).first()
todos = Todo.query.filter_by(user_id=user.id).all()
return jsonify([{"id": todo.id, "title": todo.title, "completed": todo.completed} for todo in todos])
# Todo 수정
@todo_blp.route('/<int:todo_id>', methods=['PUT'])
@jwt_required()
def update_todo(todo_id):
todo = Todo.query.get_or_404(todo_id)
if 'title' in request.json:
todo.title = request.json['title']
if 'completed' in request.json:
todo.completed = request.json['completed']
db.session.commit()
return jsonify({"msg": "Todo updated", "id": todo.id})
# Todo 삭제
@todo_blp.route('/<int:todo_id>', methods=['DELETE'])
@jwt_required()
def delete_todo(todo_id):
todo = Todo.query.get_or_404(todo_id)
db.session.delete(todo)
db.session.commit()
return jsonify({"msg": "Todo deleted", "id": todo_id})
흐름 설명:
- Todo 생성:
- POST 요청을 통해 Todo 항목을 추가합니다.
- @jwt_required() 데코레이터로 인증된 사용자만 Todo를 추가할 수 있도록 보호합니다.
- Todo의 제목을 받아 새로운 Todo를 생성하고 데이터베이스에 저장합니다.
- Todo 조회:
- GET 요청을 통해 현재 로그인한 사용자의 Todo 항목을 조회합니다.
- 로그인한 사용자만 자신의 Todo 목록을 조회할 수 있도록 @jwt_required()로 보호됩니다.
- Todo 수정:
- PUT 요청을 통해 기존 Todo 항목을 수정합니다.
- 제목이나 완료 여부를 변경할 수 있습니다.
- Todo 삭제:
- DELETE 요청을 통해 Todo 항목을 삭제합니다.
- 해당 Todo 항목의 ID를 이용해 삭제합니다.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo App</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="bg-gray-100">
<div class="container mx-auto p-6">
<!-- 헤더 -->
<header class="mb-6">
<h1 class="text-4xl font-bold text-center text-blue-500">Todo App</h1>
<p class="text-center text-gray-700">Welcome to your Todo management app</p>
</header>
<!-- 로그인 박스 -->
<section class="max-w-lg mx-auto bg-white p-6 rounded-lg shadow-lg mb-6">
<h2 class="text-2xl font-semibold text-center text-gray-700 mb-4">Login</h2>
<form id="loginForm" class="space-y-4">
<div>
<label for="username" class="block text-sm font-medium text-gray-600">Username</label>
<input type="text" id="username" name="username" placeholder="Enter username"
class="w-full p-3 border border-gray-300 rounded-md" required>
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-600">Password</label>
<input type="password" id="password" name="password" placeholder="Enter password"
class="w-full p-3 border border-gray-300 rounded-md" required>
</div>
<button type="submit" class="w-full px-6 py-2 bg-blue-500 text-white rounded-full">Login</button>
</form>
</section>
<!-- Todo 생성 폼 -->
<section class="mb-6">
<h2 class="text-2xl font-semibold text-gray-700">Create Todo</h2>
<form id="createTodoForm" class="space-y-4 mt-4">
<input type="text" id="todoTitle" placeholder="Enter todo title"
class="w-full p-3 border border-gray-300 rounded-md" required>
<button type="submit" class="px-6 py-2 bg-green-500 text-white rounded-full">Create Todo</button>
</form>
</section>
<!-- Todo 목록 -->
<section>
<h2 class="text-2xl font-semibold text-gray-700 mb-4">Todo List</h2>
<ul id="todoList" class="space-y-3">
<!-- 여기에 동적으로 Todo 목록이 렌더링됩니다 -->
</ul>
</section>
</div>
<script>
// 로그인 요청 처리
const loginForm = document.getElementById('loginForm');
loginForm.addEventListener('submit', function (event) {
event.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password })
})
.then(response => response.json())
.then(data => {
if (data.access_token) {
// 로그인 성공 시 토큰을 로컬 스토리지에 저장하고, 할 일 목록을 로드
localStorage.setItem('token', data.access_token);
alert('Login successful!');
loadTodos(); // 할 일 목록 로드
} else {
alert('Login failed! Please check your username and password.');
}
});
});
// Todo 목록을 서버에서 가져와서 보여주는 코드
function loadTodos() {
const token = localStorage.getItem('token');
if (!token) {
alert('You need to log in first!');
return;
}
fetch('/todo', {
headers: {
'Authorization': `Bearer ${token}`,
}
})
.then(response => response.json())
.then(data => {
const todoList = document.getElementById('todoList');
todoList.innerHTML = ''; // 목록을 초기화
data.forEach(todo => {
const li = document.createElement('li');
li.classList.add('p-4', 'border', 'border-gray-200', 'rounded-md');
li.innerHTML = `
${todo.title ? todo.title : 'No Title'} - ${todo.completed ? 'Completed' : 'Not Completed'}
<button onclick="toggleComplete(${todo.id})" class="px-4 py-2 bg-blue-500 text-white rounded-md ml-2">${todo.completed ? 'Undo' : 'Complete'}</button>
<button onclick="deleteTodo(${todo.id})" class="px-4 py-2 bg-red-500 text-white rounded-md ml-2">Delete</button>
`;
todoList.appendChild(li);
});
});
}
// 할 일 상태 토글 (Complete/Undo)
function toggleComplete(todoId) {
const token = localStorage.getItem('token');
if (!token) {
alert('You need to log in first!');
return;
}
fetch(`/todo/${todoId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ completed: true }) // or false to undo
})
.then(response => response.json())
.then(data => {
alert('Todo updated!');
loadTodos(); // 할 일 목록 다시 로드
});
}
// 할 일 삭제
function deleteTodo(todoId) {
const token = localStorage.getItem('token');
if (!token) {
alert('You need to log in first!');
return;
}
fetch(`/todo/${todoId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
}
})
.then(response => response.json())
.then(data => {
alert('Todo deleted!');
loadTodos(); // 할 일 목록 다시 로드
});
}
// 할 일 추가 요청 처리
const createTodoForm = document.getElementById('createTodoForm');
createTodoForm.addEventListener('submit', function (event) {
event.preventDefault();
const todoTitle = document.getElementById('todoTitle').value;
const token = localStorage.getItem('token');
if (!token) {
alert('You need to log in first!');
return;
}
fetch('/todo', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ title: todoTitle, completed: false })
})
.then(response => response.json())
.then(data => {
// 새로 추가된 Todo 목록을 업데이트
const todoList = document.getElementById('todoList');
const li = document.createElement('li');
li.classList.add('p-4', 'border', 'border-gray-200', 'rounded-md');
li.textContent = `${data.title} - Not Completed`;
todoList.appendChild(li);
document.getElementById('todoTitle').value = ''; // 입력 필드 초기화
});
});
</script>
</body>
</html>
5. 실행 및 테스트
- 데이터베이스 초기화:
- flask db init, flask db migrate, flask db upgrade
- 유저 추가 예시:
from db import db
from app import app
from models import User
with app.app_context():
new_user = User(username='newuser')
new_user.set_password('user123')
db.session.add(new_user)
db.session.commit()
3.로그인 API로 JWT 토큰 받기:
curl -X POST -H "Content-Type: application/json" -d '{"username":"newuser", "password":"user123"}' http://127.0.0.1:5000/login
4.받은 토큰으로 Todo API 접근:
curl -X POST -H "Authorization: Bearer <JWT_TOKEN>" -H "Content-Type: application/json" -d '{"title":"New Todo"}' http://127.0.0.1:5000/todo
✅ 마무리 및 복습 포인트
- Flask와 Flask-Smorest를 사용하여 JWT 인증을 구현하고 Todo 애플리케이션의 RESTful API를 구축할 수 있습니다.
- JWT를 사용하여 인증된 사용자만 Todo 데이터를 관리하도록 제한할 수 있습니다.
- flask_smorest로 API 문서화 및 OpenAPI(Swagger) 문서를 자동으로 생성할 수 있습니다.
'Flask' 카테고리의 다른 글
Chapter 5-4 Flask로 만드는 인스타그램 REST API (0) | 2025.04.23 |
---|---|
Chapter 5-3 Flask와 Jinja를 활용한 사용자 관리 웹 애플리케이션 (0) | 2025.04.23 |
Chapter 5-2 Flask Authentication / Flask-JWT-Extended (0) | 2025.04.23 |
Chapter 5-1 Flask Authentication / Flask-login (0) | 2025.04.22 |
Chapter 4-8(1) Flask 애플리케이션에서 HTTP 기본 인증 사용 (0) | 2025.04.22 |