應用程式會需要發送 email 是一個非常普遍的需求,而 Gmail 又是現在最常用的 email 服務,所以很合理地會想要用 Gmail 配合 SMTP 來發送 email。不過 Gmail 在安全性方面倒是有一些比較嚴格的要求,使得要規規矩矩地串接並不是那麼簡單的一回事。最近我經過一番研究之後總算找出了可以串成功的方法,底下來分享一下。
內容目錄
走後門:允許低安全性應用程式
絕大多數講 Gmail 串接的文章或答案都會叫人到 https://myaccount.google.com 的安全性裡面去把「允許低安全性應用程式」的選項打開。這固然是一個馬上就能開通的作法,但從名字就聽得出來這樣做一定有它的問題。我簡單解釋一下是怎麼回事。
所謂「允許地安全性應用程式」的意思是說,任何應用程式只要透過 Google 使用者帳號跟密碼、就可以存取包括 SMTP 在內的各種資源。但是,這樣做等於是一口氣把整個 Google 帳戶的一切控制權都交給了這個應用程式,使得該應用程式除了 SMTP 之外、其它所有的資源也都能使用,例如支付交易等等,這是會有很多潛在危險的(例如若負責專案的 email 部份的部門中出了不肖的工程師,那就會被濫用)。Google 希望開發者做的事情是,不要把這麼大的權限交給串接的應用程式,而要在一個能夠被控管的情況下讓應用程式存取指定的資源(且必要的時候,隨時可以主動撤銷其存取權)。因此,Google 目前的政策是,即便我們把「允許低安全性應用程式」功能開啟了,只要隔一陣子沒使用(具體時間我不清楚),Google 也還是會自動把它關掉,使得下次要寄 email 又一樣會被擋掉。
設定步驟
但是要講正確的串法,那就很複雜了。準備的步驟大致如下(要是我未來還有機會再走一遍的話,細節再補充吧):
- 到 https://console.developers.google.com/ 去建立一個 Google 應用程式。
- 在建立的應用程式裡面啟用 Gmail API。
- 在 API 管理的「憑證」頁面當中,建立一個新的服務帳號。這個服務帳號會是一個
*.iam.gserviceaccount.com
網域下的 email 位置,此外也會有一個數字組成的 ID。 - 進入這個憑證的管理頁面,選擇「金鑰」並且建立一組新的金鑰。格式選擇 JSON 或 p12 格式都可以(底下兩種的用法都會介紹)。產生之後會自動下載檔案,每一組金鑰只有一次下載機會,所以下載之後要保管好(否則必須重新產生)。
- (這一步驟是有使用 Google Workspace 1 才需要)到 https://admin.google.com/ 去,在選單的「安全性→存取權與資料控管→API 控制項」裡面,點選下面的「管理全網域委派設定」,新增一個新的委派,ID 就是步驟 3 中的數字 ID,然後範圍設定為
https://mail.google.com
。如此一來,Workspace 網域底下的 email 位置就可以用該服務帳號來發送郵件。
如果沒有使用 Google Workspace 的話,則我猜應該是要在憑證的管理頁面中把要發送信件的 email 位置新增成開憑證的使用者,但是這我就沒有測試過了。
之所以要搞這麼多設定步驟,就是為了要讓應用程式僅使用「權限有限、且隨時可以被撤銷」的金鑰來存取 Gmail,以減少各種潛在的安全性問題。
用金鑰產生憑據
再來是程式碼的部份。我使用 MailKit 套件來處理 email 有關的東西,除此之外也會需要安裝 Google.Apis.Auth 套件。
首先我們需要使用金鑰來產生憑據(credential)。如果稍早選擇的是 JSON 格式的金鑰的話比較簡單,該 JSON 檔案裡面裡面會有一個欄位是 private_key
,看我們是要直接把該金鑰寫死在程式碼裡面還是用什麼方法把它讀到程式碼中都可以,然後用如下的方式產生憑據:
var account = "..."; // 服務帳號的 email 位置
var address = "..."; // 送出的 email 位置
var privateKey = "..."; // 私鑰字串
var credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(account) {
Scopes = new[] { "https://mail.google.com" },
User = address
}.FromPrivateKey(privateKey)
);
而如果是用 p12 格式,一個方法是把剛才下載到的 p12 檔案放在 .NET 專案之中、並且把其屬性設定為「永遠輸出」到建置目錄中,然後用如下的方法讀進來:
var certificate = new X509Certificate2("xxxx.p12",
"notasecret", // Google 產生的預設金鑰密碼應該會是這個
X509KeyStorageFlags.Exportable);
其中第一個參數是 p12 的檔名。或者,第一個參數也可以改成傳入金鑰檔案的 byte[]
陣列,使得我們可以自己用別的方法把金鑰檔讀取進來。讀進來了之後,產生憑據的方法只是差在最後一行而已:
var credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(account) {
Scopes = new[] { "https://mail.google.com" },
User = address
}.FromCertificate(certificate) // 使用憑證
);
取得權杖並認證
有了憑據之後就可以來拿權杖並且通過認證了:
if (!await credential.RequestAccessTokenAsync(CancellationToken.None)) {
throw new InvalidOperationException("請求權杖失敗");
}
var oauth = new SaslMechanismOAuth2(Address, credential.Token.AccessToken);
using var client = new SmtpClient();
await client.ConnectAsync("smtp-relay.gmail.com", 587, SecureSocketOptions.StartTls);
await client.AuthenticateAsync(oauth);
連線成功之後就可以正常地用 MailKit 的方法去發送信件了。收工。
順便一提,其實這邊介紹的作法雖然免於開啟「允許低安全性應用程式」,但是仍舊不是 Google 最建議的作法,因為金鑰的保管等等的考量仍舊有其潛在的安全性風險。最安全的方法是利用所謂的 workload identity federation 來串,但這就又更複雜了,所以就先不多介紹了。
-
以前叫 G Suite。 ↩
留言