09-4. SMTP 認證:從開放中繼到安全驗證

深入理解郵件認證的演進、為什麼早期不需要密碼、以及現代認證機制

🔐 SMTP 認證:從開放中繼到安全驗證

⏱️ 閱讀時間: 12 分鐘 🎯 難度: ⭐⭐⭐ (中等)


🏗️ SMTP 認證在網路模型中的位置

OSI 7 層模型

┌──────────────────────────────┬─────────────────────────┐
│ 7. Application Layer (應用層) │  SMTP + AUTH            │ ← 認證在這裡
├──────────────────────────────┼─────────────────────────┤
│ 6. Presentation Layer (表示層)│  Base64 編碼             │
├──────────────────────────────┼─────────────────────────┤
│ 5. Session Layer (會話層)     │  TLS (加密)             │
├──────────────────────────────┼─────────────────────────┤
│ 4. Transport Layer (傳輸層)   │  TCP                    │
├──────────────────────────────┼─────────────────────────┤
│ 3. Network Layer (網路層)     │  IP                     │
├──────────────────────────────┼─────────────────────────┤
│ 2. Data Link Layer (資料鏈結層)│  Ethernet               │
├──────────────────────────────┼─────────────────────────┤
│ 1. Physical Layer (實體層)    │  網路線、光纖            │
└──────────────────────────────┴─────────────────────────┘

重點:

  • SMTP 認證在應用層實現
  • 使用 Base64 編碼傳遞帳號密碼
  • 必須搭配 TLS/SSL 加密(否則密碼會被竊聽)

🤔 核心問題:為什麼有些情況不用密碼也能寄信?

生活化比喻

早期郵局(1980 年代的 SMTP):
┌──────────────────────────────┐
│ 任何人都可以投信到郵筒        │
│ 郵局不檢查身份               │
│ 只要寫了收件地址就寄出        │
└──────────────────────────────┘

就像:
你走到路邊的郵筒
投入一封信
不需要出示身份證
郵局就會幫你寄 ✅

問題:
❌ 有人濫用郵筒寄垃圾信
❌ 有人偽造寄件地址
❌ 郵局被當成免費的垃圾信發送工具

現代郵局(現在的 SMTP):
┌──────────────────────────────┐
│ 需要帳號密碼才能寄信          │
│ 驗證你的身份                 │
│ 防止濫用                     │
└──────────────────────────────┘

但仍有例外:
✅ 內部員工寄信(公司內網)
✅ 特定白名單 IP
✅ 本機測試環境

📜 歷史:SMTP 認證的演進

時間線 ⭐⭐⭐

1982 年 📧 SMTP 誕生 (RFC 821)
       └─ 完全沒有認證機制
       └─ 設計理念:「互信的學術網路」
       └─ 任何人都可以寄信給任何人

問題背景:
- 當時網際網路只有學術機構使用
- 大家互相信任
- 沒有商業利益
- 沒有垃圾郵件的概念

───────────────────────────────────────

1990 年代初 💥 垃圾郵件開始出現
       └─ 網際網路商業化
       └─ 行銷人員發現可以免費發廣告
       └─ 開放中繼(Open Relay)被濫用

濫用案例:
攻擊者找到開放中繼伺服器
     ↓
利用它發送數百萬封垃圾信
     ↓
被寄出的郵件顯示來自該伺服器
     ↓
該伺服器 IP 被列入黑名單
     ↓
正常用戶也無法寄信了 ❌

───────────────────────────────────────

1995 年 🔒 SMTP AUTH 提出 (RFC 2554)
       └─ 增加認證機制
       └─ 需要帳號密碼才能寄信

改變:
寄信前必須先 AUTH LOGIN
     ↓
提供正確的帳號密碼
     ↓
驗證通過才能發信 ✅

───────────────────────────────────────

1999 年 📮 多數 ISP 要求認證
       └─ 預設關閉開放中繼
       └─ Port 25 通常被封鎖
       └─ 改用 Port 587 (Submission)

───────────────────────────────────────

2010 年代 🔐 現代安全機制
       └─ 強制 TLS 加密
       └─ OAuth2 認證
       └─ 應用程式專用密碼
       └─ 雙因素驗證 (2FA)

───────────────────────────────────────

現在(2020+)🛡️ 多層防護
       ├─ SPF(IP 驗證)
       ├─ DKIM(簽章驗證)
       ├─ DMARC(政策執行)
       ├─ OAuth2(現代認證)
       └─ 應用程式密碼

🚪 開放中繼 (Open Relay) 的問題

什麼是開放中繼?

開放中繼 = 不需要認證就接受外部郵件轉發

傳統設計:
任何人 → SMTP 伺服器 → 任何人
         └─ 不驗證身份
         └─ 不檢查來源
         └─ 直接轉發

