Chapter 5-5 Flask 기반 JWT 인증과 Todo 앱 구축

2025. 4. 23. 10:37·Flask

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)

흐름 설명:

  1. Flask 애플리케이션 설정:
    • Flask, SQLAlchemy, JWTManager, Flask-Smorest를 설정합니다.
    • SQLALCHEMY_DATABASE_URI로 SQLite를 사용해 데이터베이스를 설정하고, JWT_SECRET_KEY로 JWT 인증에 필요한 비밀 키를 설정합니다.
  2. 데이터베이스 초기화:
    • db.init_app(app)로 데이터베이스를 초기화하고, migrate는 데이터베이스 마이그레이션을 관리합니다.
  3. Blueprint 설정:
    • 인증과 Todo API를 처리할 블루프린트(auth_blp, todo_blp)를 등록합니다.
  4. 애플리케이션 실행:
    • 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)
 

흐름 설명:

  1. User 모델:
    • 사용자 정보를 저장하는 테이블을 정의합니다.
    • username은 고유해야 하며(unique=True), password_hash는 사용자의 암호를 해시한 값을 저장합니다.
    • set_password() 메서드는 비밀번호를 해시하여 저장하고, check_password() 메서드는 입력된 비밀번호가 해시된 비밀번호와 일치하는지 확인합니다.
  2. 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

흐름 설명:

  1. 사용자 인증 요청:
    • POST 요청을 통해 username과 password를 받아옵니다.
  2. JSON 형식 확인:
    • request.is_json을 통해 요청이 JSON 형식인지를 확인하고, 아니라면 오류 메시지를 반환합니다.
  3. 사용자 확인 및 JWT 발급:
    • 입력된 username을 사용해 DB에서 사용자 정보를 조회하고, 비밀번호가 일치하면 JWT 액세스 토큰을 생성하여 반환합니다.
  4. 잘못된 인증 처리:
    • 비밀번호가 맞지 않거나 사용자가 존재하지 않으면 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})

흐름 설명:

  1. Todo 생성:
    • POST 요청을 통해 Todo 항목을 추가합니다.
    • @jwt_required() 데코레이터로 인증된 사용자만 Todo를 추가할 수 있도록 보호합니다.
    • Todo의 제목을 받아 새로운 Todo를 생성하고 데이터베이스에 저장합니다.
  2. Todo 조회:
    • GET 요청을 통해 현재 로그인한 사용자의 Todo 항목을 조회합니다.
    • 로그인한 사용자만 자신의 Todo 목록을 조회할 수 있도록 @jwt_required()로 보호됩니다.
  3. Todo 수정:
    • PUT 요청을 통해 기존 Todo 항목을 수정합니다.
    • 제목이나 완료 여부를 변경할 수 있습니다.
  4. 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. 실행 및 테스트

  1. 데이터베이스 초기화:
    • flask db init, flask db migrate, flask db upgrade
  2. 유저 추가 예시:
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
'Flask' 카테고리의 다른 글
  • Chapter 5-4 Flask로 만드는 인스타그램 REST API
  • Chapter 5-3 Flask와 Jinja를 활용한 사용자 관리 웹 애플리케이션
  • Chapter 5-2 Flask Authentication / Flask-JWT-Extended
  • Chapter 5-1 Flask Authentication / Flask-login
Chansman
Chansman
안녕하세요! 코딩을 시작한 지 얼마 되지 않은 초보 개발자 찬스맨입니다. 이 블로그는 제 학습 기록을 남기고, 다양한 코딩 실습을 통해 성장하는 과정을 공유하려고 합니다. 초보자의 눈높이에 맞춘 실습과 팁, 그리고 개발하면서 겪은 어려움과 해결 과정을 솔직하게 풀어내려 합니다. 함께 성장하는 개발자 커뮤니티가 되기를 바랍니다.
  • Chansman
    찬스맨의 프로그래밍 스토리
    Chansman
  • 전체
    오늘
    어제
    • 분류 전체보기 (472) N
      • Python (31)
      • 프로젝트 (43)
      • 과제 (21)
      • Database (40)
      • 멘토링 (7) N
      • 특강 (18)
      • 기술블로그 (126) N
      • AI 분석 (4)
      • HTML & CSS (31)
      • JavaScript (17)
      • AWS_Cloud (21)
      • 웹스크래핑과 데이터 수집 (14)
      • Flask (42)
      • Django (34) N
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Chansman
Chapter 5-5 Flask 기반 JWT 인증과 Todo 앱 구축
상단으로

티스토리툴바