這很重要
從 2025 年 7 月 28 日開始,App Service 受控憑證 (ASMC) 的變更將影響特定案例中憑證的發行和更新方式。 雖然大多數客戶不需要採取行動,但我們建議您檢閱我們的 ASMC 詳細部落格文章 以取得更多資訊。
您可以啟用應用程式的各種類型的驗證,以限制對 Azure App Service 應用程式的存取。 設定驗證的其中一種方式,是使用傳輸層安全性 (TLS) / 安全套接字層 (SSL) 傳送用戶端要求時要求客戶端憑證,並驗證憑證。 此機制稱為 相互驗證 或 客戶端憑證驗證。 本文說明如何設定應用程式,以使用用戶端憑證驗證。
附註
您的應用程式程式代碼必須驗證客戶端憑證。 除了將客戶端憑證轉送至您的應用程式以外,App Service 不會對客戶端憑證執行任何動作。
如果您透過 HTTP 存取您的網站,而不是 HTTPS,則不會收到任何用戶端憑證。 如果您的應用程式需要用戶端憑證,您不應該允許透過 HTTP 對應用程式提出要求。
準備您的 Web 應用程式
如果您想要建立自定義 TLS/SSL 系結,或啟用 App Service 應用程式的用戶端憑證, App Service 方案 必須位於基本、標準、進階或隔離層中。
若要確定您的 Web 應用程式位於支援的定價層:
移至您的 Web 應用程式
在 Azure 入口網站 搜尋方塊中,輸入 App Services ,然後在搜尋結果中加以選取。
在 [ 應用程式服務] 頁面上,選取您的 Web 應用程式:
您現在位於 Web 應用程式的管理頁面上。
檢查定價層
在 Web 應用程式左側功能表的 [設定] 中,選取 [擴大 (App Service 方案)]。
請確定您的 Web 應用程式不在 F1 或 D1 層中。 這些層不支援自定義 TLS/SSL。
如果您需要擴大,請遵循下一節中的步驟來進行。 否則,請關閉擴展窗格,然後略過下一節。
擴大您的 App Service 方案
選取任何非免費層,例如 B1、B2、B3 或生產類別中的任何一層。
完成時,請選擇 [選取]。
當擴展操作完成時,您會看到訊息指出計劃已更新。
啟用用戶端憑證
當您為應用程式啟用用戶端憑證時,您應該選取您選擇的客戶端憑證模式。 此模式會定義您的應用程式如何處理傳入客戶端憑證。 下表說明這些模式:
| 用戶端憑證模式 | 描述 |
|---|---|
| 必要 | 所有要求都需要用戶端憑證。 |
| 選用 | 要求可以使用客戶端憑證。 根據預設,系統會提示用戶端輸入憑證。 例如,瀏覽器用戶端會顯示提示以選取憑證進行驗證。 |
| 選擇性的互動使用者 | 要求可以使用客戶端憑證。 客戶端預設不會提示您輸入憑證。 例如,瀏覽器用戶端不會顯示提示以選取憑證進行驗證。 |
若要使用 Azure 入口網站來啟用用戶端憑證:
- 移至您的應用程式管理頁面。
- 在左側功能表中,選取 [ 組態>一般設定]。
- 針對 [用戶端憑證模式],選取您的選擇。
- 選取 [儲存]。
排除路徑以要求驗證
當您為應用程式啟用相互驗證時,應用程式根目錄下的所有路徑都需要用戶端憑證進行存取。 若要讓特定路徑免除此需求,請將排除路徑定義為應用程式組態的一部分。
附註
使用任何用戶端憑證排除路徑會觸發對應用程式傳入要求的 TLS 重新交涉。
在應用程式管理頁面的左側功能表中,選取 [ 設定>組態]。 選取一般設定索引標籤。
在 [憑證排除路徑] 旁,選取鉛筆圖示。
選取 [新增路徑]、指定路徑或以
,或;分隔的路徑清單,然後選取 [ 確定]。選取 [儲存]。
下列螢幕快照顯示如何設定憑證排除路徑。 在此範例中,以 開頭 /public 之應用程式的任何路徑都不會要求客戶端憑證。 路徑比對不區分大小寫。
用戶端憑證和 TLS 重新交涉
針對某些用戶端憑證設定,App Service 需要 TLS 重新交涉讀取要求後,才能知道是否提示提供用戶端憑證。 下列兩個設定都會觸發 TLS 重新談判:
- 使用 選擇性的互動式使用者 用戶端憑證模式。
- 使用 客戶端憑證排除路徑。
附註
TLS 1.3 和 HTTP 2.0 不支援 TLS 重新交涉。 如果您的應用程式設定了使用 TLS 重新談判的用戶端憑證設定,這些通訊協定將無法運作。
若要停用 TLS 重新談判,並在 TLS 交握期間讓應用程式交涉用戶端憑證,您必須在應用程式中採取下列動作:
- 將客戶端憑證模式設定為 [必要 ] 或 [選擇性]。
- 拿掉所有客戶端憑證排除路徑。
使用 TLS 重新談判上傳大型檔案
使用 TLS 重新談判的用戶端憑證組態不支援具有大於 100 KB 的檔案的連入要求。 此限制是由緩衝區大小限制所造成。 在此案例中,超過 100 KB 的任何 POST 或 PUT 要求都失敗,並出現 403 錯誤。 此限制無法設定且無法增加。
若要解決 100 KB 的限制,請考慮下列解決方案:
- 停用 TLS 重新協商。 在應用程式的用戶端憑證設定中採取下列動作:
- 將客戶端憑證模式設定為 [必要 ] 或 [選擇性]。
- 拿掉所有客戶端憑證排除路徑。
- 在 PUT/POST 要求之前傳送 HEAD 要求。 HEAD 請求會處理此用戶端憑證。
- 將
Expect: 100-Continue標頭新增至您的要求。 此標頭會導致用戶端在傳送要求本文前,等待伺服器回應100 Continue,並且會略過緩衝區。
存取客戶端憑證
在 App Service 中,請求的 TLS 終止發生於前端負載平衡器。 App Service 將要求轉送至您的已啟用用戶端憑證應用程式程式碼時,會插入具有該用戶端憑證的 X-ARR-ClientCert 要求標頭。 除了將客戶端憑證轉送至您的應用程式以外,App Service 不會對此客戶端憑證執行任何動作。 您的應用程式程式代碼必須驗證客戶端憑證。
在 ASP.NET 中,用戶端憑證可透過 HttpRequest.ClientCertificate 屬性取得。
在其他應用程式堆疊中(Node.js,PHP),用戶端憑證可透過要求標頭中的 X-ARR-ClientCert Base64編碼值來取得。
ASP.NET Core 範例
針對 ASP.NET Core,中間件可用來剖析轉寄的憑證。 個別的中間件可用於使用轉送的通訊協議標頭。 兩者皆須存在,才能接受轉送的憑證。 您可以在 CertificateAuthentication 選項中放置自訂憑證驗證邏輯:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
// Configure the application to use the protocol and client IP address forwarded by the front-end load balancer.
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
// By default, only loopback proxies are allowed. Clear that restriction to enable this explicit configuration.
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
// Configure the application to use the client certificate forwarded by the front-end load balancer.
services.AddCertificateForwarding(options => { options.CertificateHeader = "X-ARR-ClientCert"; });
// Add certificate authentication so that when authorization is performed the user will be created from the certificate.
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseForwardedHeaders();
app.UseCertificateForwarding();
app.UseHttpsRedirection();
app.UseAuthentication()
app.UseAuthorization();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
ASP.NET Web Forms 範例
using System;
using System.Collections.Specialized;
using System.Security.Cryptography.X509Certificates;
using System.Web;
namespace ClientCertificateUsageSample
{
public partial class Cert : System.Web.UI.Page
{
public string certHeader = "";
public string errorString = "";
private X509Certificate2 certificate = null;
public string certThumbprint = "";
public string certSubject = "";
public string certIssuer = "";
public string certSignatureAlg = "";
public string certIssueDate = "";
public string certExpiryDate = "";
public bool isValidCert = false;
//
// Read the certificate from the header into an X509Certificate2 object.
// Display properties of the certificate on the page.
//
protected void Page_Load(object sender, EventArgs e)
{
NameValueCollection headers = base.Request.Headers;
certHeader = headers["X-ARR-ClientCert"];
if (!String.IsNullOrEmpty(certHeader))
{
try
{
byte[] clientCertBytes = Convert.FromBase64String(certHeader);
certificate = new X509Certificate2(clientCertBytes);
certSubject = certificate.Subject;
certIssuer = certificate.Issuer;
certThumbprint = certificate.Thumbprint;
certSignatureAlg = certificate.SignatureAlgorithm.FriendlyName;
certIssueDate = certificate.NotBefore.ToShortDateString() + " " + certificate.NotBefore.ToShortTimeString();
certExpiryDate = certificate.NotAfter.ToShortDateString() + " " + certificate.NotAfter.ToShortTimeString();
}
catch (Exception ex)
{
errorString = ex.ToString();
}
finally
{
isValidCert = IsValidClientCertificate();
if (!isValidCert) Response.StatusCode = 403;
else Response.StatusCode = 200;
}
}
else
{
certHeader = "";
}
}
//
// This is a sample verification routine. You should modify this method to suit your application logic and security requirements.
//
//
private bool IsValidClientCertificate()
{
// In this example, the certificate is accepted as a valid certificate only if these conditions are met:
// - The certificate isn't expired and is active for the current time on the server.
// - The subject name of the certificate has the common name nildevecc.
// - The issuer name of the certificate has the common name nildevecc and the organization name Microsoft Corp.
// - The thumbprint of the certificate is 30757A2E831977D8BD9C8496E4C99AB26CB9622B.
//
// This example doesn't test that the certificate is chained to a trusted root authority (or revoked) on the server.
// It allows self-signed certificates.
//
if (certificate == null || !String.IsNullOrEmpty(errorString)) return false;
// 1. Check time validity of the certificate.
if (DateTime.Compare(DateTime.Now, certificate.NotBefore) < 0 || DateTime.Compare(DateTime.Now, certificate.NotAfter) > 0) return false;
// 2. Check the subject name of the certificate.
bool foundSubject = false;
string[] certSubjectData = certificate.Subject.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach (string s in certSubjectData)
{
if (String.Compare(s.Trim(), "CN=nildevecc") == 0)
{
foundSubject = true;
break;
}
}
if (!foundSubject) return false;
// 3. Check the issuer name of the certificate.
bool foundIssuerCN = false, foundIssuerO = false;
string[] certIssuerData = certificate.Issuer.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach (string s in certIssuerData)
{
if (String.Compare(s.Trim(), "CN=nildevecc") == 0)
{
foundIssuerCN = true;
if (foundIssuerO) break;
}
if (String.Compare(s.Trim(), "O=Microsoft Corp") == 0)
{
foundIssuerO = true;
if (foundIssuerCN) break;
}
}
if (!foundIssuerCN || !foundIssuerO) return false;
// 4. Check the thumbprint of the certificate.
if (String.Compare(certificate.Thumbprint.Trim().ToUpper(), "30757A2E831977D8BD9C8496E4C99AB26CB9622B") != 0) return false;
return true;
}
}
}
Node.js 範例
下列 Node.js 範例程式代碼會取得 X-ARR-ClientCert 標頭,並使用 node-forge 將這個 Base64 編碼的 Privacy Enhanced Mail(PEM)字串轉換為憑證物件,並加以驗證。
import { NextFunction, Request, Response } from 'express';
import { pki, md, asn1 } from 'node-forge';
export class AuthorizationHandler {
public static authorizeClientCertificate(req: Request, res: Response, next: NextFunction): void {
try {
// Get header.
const header = req.get('X-ARR-ClientCert');
if (!header) throw new Error('UNAUTHORIZED');
// Convert from PEM to PKI certificate.
const pem = `-----BEGIN CERTIFICATE-----${header}-----END CERTIFICATE-----`;
const incomingCert: pki.Certificate = pki.certificateFromPem(pem);
// Validate certificate thumbprint.
const fingerPrint = md.sha1.create().update(asn1.toDer(pki.certificateToAsn1(incomingCert)).getBytes()).digest().toHex();
if (fingerPrint.toLowerCase() !== 'abcdef1234567890abcdef1234567890abcdef12') throw new Error('UNAUTHORIZED');
// Validate time validity.
const currentDate = new Date();
if (currentDate < incomingCert.validity.notBefore || currentDate > incomingCert.validity.notAfter) throw new Error('UNAUTHORIZED');
// Validate issuer.
if (incomingCert.issuer.hash.toLowerCase() !== 'abcdef1234567890abcdef1234567890abcdef12') throw new Error('UNAUTHORIZED');
// Validate subject.
if (incomingCert.subject.hash.toLowerCase() !== 'abcdef1234567890abcdef1234567890abcdef12') throw new Error('UNAUTHORIZED');
next();
} catch (e) {
if (e instanceof Error && e.message === 'UNAUTHORIZED') {
res.status(401).send();
} else {
next(e);
}
}
}
}
Java 範例
下列 JAVA 類別會將憑證從 X-ARR-ClientCert 編碼為 X509Certificate 執行個體。 certificateIsValid() 驗證憑證的指紋是否符合建構函式中指定的指紋,且憑證未過期。
import java.io.ByteArrayInputStream;
import java.security.NoSuchAlgorithmException;
import java.security.cert.*;
import java.security.MessageDigest;
import sun.security.provider.X509Factory;
import javax.xml.bind.DatatypeConverter;
import java.util.Base64;
import java.util.Date;
public class ClientCertValidator {
private String thumbprint;
private X509Certificate certificate;
/**
* Constructor.
* @param certificate. The certificate from the "X-ARR-ClientCert" HTTP header.
* @param thumbprint. The thumbprint to check against.
* @throws CertificateException if the certificate factory can't be created.
*/
public ClientCertValidator(String certificate, String thumbprint) throws CertificateException {
certificate = certificate
.replaceAll(X509Factory.BEGIN_CERT, "")
.replaceAll(X509Factory.END_CERT, "");
CertificateFactory cf = CertificateFactory.getInstance("X.509");
byte [] base64Bytes = Base64.getDecoder().decode(certificate);
X509Certificate X509cert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(base64Bytes));
this.setCertificate(X509cert);
this.setThumbprint(thumbprint);
}
/**
* Check that the certificate's thumbprint matches the one given in the constructor, and that the
* certificate isn't expired.
* @return True if the certificate's thumbprint matches and isn't expired. False otherwise.
*/
public boolean certificateIsValid() throws NoSuchAlgorithmException, CertificateEncodingException {
return certificateHasNotExpired() && thumbprintIsValid();
}
/**
* Check certificate's timestamp.
* @return True if the certificate isn't expired. It returns False if it is expired.
*/
private boolean certificateHasNotExpired() {
Date currentTime = new java.util.Date();
try {
this.getCertificate().checkValidity(currentTime);
} catch (CertificateExpiredException | CertificateNotYetValidException e) {
return false;
}
return true;
}
/**
* Check whether the certificate's thumbprint matches the given one.
* @return True if the thumbprints match. False otherwise.
*/
private boolean thumbprintIsValid() throws NoSuchAlgorithmException, CertificateEncodingException {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] der = this.getCertificate().getEncoded();
md.update(der);
byte[] digest = md.digest();
String digestHex = DatatypeConverter.printHexBinary(digest);
return digestHex.toLowerCase().equals(this.getThumbprint().toLowerCase());
}
// Getters and setters.
public void setThumbprint(String thumbprint) {
this.thumbprint = thumbprint;
}
public String getThumbprint() {
return this.thumbprint;
}
public X509Certificate getCertificate() {
return certificate;
}
public void setCertificate(X509Certificate certificate) {
this.certificate = certificate;
}
}
Python 範例
下列 Flask 和 Django Python 程式碼範例會實作一個名為 authorize_certificate 的裝飾項目,可在檢視函式上使用,僅允許存取出示有效用戶端憑證的呼叫端。 它會預期標頭中有 PEM 格式的 X-ARR-ClientCert 憑證,並使用 Python 密碼編譯 套件根據其指紋、主體通用名稱、簽發者通用名稱和開始和到期日期來驗證憑證。 如果驗證失敗,裝飾項目可確保將狀態代碼為 403 的 HTTP 回應傳回給用戶端。
from functools import wraps
from datetime import datetime, timezone
from flask import abort, request
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
def validate_cert(request):
try:
cert_value = request.headers.get('X-ARR-ClientCert')
if cert_value is None:
return False
cert_data = ''.join(['-----BEGIN CERTIFICATE-----\n', cert_value, '\n-----END CERTIFICATE-----\n',])
cert = x509.load_pem_x509_certificate(cert_data.encode('utf-8'))
fingerprint = cert.fingerprint(hashes.SHA1())
if fingerprint != b'12345678901234567890':
return False
subject = cert.subject
subject_cn = subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
if subject_cn != "contoso.com":
return False
issuer = cert.issuer
issuer_cn = issuer.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
if issuer_cn != "contoso.com":
return False
current_time = datetime.now(timezone.utc)
if current_time < cert.not_valid_before_utc:
return False
if current_time > cert.not_valid_after_utc:
return False
return True
except Exception as e:
# Handle any errors encountered during validation.
print(f"Encountered the following error during certificate validation: {e}")
return False
def authorize_certificate(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not validate_cert(request):
abort(403)
return f(*args, **kwargs)
return decorated_function
下列程式碼片段示範如何在 Flask 檢視函式上使用裝飾項目。
@app.route('/hellocert')
@authorize_certificate
def hellocert():
print('Request for hellocert page received')
return render_template('index.html')