就像:
公共郵局不檢查身份
任何人都可以請郵局幫忙寄信
不管你是不是本地居民

被濫用的場景

案例 1:垃圾郵件發送者

垃圾郵件發送者 (Spammer)
     │
     ├─ 掃描網路尋找開放中繼
     │  (使用自動化工具)
     │
     ├─ 找到:mail.company.com (開放中繼)
     │
     ├─ 透過它發送 100 萬封垃圾信
     │  From: fake@example.com
     │  Subject: 快速致富!點這裡!
     │
     └─ 後果:
         ├─ mail.company.com 被列入黑名單
         ├─ 公司正常員工無法寄信
         ├─ 頻寬被吃光
         └─ 法律責任(幫助發送垃圾信)

案例 2:釣魚郵件

攻擊者
  │
  ├─ 利用開放中繼
  │
  ├─ 偽造高管郵件
  │  From: ceo@company.com
  │  Subject: 緊急!請立即匯款
  │
  └─ 員工收到後信以為真 ❌

案例 3:DDoS 放大攻擊

攻擊者
  │
  ├─ 找到 1000 個開放中繼
  │
  ├─ 向每個中繼發送郵件
  │  收件者:victim@target.com
  │
  └─ victim 收到 1000 倍流量
      → 郵件伺服器癱瘓 ❌

🔍 如何檢測開放中繼?

測試方法

使用 Telnet 測試:

# 連線到 SMTP 伺服器
telnet mail.example.com 25

# 如果連線成功,嘗試發送郵件
S: 220 mail.example.com SMTP Ready
C: HELO test.com
S: 250 mail.example.com

# 嘗試寄信給外部地址(不認證)
C: MAIL FROM:<spammer@evil.com>
S: 250 OK

C: RCPT TO:<victim@other-domain.com>

# 如果回應 250 OK = 開放中繼 ❌
S: 250 OK  ← 危險!

# 如果回應 550 = 有保護 ✅
S: 550 Relaying denied  ← 安全!

使用線上工具:

MXToolbox Open Relay Test:
https://mxtoolbox.com/diagnostic.aspx

功能:
✅ 自動測試開放中繼
✅ 檢查黑名單狀態
✅ DNS 記錄驗證

🔐 SMTP AUTH:認證機制

認證流程

完整認證會話:

客戶端                              伺服器
  │                                   │
  ├─ EHLO client.example.com ────────>│
  │<──────────────────────── 250-SIZE │
  │                          250-AUTH  │
  │                          PLAIN     │
  │                          LOGIN     │
  │                          CRAM-MD5  │
  │                                   │
  ├─ AUTH LOGIN ─────────────────────>│
  │<───────────────── 334 VXNlcm5hbWU6│
  │                    (Base64: Username:)
  │                                   │
  ├─ YWxpY2U= ───────────────────────>│
  │  (Base64: alice)                  │
  │<───────────────── 334 UGFzc3dvcmQ6│
  │                    (Base64: Password:)
  │                                   │
  ├─ c2VjcmV0MTIz ───────────────────>│
  │  (Base64: secret123)              │
  │                                   │
  │<─────────── 235 Authentication OK ─┤
  │                                   │
  ├─ MAIL FROM:<alice@example.com> ──>│
  │                                   │
  └─ 開始發送郵件...                   │

🔧 認證方式詳解

1. PLAIN(明文)

原理:

將帳號密碼用 Base64 編碼
一次發送:\0username\0password

範例:

# 編碼帳號密碼
echo -ne '\000alice\000secret123' | base64
# 輸出:AGFsaWNlAHNlY3JldDEyMw==

# SMTP 會話
C: AUTH PLAIN AGFsaWNlAHNlY3JldDEyMw==
S: 235 Authentication successful

Python 實作:

import smtplib
import base64

server = smtplib.SMTP('smtp.gmail.com', 587)
server.starttls()

# PLAIN 認證(自動處理)
server.login('alice@gmail.com', 'password')

# 或手動實作
auth_string = f'\x00alice@gmail.com\x00password'
encoded = base64.b64encode(auth_string.encode()).decode()
server.docmd('AUTH PLAIN', encoded)

安全性:

❌ Base64 不是加密!只是編碼
❌ 明文密碼容易被截取

解決方案:
✅ 必須搭配 TLS/SSL
✅ 使用 Port 587 (STARTTLS) 或 465 (SSL)

2. LOGIN(分步明文)

原理:

分兩步發送帳號和密碼
每個都用 Base64 編碼

範例:

C: AUTH LOGIN
S: 334 VXNlcm5hbWU6
    (Base64: Username:)

C: YWxpY2U=
    (Base64: alice)
S: 334 UGFzc3dvcmQ6
    (Base64: Password:)

