Flask와 Vue.js를 활용한 JWT 인증 시스템 구축

JWT (JSON Web Token) 인증의 필요성

최근 웹 애플리케이션은 단일 서버 구성보다는 여러 서비스가 유기적으로 연결된 분산 환경에서 운영되는 경우가 많습니다. 이러한 환경에서 사용자 인증을 처리할 때 세션(Session) 기반 방식은 여러 제약을 가집니다. 예를 들어, 여러 서버에 걸쳐 사용자의 로그인 상태를 유지하려면 복잡한 세션 공유 메커니즘이 필요하거나, 각 서버에 요청이 전달될 때마다 사용자 정보를 다시 확인해야 하는 오버헤드가 발생할 수 있습니다.

JWT는 이러한 문제를 해결하기 위한 효과적인 대안으로 주목받고 있습니다. JWT는 서버가 사용자 인증 성공 시 고유한 토큰을 발행하고, 이 토큰을 클라이언트(브라우저 등)가 저장했다가 다음 요청 시 서버에 함께 전송하는 방식입니다. 서버는 토큰의 유효성을 검증하여 사용자를 식별하며, 이 과정에서 별도의 세션 저장 공간을 유지할 필요가 없어 무상태(stateless) 인증을 구현할 수 있습니다.

JWT를 사용하면 다음과 같은 이점을 얻을 수 있습니다:

  • 무상태성(Stateless): 서버가 클라이언트의 상태를 저장하지 않아도 되므로, 서버 확장성(Scalability)이 향상됩니다.
  • 분산 환경 지원: 여러 서버나 마이크로서비스 간에 사용자 인증 정보를 공유하기 용이합니다.
  • 보안: 토큰은 서명되어 위변조 여부를 확인할 수 있으며, HTTPS와 함께 사용 시 안전하게 전송됩니다.
  • 모바일 친화적: 모바일 애플리케이션에서도 쉽게 적용할 수 있습니다.

Flask 백엔드 구현

Flask 애플리케이션에서 JWT를 사용하여 인증 시스템을 구축하는 방법을 살펴보겠습니다. 먼저 필요한 라이브러리를 설치합니다.

pip install pyjwt Flask

다음은 사용자 로그인 및 토큰 발행, 그리고 발행된 토큰을 검증하는 예시 코드입니다.

from flask import Flask, request, jsonify, current_app
import jwt
from datetime import datetime, timedelta, timezone
import functools

app = Flask(__name__)
# 실제 운영 환경에서는 환경 변수 등을 통해 관리해야 합니다.
app.config['SECRET_KEY'] = 'my_secret_key_for_jwt_signing'

# 로그인 엔드포인트: 사용자 인증 후 JWT 토큰 발행
@app.route("/api/authenticate", methods=["POST"])
def authenticate_user():
    if request.method == 'POST':
        credentials = request.json
        username = credentials.get('username')
        password = credentials.get('password')

        # 실제로는 데이터베이스에서 사용자 정보를 조회하여 검증합니다.
        if username == 'testuser' and password == 'password123':
            # 토큰 페이로드 구성: 사용자 식별 정보, 만료 시간 등
            payload = {
                'user_id': username,
                'exp': datetime.now(timezone.utc) + timedelta(minutes=30) # 토큰 30분 만료
            }
            # JWT 토큰 인코딩 (SECRET_KEY로 서명)
            access_token = jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm="HS256")
            return jsonify({'message': '인증 성공', 'access_token': access_token})
        else:
            return jsonify({'message': '유효하지 않은 사용자명 또는 비밀번호입니다.'}), 401

# JWT 토큰이 필요한 엔드포인트에 적용할 데코레이터
def jwt_required(f):
    @functools.wraps(f)
    def decorated_function(*args, **kwargs):
        # 요청 헤더에서 'Authorization: Bearer <token>' 형식으로 토큰 추출
        auth_header = request.headers.get('Authorization')
        if not auth_header:
            return jsonify({'message': '인증 토큰이 없습니다.'}), 401

        try:
            token_prefix, actual_token = auth_header.split(' ', 1)
            if token_prefix.lower() != 'bearer':
                raise ValueError("유효하지 않은 토큰 형식입니다. 'Bearer'로 시작해야 합니다.")

            # JWT 토큰 디코딩 및 검증 (SECRET_KEY로 서명 검증, 만료 시간 확인)
            decoded_payload = jwt.decode(actual_token, current_app.config['SECRET_KEY'], algorithms=["HS256"])
            # 요청 객체에 사용자 정보를 추가하여 다음 함수에서 사용 가능하도록 함
            request.user_id = decoded_payload['user_id']
        except jwt.ExpiredSignatureError:
            return jsonify({'message': '토큰이 만료되었습니다. 다시 로그인해주세요.'}), 401
        except jwt.InvalidTokenError:
            return jsonify({'message': '유효하지 않은 토큰입니다.'}), 401
        except ValueError as e:
            return jsonify({'message': str(e)}), 400
        except Exception as e:
            return jsonify({'message': f'토큰 처리 중 오류 발생: {e}'}), 500

        return f(*args, **kwargs)
    return decorated_function

