소개
안전한 인증 구현은 강력한 Python 클라이언트 - 서버 시스템을 구축하는 데 필수적인 측면입니다. 이 튜토리얼에서는 기본적인 사용자 이름과 비밀번호 인증부터 토큰 기반 방식까지, Python 애플리케이션에서 인증을 구현하는 과정을 안내합니다. 이 Lab 을 완료하면 적절한 인증 메커니즘을 통해 Python 애플리케이션을 안전하게 보호하는 방법을 이해하게 될 것입니다.
안전한 인증 구현은 강력한 Python 클라이언트 - 서버 시스템을 구축하는 데 필수적인 측면입니다. 이 튜토리얼에서는 기본적인 사용자 이름과 비밀번호 인증부터 토큰 기반 방식까지, Python 애플리케이션에서 인증을 구현하는 과정을 안내합니다. 이 Lab 을 완료하면 적절한 인증 메커니즘을 통해 Python 애플리케이션을 안전하게 보호하는 방법을 이해하게 될 것입니다.
인증은 보호된 리소스에 대한 접근 권한을 부여하기 전에 사용자 또는 시스템의 신원을 확인하는 과정입니다. 클라이언트 - 서버 시스템에서 적절한 인증은 권한이 있는 사용자만 민감한 데이터에 접근하거나 특정 작업을 수행할 수 있도록 보장합니다.
작업 환경을 설정하는 것으로 시작해 보겠습니다. 먼저, 프로젝트를 위한 새 디렉토리를 생성합니다.
mkdir -p ~/project/auth_demo
cd ~/project/auth_demo
다음으로, 이 Lab 전체에서 사용할 필수 Python 패키지를 설치해야 합니다.
pip install flask flask-login requests
다음과 유사한 출력을 볼 수 있습니다.
Collecting flask
Downloading Flask-2.2.3-py3-none-any.whl (101 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 101.8/101.8 KB 6.2 MB/s eta 0:00:00
Collecting flask-login
Downloading Flask_Login-0.6.2-py3-none-any.whl (17 kB)
Collecting requests
Downloading requests-2.29.0-py3-none-any.whl (62 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.5/62.5 KB 5.5 MB/s eta 0:00:00
...
Successfully installed flask-2.2.3 flask-login-0.6.2 requests-2.29.0 ...
구현에 들어가기 전에 기본적인 인증 개념을 이해해 보겠습니다.
사용자 이름 및 비밀번호 인증: 사용자가 신원을 확인하기 위해 자격 증명을 제공하는 가장 일반적인 형태입니다.
토큰 기반 인증: 성공적인 로그인 후, 서버는 클라이언트가 후속 요청에 사용할 토큰을 생성하여 매번 자격 증명을 보낼 필요가 없도록 합니다.
세션 기반 인증: 서버는 성공적인 로그인 후 세션 정보를 저장하고 클라이언트에게 세션 ID 를 제공합니다.
기본적인 인증 흐름을 시각화해 보겠습니다.
Client Server
| |
|--- Login Request (credentials) ---->|
| |--- Verify Credentials
| |
|<---- Authentication Response -------|
| |
|--- Subsequent Requests (with auth) >|
| |--- Verify Auth
| |
|<---- Protected Resource Response ---|
이제 기본 사항을 이해하고 환경을 설정했으므로, 프로젝트를 위한 간단한 파일 구조를 생성해 보겠습니다.
touch ~/project/auth_demo/server.py
touch ~/project/auth_demo/client.py
다음 단계에서는 서버 측에서 Flask 를 사용하여 기본적인 사용자 이름 및 비밀번호 인증 시스템을 구현하고, Python 클라이언트를 사용하여 인증합니다.
이 단계에서는 사용자 이름과 비밀번호를 사용하는 기본적인 인증 시스템을 구현합니다. 다음을 생성합니다.
기본적인 인증 기능을 갖춘 Flask 서버를 구현해 보겠습니다. VSCode 편집기에서 server.py 파일을 엽니다.
code ~/project/auth_demo/server.py
이제 다음 코드를 추가하여 간단한 인증 서버를 구현합니다.
from flask import Flask, request, jsonify, session
import os
app = Flask(__name__)
## Generate a random secret key for session management
app.secret_key = os.urandom(24)
## Simple in-memory user database
users = {
"alice": "password123",
"bob": "qwerty456"
}
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
## Extract username and password from the request
username = data.get('username')
password = data.get('password')
## Check if the user exists and the password is correct
if username in users and users[username] == password:
session['logged_in'] = True
session['username'] = username
return jsonify({"status": "success", "message": f"Welcome {username}!"})
else:
return jsonify({"status": "error", "message": "Invalid credentials"}), 401
@app.route('/protected', methods=['GET'])
def protected():
## Check if the user is logged in
if session.get('logged_in'):
return jsonify({
"status": "success",
"message": f"You are viewing protected content, {session.get('username')}!"
})
else:
return jsonify({"status": "error", "message": "Authentication required"}), 401
@app.route('/logout', methods=['POST'])
def logout():
## Remove session data
session.pop('logged_in', None)
session.pop('username', None)
return jsonify({"status": "success", "message": "Logged out successfully"})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
이 서버는 다음과 같습니다.
이제 서버로 인증할 수 있는 클라이언트를 만들어 보겠습니다. client.py 파일을 엽니다.
code ~/project/auth_demo/client.py
다음 코드를 추가합니다.
import requests
import json
## Server URL
BASE_URL = "http://localhost:5000"
def login(username, password):
"""Authenticate with the server using username and password"""
response = requests.post(
f"{BASE_URL}/login",
json={"username": username, "password": password}
)
print(f"Login response: {response.status_code}")
print(response.json())
## Return the session cookies if login was successful
return response.cookies if response.status_code == 200 else None
def access_protected(cookies=None):
"""Access a protected resource"""
response = requests.get(f"{BASE_URL}/protected", cookies=cookies)
print(f"Protected resource response: {response.status_code}")
print(response.json())
return response
def logout(cookies=None):
"""Logout from the server"""
response = requests.post(f"{BASE_URL}/logout", cookies=cookies)
print(f"Logout response: {response.status_code}")
print(response.json())
return response
if __name__ == "__main__":
## Test with valid credentials
print("=== Authenticating with valid credentials ===")
cookies = login("alice", "password123")
if cookies:
print("\n=== Accessing protected resource ===")
access_protected(cookies)
print("\n=== Logging out ===")
logout(cookies)
## Test with invalid credentials
print("\n=== Authenticating with invalid credentials ===")
login("alice", "wrongpassword")
## Try to access protected resource without authentication
print("\n=== Accessing protected resource without authentication ===")
access_protected()
이 클라이언트는 다음과 같습니다.
인증 시스템이 작동하는 것을 확인하려면 서버와 클라이언트를 모두 실행해야 합니다. 먼저, 서버를 시작해 보겠습니다.
python ~/project/auth_demo/server.py
다음과 유사한 출력을 볼 수 있습니다.
* Serving Flask app 'server'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
이제 터미널 옆에 있는 "+" 버튼을 클릭하여 새 터미널 탭을 열고 클라이언트를 실행합니다.
cd ~/project/auth_demo
python client.py
다음과 같은 출력을 볼 수 있습니다.
이 출력은 기본적인 사용자 이름 및 비밀번호 인증 시스템이 올바르게 작동하고 있음을 확인합니다.
다음 단계에서는 보안을 개선하기 위해 토큰 기반 인증을 구현하여 이 시스템을 개선할 것입니다.
이전 단계에서는 세션을 사용하여 기본적인 인증 시스템을 만들었습니다. 이는 간단한 애플리케이션에 적합하지만, 토큰 기반 인증은 특히 API 및 분산 시스템에 여러 가지 이점을 제공합니다.
토큰 기반 인증에서:
토큰 기반 인증을 위한 널리 사용되는 표준인 JSON Web Token (JWT) 을 사용하도록 시스템을 업그레이드해 보겠습니다.
먼저, PyJWT 패키지를 설치해야 합니다.
pip install pyjwt
설치를 확인하는 출력을 볼 수 있습니다.
Collecting pyjwt
Downloading PyJWT-2.6.0-py3-none-any.whl (20 kB)
Installing collected packages: pyjwt
Successfully installed pyjwt-2.6.0
세션 대신 JWT 토큰을 사용하도록 서버를 수정해 보겠습니다. server.py 파일을 업데이트합니다.
code ~/project/auth_demo/server.py
기존 코드를 다음으로 바꿉니다.
from flask import Flask, request, jsonify
import jwt
import datetime
import os
app = Flask(__name__)
## Secret key for signing JWT tokens
SECRET_KEY = os.urandom(24)
## Simple in-memory user database
users = {
"alice": "password123",
"bob": "qwerty456"
}
def generate_token(username):
"""Generate a JWT token for the authenticated user"""
## Token expires after 30 minutes
expiration = datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
payload = {
'username': username,
'exp': expiration
}
## Create the JWT token
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
return token
def verify_token(token):
"""Verify the JWT token"""
try:
## Decode and verify the token
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
return payload
except jwt.ExpiredSignatureError:
## Token has expired
return None
except jwt.InvalidTokenError:
## Invalid token
return None
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
## Extract username and password from the request
username = data.get('username')
password = data.get('password')
## Check if the user exists and the password is correct
if username in users and users[username] == password:
## Generate a JWT token
token = generate_token(username)
return jsonify({
"status": "success",
"message": f"Welcome {username}!",
"token": token
})
else:
return jsonify({"status": "error", "message": "Invalid credentials"}), 401
@app.route('/protected', methods=['GET'])
def protected():
## Get the token from the Authorization header
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({"status": "error", "message": "Missing or invalid token"}), 401
## Extract the token
token = auth_header.split(' ')[1]
## Verify the token
payload = verify_token(token)
if payload:
return jsonify({
"status": "success",
"message": f"You are viewing protected content, {payload['username']}!"
})
else:
return jsonify({"status": "error", "message": "Invalid or expired token"}), 401
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
이 업데이트된 서버는 다음과 같습니다.
이제 JWT 토큰을 사용하도록 클라이언트를 업데이트해 보겠습니다. client.py 파일을 업데이트합니다.
code ~/project/auth_demo/client.py
기존 코드를 다음으로 바꿉니다.
import requests
import json
## Server URL
BASE_URL = "http://localhost:5000"
def login(username, password):
"""Authenticate with the server using username and password"""
response = requests.post(
f"{BASE_URL}/login",
json={"username": username, "password": password}
)
print(f"Login response: {response.status_code}")
data = response.json()
print(data)
## Return the token if login was successful
return data.get('token') if response.status_code == 200 else None
def access_protected(token=None):
"""Access a protected resource using JWT token"""
headers = {}
if token:
headers['Authorization'] = f'Bearer {token}'
response = requests.get(f"{BASE_URL}/protected", headers=headers)
print(f"Protected resource response: {response.status_code}")
print(response.json())
return response
if __name__ == "__main__":
## Test with valid credentials
print("=== Authenticating with valid credentials ===")
token = login("alice", "password123")
if token:
print("\n=== Accessing protected resource with valid token ===")
access_protected(token)
## Test with invalid credentials
print("\n=== Authenticating with invalid credentials ===")
login("alice", "wrongpassword")
## Try to access protected resource without token
print("\n=== Accessing protected resource without token ===")
access_protected()
## Try to access protected resource with invalid token
print("\n=== Accessing protected resource with invalid token ===")
access_protected("invalid.token.value")
이 업데이트된 클라이언트는 다음과 같습니다.
이제 업데이트된 토큰 기반 인증 시스템을 실행해 보겠습니다. 먼저, Ctrl+C를 사용하여 실행 중인 모든 서버 프로세스를 중지한 다음 업데이트된 서버를 시작합니다.
python ~/project/auth_demo/server.py
새 터미널 탭을 열거나 기존 두 번째 탭을 사용하여 클라이언트를 실행합니다.
cd ~/project/auth_demo
python client.py
다음과 같은 출력을 볼 수 있습니다.
이 출력은 토큰 기반 인증 시스템이 올바르게 작동하고 있음을 확인합니다.
토큰 기반 인증은 세션 기반 인증보다 몇 가지 장점을 제공합니다.
실제 애플리케이션에서는 토큰 갱신, 역할 기반 접근 제어 및 안전한 토큰 저장과 같은 기능으로 인증 시스템을 더욱 향상시킬 수 있습니다.
이전 구현에서는 비밀번호를 일반 텍스트로 저장했는데, 이는 심각한 보안 위험입니다. 이 단계에서는 bcrypt 알고리즘을 사용하여 비밀번호 해싱을 구현하여 인증 시스템을 강화합니다.
비밀번호를 일반 텍스트로 저장하면 다음과 같은 여러 가지 보안 위험이 발생합니다.
비밀번호 해싱은 다음을 통해 이러한 문제를 해결합니다.
먼저, bcrypt 패키지를 설치해야 합니다.
pip install bcrypt
설치를 확인하는 출력을 볼 수 있습니다.
Collecting bcrypt
Downloading bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl (593 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 593.8/593.8 KB 10.5 MB/s eta 0:00:00
Installing collected packages: bcrypt
Successfully installed bcrypt-4.0.1
비밀번호 해싱을 포함하는 사용자 등록을 포함하도록 서버를 강화해 보겠습니다. server.py 파일을 업데이트합니다.
code ~/project/auth_demo/server.py
기존 코드를 다음으로 바꿉니다.
from flask import Flask, request, jsonify
import jwt
import datetime
import os
import bcrypt
app = Flask(__name__)
## Secret key for signing JWT tokens
SECRET_KEY = os.urandom(24)
## Store users as a dictionary with username as key and a dict of hashed password and roles as value
users = {}
def hash_password(password):
"""Hash a password using bcrypt"""
## Convert password to bytes if it's a string
if isinstance(password, str):
password = password.encode('utf-8')
## Generate a salt and hash the password
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password, salt)
return hashed
def check_password(password, hashed):
"""Verify a password against its hash"""
## Convert password to bytes if it's a string
if isinstance(password, str):
password = password.encode('utf-8')
## Check if the password matches the hash
return bcrypt.checkpw(password, hashed)
def generate_token(username, role):
"""Generate a JWT token for the authenticated user"""
## Token expires after 30 minutes
expiration = datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
payload = {
'username': username,
'role': role,
'exp': expiration
}
## Create the JWT token
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
return token
def verify_token(token):
"""Verify the JWT token"""
try:
## Decode and verify the token
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
return payload
except jwt.ExpiredSignatureError:
## Token has expired
return None
except jwt.InvalidTokenError:
## Invalid token
return None
@app.route('/register', methods=['POST'])
def register():
data = request.get_json()
## Extract registration data
username = data.get('username')
password = data.get('password')
## Basic validation
if not username or not password:
return jsonify({"status": "error", "message": "Username and password are required"}), 400
## Check if the username already exists
if username in users:
return jsonify({"status": "error", "message": "Username already exists"}), 409
## Hash the password and store the user
hashed_password = hash_password(password)
users[username] = {
'password': hashed_password,
'role': 'user' ## Default role
}
return jsonify({
"status": "success",
"message": f"User {username} registered successfully"
})
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
## Extract username and password from the request
username = data.get('username')
password = data.get('password')
## Check if the user exists
if username not in users:
return jsonify({"status": "error", "message": "Invalid credentials"}), 401
## Check if the password is correct
if check_password(password, users[username]['password']):
## Generate a JWT token
token = generate_token(username, users[username]['role'])
return jsonify({
"status": "success",
"message": f"Welcome {username}!",
"token": token
})
else:
return jsonify({"status": "error", "message": "Invalid credentials"}), 401
@app.route('/protected', methods=['GET'])
def protected():
## Get the token from the Authorization header
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({"status": "error", "message": "Missing or invalid token"}), 401
## Extract the token
token = auth_header.split(' ')[1]
## Verify the token
payload = verify_token(token)
if payload:
return jsonify({
"status": "success",
"message": f"You are viewing protected content, {payload['username']}!",
"role": payload['role']
})
else:
return jsonify({"status": "error", "message": "Invalid or expired token"}), 401
@app.route('/admin', methods=['GET'])
def admin():
## Get the token from the Authorization header
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({"status": "error", "message": "Missing or invalid token"}), 401
## Extract the token
token = auth_header.split(' ')[1]
## Verify the token
payload = verify_token(token)
if not payload:
return jsonify({"status": "error", "message": "Invalid or expired token"}), 401
## Check if user has admin role
if payload['role'] != 'admin':
return jsonify({"status": "error", "message": "Admin access required"}), 403
return jsonify({
"status": "success",
"message": f"Welcome to the admin panel, {payload['username']}!"
})
## Add an admin user for testing
admin_password = hash_password("admin123")
users["admin"] = {
'password': admin_password,
'role': 'admin'
}
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
이 향상된 서버는 다음과 같습니다.
이제 이러한 새로운 기능을 테스트하기 위해 클라이언트를 업데이트해 보겠습니다. client.py 파일을 업데이트합니다.
code ~/project/auth_demo/client.py
기존 코드를 다음으로 바꿉니다.
import requests
import json
## Server URL
BASE_URL = "http://localhost:5000"
def register(username, password):
"""Register a new user"""
response = requests.post(
f"{BASE_URL}/register",
json={"username": username, "password": password}
)
print(f"Registration response: {response.status_code}")
print(response.json())
return response.status_code == 200
def login(username, password):
"""Authenticate with the server using username and password"""
response = requests.post(
f"{BASE_URL}/login",
json={"username": username, "password": password}
)
print(f"Login response: {response.status_code}")
data = response.json()
print(data)
## Return the token if login was successful
return data.get('token') if response.status_code == 200 else None
def access_protected(token=None):
"""Access a protected resource using JWT token"""
headers = {}
if token:
headers['Authorization'] = f'Bearer {token}'
response = requests.get(f"{BASE_URL}/protected", headers=headers)
print(f"Protected resource response: {response.status_code}")
print(response.json())
return response
def access_admin(token=None):
"""Access the admin panel using JWT token"""
headers = {}
if token:
headers['Authorization'] = f'Bearer {token}'
response = requests.get(f"{BASE_URL}/admin", headers=headers)
print(f"Admin panel response: {response.status_code}")
print(response.json())
return response
if __name__ == "__main__":
## Test user registration
print("=== Registering a new user ===")
register("testuser", "testpass123")
## Try to register with the same username (should fail)
print("\n=== Registering with existing username ===")
register("testuser", "differentpass")
## Test regular user login and access
print("\n=== Regular user login ===")
user_token = login("testuser", "testpass123")
if user_token:
print("\n=== Regular user accessing protected resource ===")
access_protected(user_token)
print("\n=== Regular user trying to access admin panel ===")
access_admin(user_token)
## Test admin login and access
print("\n=== Admin login ===")
admin_token = login("admin", "admin123")
if admin_token:
print("\n=== Admin accessing protected resource ===")
access_protected(admin_token)
print("\n=== Admin accessing admin panel ===")
access_admin(admin_token)
이 업데이트된 클라이언트는 다음과 같습니다.
이제 향상된 인증 시스템을 실행해 보겠습니다. 먼저, Ctrl+C를 사용하여 실행 중인 모든 서버 프로세스를 중지한 다음 업데이트된 서버를 시작합니다.
python ~/project/auth_demo/server.py
새 터미널 탭을 열거나 기존 두 번째 탭을 사용하여 클라이언트를 실행합니다.
cd ~/project/auth_demo
python client.py
다음과 같은 출력을 볼 수 있습니다.
이 출력은 비밀번호 해싱 및 역할 기반 접근 제어가 있는 향상된 인증 시스템이 올바르게 작동하고 있음을 확인합니다.
이제 향상된 시스템은 다음을 제공합니다.
이러한 향상으로 인증 시스템이 훨씬 더 강력해지고 실제 애플리케이션에 적합해집니다.
이 랩에서는 기본적인 개념부터 고급 기술까지, Python 클라이언트 - 서버 시스템에서 인증을 구현하는 방법을 배웠습니다. 다음을 성공적으로 수행했습니다.
이러한 기술은 안전한 Python 애플리케이션을 구축하기 위한 견고한 기반을 제공합니다. 인증은 애플리케이션 보안의 한 측면일 뿐이며, 프로덕션 시스템에서는 다음 사항도 고려해야 합니다.
이러한 원칙을 적용하여 사용자 데이터를 효과적으로 보호하고 안전한 운영을 유지하는 Python 애플리케이션을 구축할 수 있습니다.