C: c2VjcmV0MTIz
    (Base64: secret123)
S: 235 Authentication successful

Python 實作:

import smtplib
import base64

server = smtplib.SMTP('smtp.example.com', 587)
server.starttls()

# 手動 LOGIN 認證
server.docmd('AUTH LOGIN')
server.docmd(base64.b64encode(b'alice').decode())
server.docmd(base64.b64encode(b'secret123').decode())

安全性:

❌ 同樣是明文(只是分步)
✅ 必須搭配 TLS/SSL

3. CRAM-MD5(挑戰-回應)

原理:

伺服器發送隨機挑戰碼
客戶端用密碼和挑戰碼計算 MD5
只發送 MD5 雜湊,不發送密碼

流程:

C: AUTH CRAM-MD5
S: 334 PDEyMzQ1Njc4OTAuMTIzNDU2QGV4YW1wbGUuY29tPg==
    (Base64 編碼的隨機挑戰碼)

客戶端:
1. 解碼挑戰碼
2. 計算 HMAC-MD5(password, challenge)
3. 回應:username + space + hash

C: YWxpY2UgZjFkMmQ0ZjRlMjc5YWJjZjFhMmMzYjRkNWU2Zjc4OTBh
    (Base64: alice f1d2d4f4e279abcf1a2c3b4d5e6f7890a)

S: 235 Authentication successful

Python 實作:

import hmac
import hashlib
import base64

def cram_md5_response(username, password, challenge):
    """計算 CRAM-MD5 回應"""
    # 解碼挑戰碼
    challenge_bytes = base64.b64decode(challenge)

    # 計算 HMAC-MD5
    digest = hmac.new(
        password.encode(),
        challenge_bytes,
        hashlib.md5
    ).hexdigest()

    # 組合回應
    response = f'{username} {digest}'
    return base64.b64encode(response.encode()).decode()

# 使用
challenge = 'PDEyMzQ1Njc4OTAuMTIzNDU2QGV4YW1wbGUuY29tPg=='
response = cram_md5_response('alice', 'secret123', challenge)
print(response)

安全性:

✅ 密碼不在網路上傳輸
✅ 可以不用 TLS(但仍建議用)
⚠️ MD5 已被認為不安全
⚠️ 容易遭受重放攻擊

4. OAuth2(現代標準)⭐⭐⭐

原理:

使用 Access Token 代替密碼
Token 有時效性
權限可控制

流程:

1. 用戶授權
   ┌────────┐
   │ Gmail  │
   └───┬────┘
       │
   「允許此應用存取你的郵件?」
       │
   [允許] [拒絕]
       │
       ▼
   產生 Access Token

2. 使用 Token 認證
   C: AUTH XOAUTH2 dXNlcj1hbGljZUBnbWFpbC5jb20BYXV0aD1CZWFyZXIgeWEyOS...
   S: 235 Authentication successful

Token 內容:
- 有效期限(通常 1 小時)
- 權限範圍(只能寄信、不能讀信等)
- 可以撤銷

Python 實作(Gmail):

import smtplib
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from base64 import b64encode

# 1. 取得授權
SCOPES = ['https://mail.google.com/']
flow = InstalledAppFlow.from_client_secrets_file(
    'credentials.json', SCOPES
)
creds = flow.run_local_server(port=0)

# 2. 產生 OAuth2 字串
def generate_oauth2_string(username, access_token):
    auth_string = f'user={username}\x01auth=Bearer {access_token}\x01\x01'
    return b64encode(auth_string.encode()).decode()

# 3. 使用 OAuth2 認證
server = smtplib.SMTP('smtp.gmail.com', 587)
server.starttls()

auth_string = generate_oauth2_string(
    'alice@gmail.com',
    creds.token
)
server.docmd('AUTH XOAUTH2', auth_string)

# 4. 發送郵件
server.sendmail('alice@gmail.com', 'bob@example.com', msg)
server.quit()

優勢:

✅ 不需要儲存密碼
✅ Token 可撤銷
✅ 權限細粒度控制
✅ 支援多因素驗證
✅ 現代應用的標準做法

🆚 認證方式對比

完整對比表

認證方式安全性實作難度TLS 必要性現代使用
無認證❌ 極差簡單N/A❌ 已淘汰
PLAIN⚠️ 需 TLS簡單✅ 必須✅ 常用
LOGIN⚠️ 需 TLS簡單✅ 必須✅ 常用
CRAM-MD5⚠️ 中等中等⚠️ 建議⚠️ 較少
OAuth2✅ 最佳困難✅ 必須✅ 推薦

🔓 什麼情況下不需要認證?

1. 本機測試環境

場景:開發測試

設定:
smtp_host = 'localhost'
smtp_port = 1025

為什麼不需要認證?
✅ 只在本機運行
✅ 不對外開放
✅ 用於開發測試

