세션 인증은 웹 애플리케이션에서 사용자의 상태를 유지하기 위해 주로 사용된다. 세션 인증은 사용자가 로그인하면 서버 측에서 그 사용자의 상태(인증 정보)를 기록하고, 사용자가 로그아웃하면 그 상태를 제거하는 방식으로 작동한다. 이를 통해 사용자가 애플리케이션 내에서 다양한 요청을 수행할 때 로그인 상태를 유지할 수 있다.
세션의 필요성
: 세션은 사용자가 웹 사이트에 접속해 있는 동안 서버에 유지되는 상태 정보다. 웹은 기본적으로 상태가 없는(stateless) 프로토콜이므로, 웹 서버는 HTTP 요청 간 사용자의 상태를 기본적으로 기억하지 못한다. 세션은 이러한 한계를 극복하기 위해 사용자의 상태(예: 로그인 여부, 사용자 설정 등)를 서버에 저장한다.
세션 인증의 사용 사례
세션 인증의 단점
SECRET_KEY
유출의 위험이 존재한다.세션 인증의 동작 방식
로그인
: 사용자가 로그인 폼을 통해 자신의 자격 증명(예: 사용자 이름, 비밀번호)을 제출하면, 서버는 이를 검증하고 세션에 사용자의 인증 정보를 저장한다. 이 정보는 서버의 메모리나 데이터베이스 등에 저장될 수 있다.
세션 ID
: 사용자의 인증 정보가 세션에 저장되면, 서버는 고유한 세션 ID를 생성하여 사용자의 브라우저에 쿠키 형태로 보낸다. 사용자는 이후의 모든 요청에서 이 세션 ID를 서버에 전송한다.
상태 유지
: 서버는 받은 세션 ID를 사용하여 사용자의 인증 상태와 관련 데이터를 조회한다. 이를 통해 사용자가 로그인한 상태인지 확인하고, 해당 사용자에게 적절한 응답을 제공할 수 있다.
로그아웃
: 사용자가 로그아웃하면, 서버는 해당 사용자의 세션을 제거하고 세션 ID를 무효화한다.
flask에서 제공하는 기본 session 모듈을 사용하면 세션 인증이 가능하다.
app.py
from flask import Flask, render_template, request, redirect, url_for, session, flash
app = Flask(__name__)
# from datetime import timedelta
# app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7) # 예: 7일
app.secret_key = 'your_secret_key' # 실제 배포시에는 .env or yaml 파일에 따로 저장 필요
# 예시 사용자 데이터
users = {
"john": "pw123",
"leo": "pw123"
}
@app.route('/')
def index():
return render_template('login.html')
# 사용자가 login.html 폼을 통해 로그인 정보를 제출하면, login 뷰에서 이를 검증
# 검증이 성공하면, 사용자 이름을 세션에 저장하고 비밀 페이지로 리디렉션
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
if username in users and users[username] == password:
session['username'] = username
# session.permanent = True # 세션 유지 기간을 활성화
return redirect('/secret')
else:
flash('Invalid username or password')
return redirect('/')
# 인증된 사용자 페이지
# secret 뷰에서는 사용자가 로그인되어 있는지 세션을 통해 확인하고,
# 로그인되지 않은 사용자는 로그인 페이지로 리디렉션합니다.
@app.route('/secret')
def secret():
if 'username' in session:
return render_template('secret.html')
else:
return redirect('/')
# 사용자가 로그아웃 링크를 클릭하면,
# logout 뷰에서 세션에서 사용자 정보를 제거하고 초기 페이지로 리디렉션
@app.route('/logout')
def logout():
session.pop('username', None)
return redirect('/')
if __name__ == '__main__':
app.run(debug=True)
templates/login.html
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
</head>
<body>
<h2>Login</h2>
<form method="POST" action="/login">
<input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Login</button>
</form>
</body>
</html>
templates/secret.html
<!DOCTYPE html>
<html>
<head>
<title>Secret Page</title>
</head>
<body>
<h2>Welcome to the Secret Page!</h2>
<a href="/logout">Logout</a>
</body>
</html>
성공적으로 로그인이 완료되면 쿠키에 session 데이터가 생성되어 있는 것을 볼 수 있다.
로그아웃 버튼을 클릭하면 쿠키에 있는 session 값은 저절로 사라진다.
session 객체는 사용자의 브라우저에 저장된 쿠키와 연결되어 있으며, 사전(dictionary) 형태로 작동하여 세션 데이터를 저장하고 접근한다.
session['username'] = 'john'
username = session['username']
username = session.get('username')
session.pop('username', None)
clear
메소드를 사용하여 세션의 모든 데이터를 제거할 수 있다.session.clear()
permanent
속성을 True
로 설정하여 세션의 유지 기간을 PERMANENT_SESSION_LIFETIME
설정값에 따라 조정 가능하다.
app.py
from datetime import timedelta
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7) # 예: 7일
routes/user.py
@app.route('/login', methods=['POST'])
def login():
session['username'] = 'your_username'
session.permanent = True # 세션 유지 기간을 활성화
return redirect(url_for('secret'))
Flask에서의 HTTP 기본 인증(Basic Authentication) 방법이다. 서버에 요청을 보낼 때 HTTP 헤더에 사용자 이름과 비밀번호를 함께 전송하여 인증을 수행한다. 이때 Flask의 flask_httpauth
라이브러리를 사용하여 기본 인증을 구현한다.
> pip install Flask-HTTPAuth
위 코드로 Flask-HTTPAuth 라이브러리를 설치할 수 있다.
app.py
from flask import Flask, jsonify, render_template
from flask_httpauth import HTTPBasicAuth
app = Flask(__name__)
auth = HTTPBasicAuth()
# 사용자 정보
users = {
"admin": "secret",
"guest": "guest"
}
@auth.verify_password
def verify_password(username, password):
if username in users and users[username] == password:
return username
@app.route('/protected')
@auth.login_required
def protected():
return render_template('secret.html')
@app.route('/')
def index():
return render_template('index.html')
if __name__ == '__main__':
app.run(debug=True)
@auth.verify_password
(사용자 인증)
: 사용자 이름과 비밀번호가 유효한지 확인하는 함수를 정의한다. 여기서는 간단한 사전 users
를 사용하여 사용자 이름과 비밀번호를 확인한다. 실전 환경에서는 데이터베이스 또는 다른 안전한 저장소를 사용해야한다.
@auth.login_required
(라우트 보호)
: 인증된 사용자만 해당 라우트로 접근할 수 있도록하는 목적으로 사용자 인증을 요구한다.
@auth.error_handler
(오류 핸들링)
: 인증에 실패했을 때의 동작을 정의한다.
위 코드에서는 403 상태 코드와 함께 오류 메시지를 반환
보안을 강화하기 위해서는 HTTPS를 사용하는 것이 좋으며, 실제 프로덕션 환경에서는 더 견고한 인증 방식(예: OAuth, JWT)을 사용하는 것이 권장된다.
<!DOCTYPE html>
<html>
<head>
<title>Home Page</title>
</head>
<body>
<h1>Welcome to the Home Page</h1>
<a href="/protected">Go to Protected Resource</a>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Secret Page</title>
</head>
<body>
<h1>Secret Page</h1>
<p>Welcome to the secret page!</p>
<!-- 여기에 보호된 콘텐츠나 기능을 추가할 수 있습니다. -->
<a href="/">Home</a>
<a href="/logout">Logout</a>
</body>
</html>
사용자 인증을 쉽게 관리할 수 있도록 도와주는 라이브러리이다. 사용자 로그인 및 로그아웃 프로세스를 처리하고, 현재 로그인한 사용자의 정보에 접근 가능하게 해준다.
> pip install flask-login
위 코드로 Flask-Login 라이브러리를 설치할 수 있다.
flask_login/
│
├── app.py
├── models.py
├── routes.py
└── templates/
├── index.html
└── login.html
app.py
from flask import Flask
from flask_login import LoginManager
from models import User
# 'routes' 모듈을 임포트하기 전에 'app'과 'login_manager' 객체를 생성해야 함
app = Flask(__name__)
app.secret_key = 'your_secret_key'
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
@login_manager.user_loader
def load_user(user_id):
return User.get(user_id)
# 이제 'routes' 모듈을 임포트
from routes import configure_routes
configure_routes(app)
if __name__ == "__main__":
app.run(debug=True)
models.py
from flask_login import UserMixin
users = {'admin': {'password': 'secret'}}
class User(UserMixin):
def __init__(self, username):
self.id = username
@staticmethod
def get(user_id):
if user_id in users:
return User(user_id)
return None
routes.py
from flask import render_template, redirect, url_for, request, flash
from flask_login import login_user, logout_user, login_required
from models import User, users
def configure_routes(app):
@app.route('/')
def index():
return render_template('index.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = User.get(username)
if user and users[username]['password'] == password:
login_user(user)
return redirect(url_for('index'))
else:
flash('Invalid username or password')
return render_template('login.html')
@app.route('/logout')
def logout():
logout_user()
return redirect(url_for('index'))
@app.route('/protected')
@login_required
def protected():
return '<h1>Protected area</h1> <a href="/logout">Logout</a>'
index.html
<!DOCTYPE html>
<html>
<head>
<title>Home Page</title>
</head>
<body>
<h1>Welcome to the Home Page</h1>
<a href="/login">Login</a> |
<a href="/logout">Logout</a> |
<a href="/protected">Protected Page</a>
</body>
</html>
login.html
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
</head>
<body>
<h1>Login</h1>
<form method="post">
Username: <input type="text" name="username" /><br />
Password: <input type="password" name="password" /><br />
<input type="submit" value="Login" />
</form>
</body>
</html>
테스트 방법
http://127.0.0.1:5000/
으로 접속한다.admin
, password: secret
).위 예제는 Flask-Login
의 기본적인 사용 방법을 보여주는 예제다. 실제 서비스 운영시에는 데이터베이스와 함께 사용자 인증 로직을 더 복잡하게 구성할 수 있다.
JSON Web Tokens (JWT)을 쉽게 다룰 수 있도록 해주는 확장 라이브러리다.
> pip install Flask-JWT-Extended
JWT-Extended/
│
├── app.py
├── jwt_utils.py
├── models/user.py
├── routes/user.py
└── templates/
├── index.html
├── login.html
└── protexted.html
app.py
from flask import Flask, render_template
from routes.user import user_bp
from jwt_utils import configure_jwt # JWT 설정 함수를 임포트합니다.
app = Flask(__name__)
configure_jwt(app) # JWT 관련 추가 설정을 적용합니다.
app.register_blueprint(user_bp, url_prefix='/user')
@app.route('/')
def index():
return render_template('index.html')
if __name__ == '__main__':
app.run(debug=True)
models/user.py
: 사용자 모델 정의
class User:
def __init__(self, id, username, password):
self.id = id
self.username = username
self.password = password
jwt_utils.py
: JWT 관련 유틸리티 함수와 설정 정의
from flask_jwt_extended import JWTManager
from blocklist import BLOCKLIST
from flask import jsonify
jwt = JWTManager()
def configure_jwt(app):
app.config["JWT_SECRET_KEY"] = "your-secret-key"
jwt.init_app(app)
# token expired time settings
freshness_in_minutes = 1
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = freshness_in_minutes * 60 # 1 hour
jwt.init_app(app)
# claim: 청구하다
# 추가적인 정보를 토큰에 넣고 싶을 때 사용
@jwt.additional_claims_loader # @데코레이터
def add_claims_to_jwt(identity):
if identity == 1:
return {"is_admin": True}
return {"is_admin": False}
# 토큰이 블록리스트에 있는지 확인하는 함수
# 블록리스트에 있으면 해당 토큰이 유효하지 않다고 판단
@jwt.token_in_blocklist_loader
def check_if_token_in_blocklist(jwt_header, jwt_payload):
# jti=jwt id
return jwt_payload["jti"] in BLOCKLIST
# 만료된 토큰이 사용되었을 때 실행되는 함수
@jwt.expired_token_loader
def expired_token_callback(jwt_header, jwt_payload):
return jsonify({"msg": "Token expired", "error": "token_expired"}), 401
# 유효하지 않은 토큰이 사용되었을 때 실행되는 함수
# 토큰의 서명이나 구조가 유효하지 않을 때 실행됩니다. 주로 토큰 자체의 문제로 발생하는 경우에 해당합니다.
@jwt.invalid_token_loader
def invalid_token_callback(error):
return (
jsonify(
{"message": "Invalid token", "error": "invalid_token"}
),
401,
)
# 해당 토큰으로 접근 권한이 없는 경우
@jwt.unauthorized_loader
def missing_token_callback(error):
return (
jsonify(
{
"description": "Access token required",
"error": "access_token_required",
}
),
401,
)
# fresh한 토큰이 필요한데 fresh하지 않은 토큰이 사용되었을 때 실행되는 함수를 정의합니다.
# 해당 응답을 반환하여 fresh한 토큰이 필요하다는 메시지를 전달
# JWT_ACCESS_TOKEN_EXPIRES으로 토큰 만료 시간 조정
@jwt.needs_fresh_token_loader
def token_not_fresh_callback(jwt_header, jwt_payload):
return (
jsonify(
{"description": "Token is not fresh.", "error": "fresh_token_required"}
),
401,
)
# 토큰이 폐기되었을 때 실행되는 함수를
@jwt.revoked_token_loader
def revoked_token_callback(jwt_header, jwt_payload):
return (
jsonify(
{"description": "Token has been revoked.", "error": "token_revoked"}
),
401,
)
blocklist.py
: jwt_utils.py 파일 안에 있는 blocklist 관련 함수 정의, 토큰 관리를 도와주는 파일
BLOCKLIST = set()
def add_to_blocklist(jti):
BLOCKLIST.add(jti)
def remove_from_blocklist(jti):
BLOCKLIST.discard(jti)
routes/user.py
from flask import Blueprint, jsonify, request, render_template
from flask_jwt_extended import create_access_token, create_refresh_token, jwt_required, get_jwt_identity
from models.user import User
user_bp = Blueprint('user', __name__)
# 임시 사용자 데이터
users = {
'user1': User('1', 'user1', 'pw123'),
'user2': User('2', 'user2', 'pw123')
}
@user_bp.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.json.get('username', None)
password = request.json.get('password', None)
user = users.get(username)
if user and user.password == password:
access_token = create_access_token(identity=username)
refresh_token = create_refresh_token(identity=username)
return jsonify(access_token=access_token, refresh_token=refresh_token)
else:
return jsonify({"msg": "Bad username or password"}), 401
else:
return render_template('login.html')
@user_bp.route('/protected', methods=['GET'])
@jwt_required()
def protected():
current_user = get_jwt_identity()
return jsonify(logged_in_as=current_user), 200
@user_bp.route('/protected_page')
def protected_page():
return render_template('protected.html')
from flask_jwt_extended import get_jwt
from blocklist import add_to_blocklist # 블랙리스트 관리 모듈 임포트
@user_bp.route('/logout', methods=['POST'])
@jwt_required()
def logout():
jti = get_jwt()["jti"]
add_to_blocklist(jti) # jti를 블랙리스트에 추가
return jsonify({"msg": "Successfully logged out"}), 200
templates
index.html
<!DOCTYPE html>
<html>
<head>
<title>Home Page</title>
</head>
<body>
<h1>Welcome to the Home Page</h1>
<a href="/user/login">Login</a> | <a href="/user/logout">Logout</a>
<a href="/user/protected">Protected Page</a>
</body>
</html>
```
login.html
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<script>
function handleLogin(event) {
event.preventDefault(); // 폼의 기본 제출 동작을 방지
// 폼 데이터를 JSON으로 변환
var username = document.getElementById("username").value;
var password = document.getElementById("password").value;
var data = { username: username, password: password };
// fetch를 사용하여 서버에 POST 요청 보내기
fetch("/user/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
})
.then((response) => response.json())
.then((data) => {
console.log("Success:", data);
// 로그인 성공 후 'protected' 페이지로 리다이렉트
localStorage.setItem("access_token", data.access_token);
localStorage.setItem("refresh_token", data.refresh_token);
window.location.href = "/user/protected_page";
})
.catch((error) => {
console.error("Error:", error);
});
}
</script>
</head>
<body>
<h1>Login</h1>
<form onsubmit="handleLogin(event)">
Username: <input type="text" id="username" name="username" /><br />
Password: <input type="password" id="password" name="password" /><br />
<input type="submit" value="Login" />
</form>
</body>
</html>
```
protexted.html
<!DOCTYPE html>
<html>
<head>
<title>Protected Page</title>
<script>
document.addEventListener("DOMContentLoaded", function () {
const token = localStorage.getItem("access_token");
console.log("token", token);
if (token) {
fetch("/user/protected", {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then((response) => {
if (response.ok) {
return response.json();
} else {
throw new Error("Access Denied");
}
})
.then((data) => {
document.getElementById("content").innerHTML =
"Welcome, " + data.logged_in_as;
})
.catch((error) => {
document.getElementById("content").innerHTML = "Access Denied";
console.error("Error:", error);
});
} else {
document.getElementById("content").innerHTML =
"No token found, please login.";
}
});
</script>
</head>
<body>
<h1>This is a Protected Page</h1>
<div id="content">
<p>Loading...</p>
</div>
<button onclick="logout()">Logout</button>
</body>
<script>
// 로그아웃 함수
function logout() {
// 로컬 스토리지에서 JWT 토큰 제거
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
// 로그인 페이지 또는 홈페이지로 리다이렉트
window.location.href = "/";
}
</script>
</html>
```
정리
Flask JWT-Extended를 사용하여 간단한 JWT 기반 인증 시스템을 구축한다.
app.py
에서 Flask 애플리케이션을 설정하고 JWT 인증 라우트를 정의한다.
models/user.py
에서 사용자 모델을 정의한다.
jwt_utils.py
에서 JWT 설정과 인증 관련 유틸리티 함수를 정의한다.
from flask_jwt_extended import jwt_required
@jwt_required()
def protected_route():
# 이 라우트는 JWT가 필요하다.
# JWT가 유효하면 실행된다.
이 데코레이터는 해당 엔드포인트에 접근하려면 JWT가 필요하다는 것을 나타낸다. 클라이언트는 유효한 JWT를 제공해야만 해당 라우트에 접근할 수 있다.
from flask_jwt_extended import jwt_optional
@jwt_optional()
def optional_route():
# 이 라우트는 JWT가 선택적이다.
# JWT가 제공되면 유효성을 확인하고, 제공되지 않으면 계속 진행한다.
이 데코레이터는 해당 엔드포인트에 클라이언트가 JWT를 제공할 수 있지만, 필수적이지 않다는 것을 나타낸다. 클라이언트가 JWT를 제공하면 이를 확인하고 유효성을 검사하며, 제공되지 않으면 계속 진행한다.
from flask_jwt_extended import fresh_jwt_required
@fresh_jwt_required()
def fresh_route():
# 이 라우트는 fresh한 JWT가 필요하다.
# JWT가 fresh하지 않으면 해당 라우트에 접근할 수 없다.
이 데코레이터는 해당 엔드포인트에 접근하려면 fresh한 JWT가 필요하다는 것을 나타낸다. JWT가 fresh하지 않으면 해당 라우트에 접근할 수 없다. 또한 @jwt_required(optional=True)
, @jwt_required(fresh=True, optional=True)
등의 옵션을 사용하여 더 세부적인 제어가 가능하다.
from flask_jwt_extended import jwt_refresh_token_required
@jwt_refresh_token_required()
def refresh_route():
# 이 라우트는 refresh token이 필요하다.
# refresh token이 유효하면 해당 라우트에 접근할 수 있다.
이 데코레이터는 해당 엔드포인트에 접근하려면 refresh token이 필요하다는 것을 나타낸다. refresh token이 유효하면 해당 라우트에 접근할 수 있다.
본격적으로 로그인, 로그아웃, 쿠키 생성, 접근 관리 등을 하다보니 재밌다!