はじめに
セキュアな認証の実装は、堅牢な 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 を提供します。
基本的な認証フローを視覚化してみましょう。
クライアント サーバー
| |
|--- ログインリクエスト (資格情報) ---->|
| |--- 資格情報の検証
| |
|<---- 認証レスポンス -------|
| |
|--- その後のリクエスト (認証付き) >|
| |--- 認証の検証
| |
|<---- 保護されたリソースレスポンス ---|
基本的な内容を理解し、環境を設定したので、プロジェクトのシンプルなファイル構造を作成しましょう。
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
新しいターミナルタブを開くか、既存の 2 番目のタブを使用してクライアントを実行します。
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
新しいターミナルタブを開くか、既存の 2 番目のタブを使用してクライアントを実行します。
cd ~/project/auth_demo
python client.py
次のような出力が表示されるはずです。
この出力は、パスワードハッシュとロールベースのアクセス制御を備えた強化された認証システムが正しく機能していることを確認します。
強化されたシステムは現在、以下を提供します。
これらの強化により、認証システムは、より堅牢になり、実際のアプリケーションに適したものになります。
この実験では、基本的な概念から高度なテクニックまで、Python クライアントサーバーシステムでの認証の実装方法を学びました。以下のことに成功しました。
これらのスキルは、安全な Python アプリケーションを構築するための強固な基盤を提供します。認証はアプリケーションセキュリティのほんの一部であることを覚えておいてください。本番システムでは、次のことも考慮する必要があります。
これらの原則を適用することにより、ユーザーデータを効果的に保護し、安全な運用を維持する Python アプリケーションを構築できます。