Python 範例:
# 使用 Python 內建 SMTP 測試伺服器
python -m smtpd -n -c DebuggingServer localhost:1025

# 發送測試郵件(不需認證)
server = smtplib.SMTP('localhost', 1025)
server.sendmail(from_addr, to_addr, msg)
server.quit()

2. 公司內部網路

場景:企業內網

設定:
內網 SMTP 伺服器:mail.internal.company.com
只允許內網 IP:192.168.0.0/16

為什麼不需要認證?
✅ 只有公司員工能存取
✅ 基於 IP 白名單
✅ 防火牆保護

配置範例(Postfix):
# /etc/postfix/main.cf
mynetworks = 192.168.0.0/16, 127.0.0.0/8
smtpd_recipient_restrictions =
    permit_mynetworks,
    reject_unauth_destination

3. 伺服器間通訊(MTA to MTA)

場景:郵件伺服器互相轉發

流程:
Gmail Server → Yahoo Server
(Port 25, 無認證)

為什麼不需要認證?
✅ 這是郵件伺服器的正常職責
✅ 使用 SPF/DKIM/DMARC 驗證
✅ 基於 MX 記錄信任

範例:
Alice@gmail.com 寄信給 Bob@yahoo.com

1. Gmail 查詢 yahoo.com 的 MX 記錄
   → mta5.am0.yahoodns.net

2. Gmail 連到 Yahoo MX (Port 25)
   → 不需要認證
   → 直接 MAIL FROM / RCPT TO

3. Yahoo 檢查:
   ✅ SPF 記錄(IP 是否授權)
   ✅ DKIM 簽章(郵件完整性)
   ✅ DMARC 政策(如何處理)

4. 白名單 IP

場景:特定服務的固定 IP

設定:
允許 IP:203.0.113.10(公司官網伺服器)
用途:發送「重設密碼」、「訂單確認」等系統郵件

為什麼不需要認證?
✅ IP 固定且可信
✅ 防火牆限制
✅ 簡化自動化流程

配置範例(Postfix):
# /etc/postfix/main.cf
smtpd_recipient_restrictions =
    permit_sasl_authenticated,
    permit_mynetworks,
    check_client_access hash:/etc/postfix/client_whitelist,
    reject_unauth_destination

# /etc/postfix/client_whitelist
203.0.113.10    OK

5. 應用程式專用密碼

場景:舊版應用程式

問題:
應用程式不支援 OAuth2
但 Google 要求使用 2FA

解決方案:
生成「應用程式專用密碼」
├─ 16 字元隨機密碼
├─ 只用於該應用
└─ 可隨時撤銷

使用方式:
server = smtplib.SMTP('smtp.gmail.com', 587)
server.starttls()
server.login('alice@gmail.com', 'abcd efgh ijkl mnop')  # 應用程式密碼

⚠️ 不使用認證的風險

安全問題

1. 開放中繼 (Open Relay)
   └─ 被當成垃圾郵件發送工具
   └─ IP 被列入黑名單
   └─ 法律責任

2. 郵件偽造
   └─ 任何人可偽造 From 地址
   └─ 釣魚郵件
   └─ 社交工程攻擊

3. 資源濫用
   └─ 頻寬被吃光
   └─ 伺服器過載
   └─ 成本增加

4. 無法追蹤
   └─ 不知道誰發的郵件
   └─ 無法稽核
   └─ 出問題難以追查

🛡️ 現代最佳實踐

推薦設定

1. 客戶端提交(Port 587)

# 使用 Port 587 + STARTTLS + 認證
submission inet n       -       n       -       -       smtpd
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_sasl_type=dovecot
  -o smtpd_sasl_path=private/auth
  -o smtpd_reject_unlisted_recipient=no
  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject

2. 強制 TLS

# Python 範例
server = smtplib.SMTP('smtp.gmail.com', 587)
server.starttls()  # 強制升級為 TLS
server.login(username, password)

3. 使用 OAuth2

# 現代應用推薦
from google.oauth2.credentials import Credentials

creds = get_credentials()  # OAuth2 流程
server = smtplib.SMTP('smtp.gmail.com', 587)
server.starttls()
server.auth('XOAUTH2', lambda: generate_oauth2_string(creds))

4. 應用程式專用密碼

Gmail 設定:
1. 啟用 2FA
2. 生成應用程式密碼
3. 在應用程式中使用該密碼(非真實密碼)

5. IP 限制

# Postfix
smtpd_recipient_restrictions =
    permit_mynetworks,          # 只允許內網
    permit_sasl_authenticated,  # 或已認證
    reject_unauth_destination   # 其他拒絕

🎓 常見面試題

Q1:為什麼早期的 SMTP 不需要認證?

答案:

歷史背景:

