Learning Objectives
- 說明密碼為什麼要雜湊後才存,以及雜湊和加密的差別
- 用 generate_password_hash、check_password_hash 寫出註冊與登入
- 說明登入狀態怎麼放進 session,以及登入成功時為什麼要先清空 session
- 用裝飾器寫出 login_required,保護需要登入的頁面
登入要解決什麼問題
上一課看到 session 可以記住「這個瀏覽器是誰」。這一課補上前面那一段:怎麼確認對方真的是他自稱的人。
做法分成兩半:註冊時把帳號和密碼存起來,登入時比對。問題在於密碼不能照原樣存。資料庫外洩的事件時常發生,如果存的是明文,攻擊者立刻拿到所有人的密碼,而且很多人會在不同網站用同一組密碼。
所以資料庫裡存的是雜湊值(hash):一個只能從密碼算出來、不能倒回去的字串。
逐步看:雜湊怎麼比對
同一個密碼雜湊兩次,請比較兩次的結果,再看 check_password_hash 怎麼判斷。
雜湊、加密與 salt
| 雜湊 | 加密 | |
|---|---|---|
| 能不能還原 | 不能 | 有金鑰就能 |
| 適合 | 密碼:只需要比對,不需要知道原文 | 之後要讀回來的資料 |
存密碼不要用 MD5、SHA-256 這類快速雜湊。它們設計成算得很快,攻擊者每秒可以試非常多次。scrypt、Argon2、bcrypt 這類演算法刻意算得慢、佔記憶體,對正常登入影響不大,對大量猜測的成本高很多。詳細建議見 OWASP Password Storage Cheat Sheet。
generate_password_hash 已經幫你處理 salt 與演算法。不要自己設計密碼的儲存方式。
逐步看:一次登入走過哪些程式
程式用 test_client 依序送出:還沒登入就開帳號頁、註冊、打錯密碼、登入、再開帳號頁。注意呼叫堆疊什麼時候進到 wrapped,又在哪裡被擋下來。
為什麼 login_required 要寫成裝飾器
需要登入的頁面可能有幾十個。每個 view 函數開頭都寫一次檢查,總有一次會漏掉,漏掉就是資安問題。裝飾器把這段檢查寫在一個地方:
@app.route('/account')
@login_required # 順序:路由在最上面,login_required 在下面
def account():
...
順序不能反過來。裝飾器由下往上套用,@app.route 登記的是它拿到的函數;如果 @login_required 寫在 @app.route 上面,登記進路由的會是沒有檢查的原函數,頁面就沒有被保護。
另外,登出用 POST,不用 GET:GET 應該只讀資料、不改狀態。如果 /logout 接受 GET,別的網站只要放一個連結,你一點就被登出了。這類問題第 17 課再談。
動手做:保護兩個頁面
完成 login_required,讓還沒登入的人被轉到 /login,登入後才能看到內容。程式最下面的測試不要改。
想一想
資料庫外洩後,為什麼存 scrypt 雜湊值比存 SHA-256 雜湊值好?
登入失敗時,下面哪個訊息比較適當?
下面的寫法有什麼問題?
@login_required
@app.route('/account')
def account(): ...
Flask 的 session 預設存在哪裡?
重點整理
- 密碼只存雜湊值,用
generate_password_hash、check_password_hash,不要自己設計 - 雜湊不能還原;salt 讓相同的密碼得到不同的結果;存密碼要用刻意變慢的演算法
- 登入失敗的訊息不要透露是信箱還是密碼錯
- 登入成功時先
session.clear()再寫入使用者編號;session 不放機密 - 用
login_required裝飾器集中檢查,@app.route寫在最上面
這一課只解決「你是誰」。下一課處理「你可以做什麼」:登入的人可以看別人的筆記嗎?