# 보호된 리소스 엔드포인트 (JWT 토큰이 있어야 접근 가능)
@app.route('/api/protected_resource', methods=['GET'])
@jwt_required
def get_protected_resource():
    # request.user_id를 통해 인증된 사용자 정보에 접근
    return jsonify({'message': f'{request.user_id}님, 보호된 리소스에 접근했습니다!'})

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

Vue.js 프론트엔드 구현

Vue.js 클라이언트에서 백엔드와 통신하고 JWT 토큰을 관리하는 방법을 알아보겠습니다. 토큰 저장을 위해 vue-cookies 라이브러리를 사용하고, HTTP 요청을 위해 axios를 사용하는 것이 일반적입니다. 먼저 필요한 라이브러리를 설치합니다.

npm install vue-cookies axios

main.js 파일에 vue-cookiesaxios를 전역적으로 설정합니다.

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import VueCookies from 'vue-cookies'
import axios from 'axios'

const app = createApp(App)

// Vue 인스턴스에 쿠키 플러그인 등록
app.use(VueCookies)

// axios를 전역 프로토타입으로 등록하여 컴포넌트에서 this.$http로 접근 가능하게 함
app.config.globalProperties.$http = axios;
// API 기본 URL 설정 (백엔드 서버 주소)
app.config.globalProperties.$http.defaults.baseURL = 'http://127.0.0.1:5000'; // Flask 서버 주소

app.mount('#app')

로그인 컴포넌트 예시 (예: LoginComponent.vue)에서 사용자 인증 정보를 전송하고 토큰을 쿠키에 저장합니다.

<!-- LoginComponent.vue -->
<template>
  <div>
    <h2>로그인</h2>
    <input type="text" v-model="username" placeholder="사용자명" /><br />
    <input type="password" v-model="password" placeholder="비밀번호" /><br />
    <button @click="performLogin">로그인</button>
    <p v-if="loginMessage">{{ loginMessage }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      username: '',
      password: '',
      loginMessage: ''
    };
  },
  methods: {
    async performLogin() {
      try {
        const response = await this.$http.post('/api/authenticate', {
          username: this.username,
          password: this.password
        });
        const accessToken = response.data.access_token;
        // 토큰을 'jwt_access_token'이라는 이름으로 30분간 유효한 쿠키에 저장
        this.$cookies.set('jwt_access_token', accessToken, '30m');
        this.loginMessage = response.data.message + ' 토큰이 저장되었습니다.';
        console.log('로그인 성공:', response.data);
        // 로그인 성공 후 다른 페이지로 리다이렉트
        // this.$router.push('/dashboard'); 
      } catch (error) {
        this.loginMessage = error.response ? error.response.data.message : '로그인 요청 실패';
        console.error('로그인 실패:', error);
      }
    }
  }
};
</script>

보호된 리소스에 접근하는 컴포넌트 예시 (예: ProtectedResource.vue)에서는 저장된 토큰을 HTTP 요청 헤더에 포함시켜 전송합니다.

<!-- ProtectedResource.vue -->
<template>
  <div>
    <h2>보호된 리소스</h2>
    <button @click="fetchProtectedData">보호된 데이터 가져오기</button>
    <p v-if="protectedData">서버 응답: {{ protectedData }}</p>
    <p v-if="errorMessage" style="color: red;">{{ errorMessage }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      protectedData: null,
      errorMessage: ''
    };
  },
  methods: {
    async fetchProtectedData() {
      const token = this.$cookies.get('jwt_access_token');
      if (!token) {
        this.errorMessage = '인증 토큰이 없습니다. 로그인해주세요.';
        return;
      }

      try {
        const response = await this.$http.get('/api/protected_resource', {
          headers: {
            'Authorization': `Bearer ${token}` // 'Bearer ' 접두사를 붙여 토큰 전송
          }
        });
        this.protectedData = response.data.message;
        this.errorMessage = '';
        console.log('보호된 데이터 로드 성공:', response.data);
      } catch (error) {
        this.protectedData = null;
        this.errorMessage = error.response 
            ? error.response.data.message 
            : '데이터 로드 실패: 서버 연결 문제';
        console.error('보호된 데이터 로드 실패:', error);
        // 토큰 만료 등 인증 실패 시 로그인 페이지로 리다이렉트 처리
        // if (error.response && (error.response.status === 401 || error.response.status === 403)) {
        //   this.$router.push('/login');
        // }
      }
    }
  }
};
</script>

태그: flask Vue.js jwt authentication Web Security

8월 27일 06:28에 게시됨