1982 年 SMTP 設計時的環境:

1. 使用者少
   └─ 只有大學、研究機構
   └─ 幾千個用戶
   └─ 大家互相認識

2. 互信環境
   └─ 學術網路
   └─ 沒有商業利益
   └─ 不會有人惡意濫用

3. 技術限制
   └─ 頻寬昂貴
   └─ 認證增加複雜度
   └─ 簡單就是美

4. 沒有垃圾郵件
   └─ 1978 年第一封垃圾郵件
   └─ 但不常見
   └─ 沒有商業化垃圾郵件產業

設計理念:

類比:小村莊的信箱

早期網路 = 小村莊
├─ 大家互相認識
├─ 不會有人亂寄信
└─ 郵筒不上鎖 ✅

現代網路 = 大城市
├─ 陌生人很多
├─ 有人專門寄垃圾信
└─ 郵筒必須上鎖 🔒

演進過程:

1982-1990: 無認證
└─ 完全開放
└─ 任何人都能寄信

1990-1995: 垃圾郵件爆發
└─ 開放中繼被濫用
└─ 大量垃圾郵件

1995: SMTP AUTH 提出
└─ RFC 2554
└─ 加入認證機制

1999: 廣泛部署
└─ 多數 ISP 要求認證
└─ Port 25 被封鎖

2010+: 現代安全
└─ OAuth2
└─ 應用程式密碼
└─ 多因素驗證

記憶技巧:

早期 SMTP = 小村莊郵局
            └─ 不鎖門
            └─ 互相信任

現代 SMTP = 大城市郵局
            └─ 需要身份證
            └─ 嚴格管控

Q2:PLAIN 和 LOGIN 有什麼不同?

答案:

核心差異:

PLAIN: 一次發送
C: AUTH PLAIN AGFsaWNlAHNlY3JldDEyMw==
    └─ \0username\0password (Base64)

LOGIN: 分兩次發送
C: AUTH LOGIN
S: 334 VXNlcm5hbWU6
C: YWxpY2U=
    └─ username (Base64)
S: 334 UGFzc3dvcmQ6
C: c2VjcmV0MTIz
    └─ password (Base64)

詳細對比:

特性PLAINLOGIN
發送次數1 次2 次(互動式)
格式\0user\0pass分別發送
編碼Base64Base64
安全性相同(都是明文)相同(都是明文)
複雜度簡單稍複雜
支援度RFC 4616 標準非標準(但常用)
TLS 需求✅ 必須✅ 必須

實際會話對比:

PLAIN 方式:
─────────────────────────────
C: EHLO client.example.com
S: 250-smtp.example.com
S: 250 AUTH PLAIN LOGIN

C: AUTH PLAIN AGFsaWNlAHNlY3JldDEyMw==
S: 235 Authentication successful
─────────────────────────────
總共:2 個往返


LOGIN 方式:
─────────────────────────────
C: EHLO client.example.com
S: 250-smtp.example.com
S: 250 AUTH PLAIN LOGIN

C: AUTH LOGIN
S: 334 VXNlcm5hbWU6

C: YWxpY2U=
S: 334 UGFzc3dvcmQ6

C: c2VjcmV0MTIz
S: 235 Authentication successful
─────────────────────────────
總共:4 個往返

為什麼兩種都存在?

歷史原因:
- LOGIN 出現較早(Microsoft 實作)
- PLAIN 後來標準化(RFC)
- 為了相容性,兩種都保留

實務選擇:
✅ PLAIN:效率較高(1 次往返)
✅ LOGIN:舊系統相容性好

共同要求:
🔒 都必須在 TLS 加密下使用
   └─ 否則密碼會被竊聽!

Python 實作對比:

import smtplib
import base64

server = smtplib.SMTP('smtp.example.com', 587)
server.starttls()

# 方法 1: 使用 login()(自動選擇 PLAIN 或 LOGIN)
server.login('alice', 'secret123')

# 方法 2: 手動 PLAIN
auth_string = '\x00alice\x00secret123'
server.docmd('AUTH PLAIN', base64.b64encode(auth_string.encode()).decode())

# 方法 3: 手動 LOGIN
server.docmd('AUTH LOGIN')
server.docmd(base64.b64encode(b'alice').decode())
server.docmd(base64.b64encode(b'secret123').decode())

Q3:什麼時候應該使用 OAuth2 而不是密碼?

答案:

應該使用 OAuth2 的情況:

1. 第三方應用程式 ⭐⭐⭐

場景:你開發的 Email 客戶端

使用密碼:
User → 輸入 Gmail 密碼 → 你的 App → Gmail
                              ↑
                          你拿到密碼 ❌

問題:
❌ 你的 App 儲存了用戶的真實密碼
❌ 如果 App 被駭,密碼外洩
❌ 用戶必須完全信任你
❌ 用戶無法限制權限

