今天的學習目標是使用 HTTPS 保護醫療資料在傳輸過程中的安全性,了解 SSL/TLS 的基本概念,並用 Python 模擬加密傳輸測試。
一、理論重點
HTTPS(HyperText Transfer Protocol Secure)
在 HTTP 上加上一層 SSL/TLS 加密,確保資料在傳輸過程中不被竊聽或篡改。
SSL/TLS 憑證
用來驗證伺服器身份,確保用戶端與真正的伺服器通信,而非中間人攻擊。
自簽名憑證(Self-signed Certificate)
開發或測試環境常用,但不被瀏覽器信任;正式環境需使用 CA 簽發的憑證。
Python HTTPS 測試
可以用 requests 或 httpx 搭配 SSL 憑證進行安全連線測試。
二、案例分享
2021 年,一家醫院推出線上病歷查詢系統,讓病人可以透過網頁查詢自己的檢驗結果與病歷紀錄。系統剛上線時,傳輸仍使用 HTTP,所有病人資料都是明文傳送,沒有任何加密保護。資安團隊在例行檢測時發現,如果有人在同一個公共網路環境中截取封包,可能取得病人的姓名、身分證號、檢驗結果等敏感資訊,存在高度資安風險。
為了防止資料外洩,醫院立刻啟用 HTTPS,並在伺服器安裝 SSL/TLS 憑證,確保病人的每次請求與伺服器的通訊都經過加密。之後,病人再透過網頁登入系統,所有病歷資料在傳輸過程中都被加密,即使被攔截,也無法解讀內容。此舉成功避免了敏感資訊外洩,並提升病人的使用信任度,同時也符合 HIPAA 等醫療資安規範。
三、簡單程式示範
python
# generate_cert.py
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import rsa
import datetime
# 生成私鑰
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
with open("key.pem", "wb") as f:
f.write(key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
))
# 生成自簽名憑證
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "TW"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Taipei"),
x509.NameAttribute(NameOID.LOCALITY_NAME, "Taipei"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Test Org"),
x509.NameAttribute(NameOID.COMMON_NAME, "localhost"),
])
cert = x509.CertificateBuilder().subject_name(subject)\
.issuer_name(issuer)\
.public_key(key.public_key())\
.serial_number(x509.random_serial_number())\
.not_valid_before(datetime.datetime.utcnow())\
.not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=365))\
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)\
.sign(key, hashes.SHA256())
with open("cert.pem", "wb") as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))
print("key.pem 和 cert.pem 已生成完成!")
server.py
from fastapi import FastAPI, Request, HTTPException
import mysql.connector
app = FastAPI()
# 簡單 Token
TOKENS = {
"patient123": "patient",
"doctor123": "doctor"
}
# MySQL 連線設定
DB_CONFIG = {
"host": "localhost",
"user": "root",
"password": "Joy940819",
"database": "health"
}
# 初始化資料表
def init_db():
conn = mysql.connector.connect(**DB_CONFIG)
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS blood_pressure (
id INT AUTO_INCREMENT PRIMARY KEY,
systolic INT,
diastolic INT,
pulse INT
)
""")
conn.commit()
conn.close()
init_db()
# 病人上傳 API
@app.post("/upload")
async def upload(request: Request):
token = request.headers.get("Authorization")
if TOKENS.get(token) != "patient":
raise HTTPException(status_code=401, detail="Unauthorized")
data = await request.json()
conn = mysql.connector.connect(**DB_CONFIG)
cur = conn.cursor()
cur.execute("INSERT INTO blood_pressure (systolic, diastolic, pulse) VALUES (%s, %s, %s)",
(data["systolic"], data["diastolic"], data["pulse"]))
conn.commit()
conn.close()
return {"status": "success", "data": data}
# 醫師查詢 API
@app.get("/records")
def records(request: Request):
token = request.headers.get("Authorization")
if TOKENS.get(token) != "doctor":
raise HTTPException(status_code=401, detail="Unauthorized")
conn = mysql.connector.connect(**DB_CONFIG)
cur = conn.cursor()
cur.execute("SELECT * FROM blood_pressure ORDER BY id DESC LIMIT 5")
rows = cur.fetchall()
conn.close()
return {"records": rows}
client.py
import requests
url = "https://127.0.0.1:8443/patient_data"
data = {
"name": "Alice",
"age": 30,
"blood_pressure": "120/80"
}
# 用 verify=False 忽略自簽名憑證警告
headers = {"Authorization": "patient123"}
response = requests.post(url, json=data, headers=headers, verify=False)
print(response.status_code, response.json())
先用 Python 快速生成憑證,再來啟動HTTPS
uvicorn server:app --host 127.0.0.1 --port 8443 --reload --ssl-keyfile key.pem --ssl-certfile cert.pem
這時FastAPI 伺服器已經順利啟動,並且使用 HTTPS(8443 埠) 運行中。
假設我是病人上傳資料(POST)
import requests
url = "https://127.0.0.1:8443/upload"
headers = {"Authorization": "patient123"}
data = {"systolic": 120, "diastolic": 80, "pulse": 70}
response = requests.post(url, json=data, headers=headers, verify=False)
print(response.status_code, response.json())
而醫師查詢我(病人)所上傳的資料
url = "https://127.0.0.1:8443/records"
headers = {"Authorization": "doctor123"}
response = requests.get(url, headers=headers, verify=False)
print(response.status_code, response.json())
執行結果如下