
🔐 인증(authentication)이 필요한 이유
🔐 사이트 보안 수준을 높이는 것이 중요
신규 사용자 등록 또는 계정에 로그인할 때만 기밀 자료를 다운로드할 수 있는 사용자 인증 만들기
🔍 유의 사항
- 신규 User 객체 생성
- register.html에 입력한 정보를 가져와 users.db에 email, name, password로 저장
- 사용자 등록이 완료되면 바로 secrets.html 페이지로 이동시키기
- secrets.html 페이지에 'Hello
양식에 입력한 이름!' 이 표시되어야 함
🔍 유의 사항
- secrets.html 페이지에서 파일 다운로드 버튼을 클릭하면 기밀 파일을 다운로드 하도록 변경
- /download 경로에서 서버에 GET 요청
- 플라스크 메소드 send_from_directory() 사용 (문서)
암호화(encryption)를 사용해싱(hashing)으로 암호키나 복호화를 할 필요없게 만들어 보안성을 높임💡 https://plaintextoffenders.com
- 사용자가 웹사이트에서 계정을 생성하거나 비밀번호 초기화 요청을 했을 때 받은 메일을 모은 사이트
- 평문 비밀번호가 메일에 그대로 노출되어 있음
- 보안 수준이 낮은 기업들을 볼 수 있다
[해킹 예시 1]
[해킹 예시 2]
↓
[해킹을 방지하는 방법]
솔팅(salting)으로 '사전 대입 공격'이나 '해시 테이블 공격' 방지솔트(salt)를 생성하여 평문과 결합 후, 해시 함수에 통과시키는 방법솔트 라운드(salt rounds)를 높이기🔍 유의 사항
- 사용자의 비밀번호가 평문 그대로 저장되는 대신 해싱 및 솔트를 한 후 저장하기
- 벡자이크(Werkzeug)의 헬퍼 함수 generate_password_hash() 사용 (문서)
pbkdf2:sha256를 사용하여 비밀번호를 해시하고 솔트 길이는8로 설정
🔍 유의 사항
- 등록하거나 로그인한 사용자만 비밀 페이지에 접속할 수 있도록 /secrets 경로 보호하기
- Flask_Login 패키지 (문서)
- 패키지의 LoginManager 클래스 임포트하고 객체 생성
- user_loader 함수 생성
- 사용자 클래스에서 UserMixin을 구현
- Mixin : 파이썬에 다중 상속을 제공하는 방법
- UserMixin : 패키지에서 제공하는 클래스 객체로, 이를 상속받으면 간단하게 구현 가능
- check_password_hash() 함수를 사용하여 사용자의 비밀번호 확인
- 로그인 양식에 입력한 이메일로 사용자 찾기
- 사용자가 성공적으로 로그인 또는 등록한 경우, login_user() 함수를 사용하여 인증
- 인증된 사용자만 액세스할 수 있도록 /secrets과 /download 경로를 모두 보호
- /logout 경로에는 logout_user() 함수를 사용하고 메인 페이지로 리다이렉트하기
🔍 유의 사항
- Flask Flash 메시지로 사용자가 수행한 작업에 대한 피드백 주기 (문서)
- 템플릿으로 전송되어 표시되는 일회성 메시지
- 페이지를 새로고침하면 사라짐
- 데이터베이스에 없는 이메일로 로그인을 시도할 경우 메시지 + 로그인 페이지로 돌아가기
- check_password_hash 함수가 False를 반환할 경우 메시지 + 로그인 페이지로 돌아가기
- 이미 데이터베이스에 있는 이메일로 가입할 경우 메시지 + 로그인 페이지로 리디렉션
🔍 유의 사항
- 로그인 전에는 네비게이션 바에 로그아웃 버튼을 숨기기
- 로그인했을 때는 홈페이지와 네비게이션 바에 로그인/등록 버튼을 숨기고 로그아웃 버튼 보이기
- flask_login 패키지의 current_user로 현재 로그인 상태를 알 수 있음
⌨️ main.py
from flask import Flask, render_template, request, url_for, redirect, flash, send_from_directory
from werkzeug.security import generate_password_hash, check_password_hash
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import Integer, String
from flask_login import UserMixin, login_user, LoginManager, login_required, current_user, logout_user
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret-key-goes-here'
# CREATE DATABASE
class Base(DeclarativeBase):
pass
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
db = SQLAlchemy(model_class=Base)
db.init_app(app)
# 사용자 인증
login_manager = LoginManager()
login_manager.init_app(app)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
# CREATE TABLE IN DB
class User(UserMixin, db.Model):
id: Mapped[int] = mapped_column(Integer, primary_key=True)
email: Mapped[str] = mapped_column(String(100), unique=True)
password: Mapped[str] = mapped_column(String(100))
name: Mapped[str] = mapped_column(String(1000))
with app.app_context():
db.create_all()
@app.route('/')
def home():
return render_template("index.html")
@app.route('/register', methods=["GET", "POST"])
def register():
if request.method == "POST":
# 입력한 이메일이 이미 데이터베이스에 있으면 로그인 페이지로 이동
if User.query.filter_by(email=request.form.get('email')).first():
flash("You've already signed up with that email, log in instead!")
return redirect(url_for('login'))
# 비밀번호 해싱과 솔팅
hash_and_salted_password = generate_password_hash(
request.form.get('password'),
method='pbkdf2:sha256',
salt_length=8
)
# 데이터베이스에 등록
new_user = User(
email=request.form.get('email'),
name=request.form.get('name'),
password=hash_and_salted_password
)
db.session.add(new_user)
db.session.commit()
# 데이터베이스에 새 유저로 저장 후 로그인 및 인증 진행
login_user(new_user)
return redirect(url_for("secrets", name=new_user.name))
return render_template("register.html")
@app.route('/login', methods=["GET", "POST"])
def login():
if request.method == "POST":
email = request.form.get('email')
password = request.form.get('password')
user = User.query.filter_by(email=email).first()
# 이메일이 데이터베이스에 존재하지 않을 경우 로그인 페이지로 돌아감
if not user:
flash("That email does not exist, please try again.")
return redirect(url_for('login'))
# 입력한 비밀번호의 해시가 해당 유저의 비밀번호 해시값과 다른 경우 로그인 페이지로 돌아감
elif not check_password_hash(user.password, password):
flash('Password incorrect, please try again.')
return redirect(url_for('login'))
# 이메일이 존재하고 비밀번호도 올바를 경우 로그인
else:
login_user(user)
return redirect(url_for('secrets', name=user.name))
return render_template("login.html")
@app.route('/secrets/<name>')
@login_required
def secrets(name):
return render_template("secrets.html", name=name)
@app.route('/logout')
def logout():
logout_user()
return redirect(url_for('home'))
@app.route('/download')
@login_required
def download():
return send_from_directory(
directory="static", path="files/cheat_sheet.pdf", as_attachment=True
)
if __name__ == "__main__":
app.run(debug=True)
🏗️ base.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Flask Authentication</title>
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
/>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/styles.css')}}"
/>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/">Flask Authentication</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('home') }}">Home</a>
</li>
<!-- 로그인 한 유저에게는 네비게이션 바의 로그인/등록 버튼을 숨기고 로그아웃 버튼 보이기 -->
{% if not current_user.is_authenticated: %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('login') }}">Login</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('register') }}">Register</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('logout') }}">Log Out</a>
</li>
{% endif %}
</ul>
</div>
</nav>
{% block content %} {% endblock %}
</body>
</html>
🏗️ index.html
{% extends "base.html" %} {% block content %}
<div class="box">
<h1>Flask Authentication</h1>
<!-- 로그인 한 유저에게는 로그인/등록 버튼을 숨기기 -->
{% if not current_user.is_authenticated: %}
<a href="{{ url_for('login') }}" class="btn btn-primary btn-block btn-large">Login</a>
<a
href="{{ url_for('register') }}"
class="btn btn-secondary btn-block btn-large"
>Register</a
>
{% else %}
<p>( you are already logged in )</p>
{% endif %}
</div>
{% endblock %}
🏗️ login.html
{% extends "base.html" %} {% block content %}
<div class="box">
<h1>Login</h1>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<p>{{ message }}</p>
{% endfor %}
{% endif %}
{% endwith %}
<form action="{{ url_for('login') }}" method="post">
<input type="text" name="email" placeholder="Email" required="required" />
<input type="password" name="password" placeholder="Password" required="required" />
<button type="submit" class="btn btn-primary btn-block btn-large">Let me in.</button>
</form>
</div>
{% endblock %}
🏗️ register.html
{% extends "base.html" %}
{% block content %}
<div class="box">
<h1>Register</h1>
<form action="{{ url_for('register') }}" method="post">
<input type="text" name="name" placeholder="Name" required="required" />
<input type="email" name="email" placeholder="Email" required="required" />
<input type="password" name="password" placeholder="Password" required="required" />
<button type="submit" class="btn btn-primary btn-block btn-large">Sign me up.</button>
</form>
</div>
{% endblock %}
🏗️ secrets.html
{% extends "base.html" %} {% block content %}
<div class="container">
<h1 class="title">Welcome, {{ name }}!</h1>
<a href="{{ url_for('download') }}">Download Your File</a>
</div>
{% endblock %}