使用 OAuth2:
User → 授權 → 你的 App 拿到 Token → Gmail
              ↑
          只有 Token,沒有密碼 ✅

優勢:
✅ 你的 App 不知道密碼
✅ Token 可以隨時撤銷
✅ 權限可控(只能寄信、不能刪信等)
✅ 支援 2FA

2. 企業應用 / SaaS 服務

場景:
- Slack 發送通知郵件
- Trello 寄送任務提醒
- GitHub 發送合併請求通知

為什麼用 OAuth2:
✅ 不需要儲存每個用戶的密碼
✅ 用戶可以隨時取消授權
✅ 符合安全合規要求
✅ 減少責任風險

範例(Slack):
Slack 請求權限:
「允許 Slack 以你的名義發送郵件?」
[允許] [拒絕]
         ↓
    產生 Token
         ↓
  Slack 使用 Token 發信

3. 多因素驗證 (2FA) 環境

問題場景:
User 啟用了 Gmail 2FA
     ↓
傳統密碼認證失敗
     ↓
應用程式無法登入 ❌

解決方案 A:應用程式專用密碼
- 生成 16 字元密碼
- 只用於該應用
- 繞過 2FA
⚠️ 仍是密碼,有風險

解決方案 B:OAuth2 ✅
- 在瀏覽器完成 2FA
- 產生 Token
- 應用程式使用 Token
- 安全且支援 2FA

4. 行動應用程式

問題:
行動 App 儲存密碼很危險
├─ 手機可能遺失
├─ App 可能被反編譯
└─ 密碼可能被提取

OAuth2 方案:
├─ Token 儲存在 Keychain/Keystore
├─ Token 有時效性(1 小時)
├─ 過期可用 Refresh Token 更新
└─ 遺失手機 → 撤銷 Token ✅

5. 微服務架構

場景:
Service A 需要代表用戶發送郵件

傳統方式:
User 密碼 → Service A → Service B → Gmail
                ↑
           儲存密碼 ❌

OAuth2 方式:
User 授權 → Token → Service A → Gmail
                      ↑
                 只有 Token ✅

優勢:
✅ 權限最小化
✅ 可審計
✅ 易於撤銷

不需要 OAuth2 的情況:

1. 個人腳本 / 自動化
   └─ 只有你自己用
   └─ 應用程式密碼即可

2. 內部系統
   └─ 企業內網
   └─ 不對外開放
   └─ PLAIN/LOGIN + TLS 即可

3. 簡單測試
   └─ 開發環境
   └─ 測試用途
   └─ OAuth2 設定太複雜

實作複雜度對比:

密碼認證:
server.login(user, password)  # 1 行

OAuth2:
1. 註冊應用程式
2. 設定 Redirect URI
3. 請求授權
4. 交換 Token
5. Refresh Token
6. 使用 Token 認證
# 需要 50+ 行程式碼

結論:
如果是第三方應用 → OAuth2 ✅
如果是自己的工具 → 密碼 OK

Q4:如何檢查 SMTP 伺服器支援哪些認證方式?

答案:

方法 1:使用 Telnet

# 連線到 SMTP 伺服器
telnet smtp.gmail.com 587

# 輸出
Trying 142.250.185.109...
Connected to smtp.gmail.com.
Escape character is '^]'.
220 smtp.gmail.com ESMTP

# 發送 EHLO
EHLO client.example.com

# 伺服器回應(查看 AUTH 行)
250-smtp.gmail.com at your service
250-SIZE 35882577
250-8BITMIME
250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER
250-ENHANCEDSTATUSCODES
250 SMTPUTF8

# 解讀
AUTH LOGIN PLAIN XOAUTH2
     │     │     └─ OAuth2 (推薦)
     │     └─ PLAIN 認證
     └─ LOGIN 認證

方法 2:使用 OpenSSL(加密連線)

# Port 465 (SSL)
openssl s_client -connect smtp.gmail.com:465 -crlf

# Port 587 (STARTTLS)
openssl s_client -connect smtp.gmail.com:587 -starttls smtp -crlf

# 然後發送 EHLO
EHLO test.com

# 查看 AUTH 行
250-AUTH LOGIN PLAIN XOAUTH2

方法 3:使用 Python

import smtplib

def check_auth_methods(host, port=587):
    """檢查 SMTP 伺服器支援的認證方式"""
    try:
        server = smtplib.SMTP(host, port)
        server.set_debuglevel(1)  # 顯示詳細資訊

        # 發送 EHLO
        server.ehlo()

        # 如果是 Port 587,啟用 TLS
        if port == 587:
            server.starttls()
            server.ehlo()  # TLS 後重新 EHLO

        # 檢查支援的認證方式
        if server.has_extn('AUTH'):
            auth_methods = server.esmtp_features['auth'].split()
            print(f'✅ 支援的認證方式: {", ".join(auth_methods)}')
            return auth_methods
        else:
            print('❌ 不支援認證')
            return []

    except Exception as e:
        print(f'❌ 錯誤: {e}')
        return []
    finally:
        try:
            server.quit()
        except:
            pass

# 測試
print('=== Gmail ===')
check_auth_methods('smtp.gmail.com', 587)

print('\n=== Outlook ===')
check_auth_methods('smtp-mail.outlook.com', 587)

print('\n=== Yahoo ===')
check_auth_methods('smtp.mail.yahoo.com', 587)

方法 4:使用命令列工具

# 使用 curl (7.66.0+)
curl smtp://smtp.gmail.com:587 -v --ssl-reqd

# 使用 swaks (Swiss Army Knife SMTP)
swaks --server smtp.gmail.com:587 --tls --quit-after EHLO

# 使用 msmtp
msmtp --host=smtp.gmail.com --port=587 --serverinfo

常見伺服器的認證方式:

Gmail (smtp.gmail.com:587)
├─ LOGIN
├─ PLAIN
├─ XOAUTH2 (推薦)
└─ OAUTHBEARER

Outlook (smtp-mail.outlook.com:587)
├─ LOGIN
├─ PLAIN
└─ XOAUTH2

Yahoo (smtp.mail.yahoo.com:587)
├─ LOGIN
└─ PLAIN

自架 Postfix (常見設定)
├─ PLAIN
├─ LOGIN
└─ CRAM-MD5

完整檢測腳本:

import smtplib
import socket

def comprehensive_check(host, ports=[25, 465, 587]):
    """完整檢查 SMTP 伺服器"""

    print(f'檢查主機: {host}\n')

    for port in ports:
        print(f'─── Port {port} ───')

        try:
            # 檢查連線
            sock = socket.create_connection((host, port), timeout=5)
            sock.close()
            print('✅ 可連線')

            # 檢查認證
            if port == 465:
                # SSL
                server = smtplib.SMTP_SSL(host, port, timeout=10)
            else:
                # 普通或 STARTTLS
                server = smtplib.SMTP(host, port, timeout=10)
                server.ehlo()

                if port == 587:
                    # 嘗試 STARTTLS
                    if server.has_extn('STARTTLS'):
                        print('✅ 支援 STARTTLS')
                        server.starttls()
                        server.ehlo()
                    else:
                        print('❌ 不支援 STARTTLS')

            # 檢查認證方式
            if server.has_extn('AUTH'):
                methods = server.esmtp_features['auth'].split()
                print(f'✅ 認證方式: {", ".join(methods)}')
            else:
                print('⚠️  不需要認證(可能是開放中繼!)')

            # 檢查其他功能
            if server.has_extn('SIZE'):
                max_size = server.esmtp_features.get('size', 'Unknown')
                print(f'📦 最大郵件: {max_size} bytes')

            server.quit()

        except socket.timeout:
            print('❌ 連線逾時')
        except socket.error as e:
            print(f'❌ 連線錯誤: {e}')
        except Exception as e:
            print(f'❌ 錯誤: {e}')

        print()

# 使用
comprehensive_check('smtp.gmail.com')

Q5:應用程式專用密碼和普通密碼有什麼不同?

答案:

核心差異:

普通密碼:
├─ 你的真實密碼
├─ 可以登入所有服務
├─ 權限完整
└─ 無法撤銷(除非改密碼)

應用程式專用密碼:
├─ 系統生成的隨機密碼
├─ 只用於特定應用程式
├─ 權限有限
└─ 可隨時撤銷

詳細對比:

特性普通密碼應用程式專用密碼
格式用戶自訂系統生成 (16 字元)
記憶需要記住不需記住(儲存)
用途登入所有服務只用於特定應用
撤銷改密碼影響全部只撤銷該應用
2FA需要輸入驗證碼繞過 2FA
安全性如果外洩影響大只影響該應用
數量1 個可有多個

生成與使用流程:

步驟 1:啟用 2FA
Gmail → 安全性 → 兩步驟驗證 → 啟用

步驟 2:生成應用程式密碼
Gmail → 安全性 → 應用程式密碼
     ↓
選擇應用類型:「郵件」
選擇裝置:「我的電腦」
     ↓
生成:abcd efgh ijkl mnop
     ↓
儲存這個密碼(只顯示一次)

步驟 3:在應用程式中使用
server = smtplib.SMTP('smtp.gmail.com', 587)
server.starttls()
server.login('alice@gmail.com', 'abcd efgh ijkl mnop')
                                  └─ 使用應用程式密碼

實際範例:

# 錯誤做法:使用真實密碼
server.login('alice@gmail.com', 'MyRealPassword123')
# ❌ 如果啟用 2FA 會失敗
# ❌ 密碼外洩影響所有服務

# 正確做法:使用應用程式密碼
server.login('alice@gmail.com', 'abcd efgh ijkl mnop')
# ✅ 繞過 2FA
# ✅ 外洩只影響這個應用
# ✅ 可隨時撤銷

管理多個應用程式密碼:

Gmail 帳戶
├─ 應用程式密碼 1: 「工作電腦 Outlook」
├─ 應用程式密碼 2: 「家裡電腦 Thunderbird」
├─ 應用程式密碼 3: 「Python 腳本」
└─ 應用程式密碼 4: 「手機郵件 App」

好處:
✅ 工作電腦遺失 → 只撤銷密碼 1
✅ Python 腳本外洩 → 只撤銷密碼 3
✅ 不影響其他應用程式

撤銷流程:

發現密碼外洩:
1. 進入 Gmail → 安全性 → 應用程式密碼
2. 找到該密碼(例如「Python 腳本」)
3. 點擊「撤銷」
4. 該應用程式立即無法登入
5. 其他應用程式不受影響 ✅

如果是真實密碼外洩:
1. 需要更改密碼
2. 所有登入的裝置都會登出
3. 需要重新登入所有服務 ❌

為什麼需要應用程式密碼?

問題場景:
你啟用了 Gmail 2FA
    ↓
舊版郵件客戶端(Outlook 2010)
    ↓
不支援 OAuth2
    ↓
無法完成 2FA 驗證
    ↓
無法登入 ❌

解決方案:
使用應用程式專用密碼
    ↓
繞過 2FA(但仍然安全)
    ↓
舊版客戶端可以登入 ✅

安全性分析:

應用程式密碼是否降低安全性?

表面上:
⚠️ 繞過 2FA
⚠️ 不需要驗證碼

實際上:
✅ 密碼由系統生成(強度高)
✅ 可隨時撤銷
✅ 權限有限
✅ 可審計(知道哪個應用在用)
✅ 比真實密碼更安全

風險控制:
如果應用程式密碼外洩
→ 只影響該應用
→ 撤銷該密碼即可
→ 不需要改真實密碼

如果真實密碼外洩
→ 影響所有服務
→ 需要改密碼
→ 所有裝置重新登入

最佳實踐:

# 1. 使用環境變數儲存
import os
from dotenv import load_dotenv

load_dotenv()
APP_PASSWORD = os.getenv('GMAIL_APP_PASSWORD')

server.login('alice@gmail.com', APP_PASSWORD)

# 2. 定期輪換
# 每 3-6 個月撤銷舊密碼,生成新密碼

# 3. 明確命名
# 不要用「密碼1」、「密碼2」
# 使用「工作電腦 Outlook」、「自動化腳本」

# 4. 記錄使用場景
# 在密碼管理器中記錄:
# 密碼: abcd efgh ijkl mnop
# 用途: Jenkins CI/CD 發送通知郵件
# 日期: 2025-01-16

📝 總結

SMTP 認證演進要點:

歷史脈絡:

1982: 無認證(互信時代)
1990s: 垃圾郵件爆發
1995: SMTP AUTH 誕生
1999: 廣泛部署
2010+: OAuth2、2FA

認證方式:

無認證    → 已淘汰 ❌
PLAIN     → 需 TLS ⚠️
LOGIN     → 需 TLS ⚠️
CRAM-MD5  → 較少用 ⚠️
OAuth2    → 推薦 ✅

不需要認證的情況:

✅ 本機測試
✅ 企業內網(IP 白名單)
✅ 伺服器間通訊(MTA to MTA)
✅ 特定場景(白名單 IP)

現代最佳實踐:

1. 使用 Port 587 + STARTTLS
2. 強制認證
3. OAuth2(第三方應用)
4. 應用程式密碼(舊版應用)
5. 定期審計

記憶口訣:

早期 = 小村莊(互信)
現代 = 大城市(需認證)

無認證 = 危險
PLAIN/LOGIN + TLS = 可用
OAuth2 = 最佳

🔗 延伸閱讀

  • 上一篇:09-3. IMAP 協定
  • 下一篇:10-1. 訊息協定
  • RFC 2554 (SMTP AUTH):https://datatracker.ietf.org/doc/html/rfc2554
  • RFC 4616 (PLAIN):https://datatracker.ietf.org/doc/html/rfc4616
  • RFC 4954 (SMTP AUTH 更新):https://datatracker.ietf.org/doc/html/rfc4954
  • Gmail SMTP 設定:https://support.google.com/mail/answer/7126229
  • OAuth2 for Gmail:https://developers.google.com/gmail/imap/xoauth2-protocol
0%