为 Azure 应用服务配置 TLS 相互身份验证Configure TLS mutual authentication for Azure App Service

通过为 Azure 应用服务应用启用不同类型的身份验证可以限制对网站的访问。You can restrict access to your Azure App Service app by enabling different types of authentication for it. 若要实现此目的,一种方法是通过 TLS/SSL 发送客户端请求时请求客户端证书,然后验证该证书。One way to do it is to request a client certificate when the client request is over TLS/SSL and validate the certificate. 此机制称为 TLS 相互身份验证或客户端证书身份验证。This mechanism is called TLS mutual authentication or client certificate authentication. 本文介绍如何将应用设置为使用客户端证书身份验证。This article shows how to set up your app to use client certificate authentication.

备注

如果通过 HTTP 而不是 HTTPS 访问站点,不会收到任何客户端证书。If you access your site over HTTP and not HTTPS, you will not receive any client certificate. 因此,如果应用程序需要客户端证书,则你不应允许通过 HTTP 对应用程序发出请求。So if your application requires client certificates, you should not allow requests to your application over HTTP.

准备 Web 应用Prepare your web app

若要为应用服务应用创建自定义安全绑定或启用客户端证书,应用服务计划必须位于“基本” 、“标准” 、“高级” 或“独立” 层级。To create custom security bindings or enable client certificates for your App Service app, your App Service plan must be in the Basic, Standard, Premium, or Isolated tier. 在此步骤中,请确保 Web 应用位于受支持的定价层。In this step, you make sure that your web app is in the supported pricing tier.

登录 AzureSign in to Azure

打开 Azure 门户Open the Azure portal.

搜索并选择“应用服务”。 Search for and select App Services.

选择应用服务

在“应用服务”页上,选择 Web 应用的名称 。On the App Services page, select the name of your web app.

在门户中导航到 Azure 应用

你已登录到 Web 应用的管理页。You have landed on the management page of your web app.

检查定价层Check the pricing tier

在 Web 应用页的左侧导航窗格中,滚动到“设置” 部分,然后选择“增加(应用服务计划)” 。In the left-hand navigation of your web app page, scroll to the Settings section and select Scale up (App Service plan).

扩展菜单

检查以确保 Web 应用不在 F1D1 层中。Check to make sure that your web app is not in the F1 or D1 tier. 深蓝色的框突出显示了 Web 应用的当前层。Your web app's current tier is highlighted by a dark blue box.

检查定价层

F1D1 层不支持自定义 SSL。Custom SSL is not supported in the F1 or D1 tier. 如果需要增加,请按照下一部分中的步骤进行操作。If you need to scale up, follow the steps in the next section. 否则,请关闭“纵向扩展” 页,并跳过纵向扩展应用服务计划部分。Otherwise, close the Scale up page and skip the Scale up your App Service plan section.

纵向扩展应用服务计划Scale up your App Service plan

选择任何非免费层(B1B2B3,或“生产” 类别中的任何层)。Select any of the non-free tiers (B1, B2, B3, or any tier in the Production category). 有关其他选项,请单击“查看其他选项” 。For additional options, click See additional options.

单击“应用” 。Click Apply.

选择定价层

看到以下通知时,说明缩放操作已完成。When you see the following notification, the scale operation is complete.

扩展通知

启用客户端证书Enable client certificates

若要将应用设置为需要客户端证书,可以通过从 Azure 门户选择“配置” > “常规设置”将“需要传入证书”切换到“开”,或者需要将应用的 clientCertEnabled 设置设为 trueTo set up your app to require client certificates, you can switch On the Require incoming certificate by selecting Configuration > General Settings from the Azure Portal or you need to set the clientCertEnabled setting for your app to true. 若要设置该设置,请在 Azure CLI 中运行以下命令。To set the setting, run the following command in the Azure CLI.

az webapp update --set clientCertEnabled=true --name <app_name> --resource-group <group_name>

使路径不要求身份验证Exclude paths from requiring authentication

为应用程序启用相互身份验证时,应用根目录下的所有路径都需要客户端证书才能进行访问。When you enable mutual auth for your application, all paths under the root of your app will require a client certificate for access. 若要允许某些路径对匿名访问保持开放,可以将排除路径定义为应用程序配置的一部分。To allow certain paths to remain open for anonymous access, you can define exclusion paths as part of your application configuration.

可以通过选择“配置” > “常规设置”并定义排除路径来配置排除路径。Exclusion paths can be configured by selecting Configuration > General Settings and defining an exclusion path. 在此示例中,应用程序的 /public 路径下的任何内容都不会请求客户端证书。In this example, anything under /public path for your application would not request a client certificate.

证书排除路径

访问客户端证书Access client certificate

在应用服务中,请求的 TLS 终止发生在前端负载均衡器上。In App Service, TLS termination of the request happens at the frontend load balancer. 已启用客户端证书的情况下将请求转发到应用代码时,应用服务会注入包含客户端证书的 X-ARR-ClientCert 请求标头。When forwarding the request to your app code with client certificates enabled, App Service injects an X-ARR-ClientCert request header with the client certificate. 应用服务不会对此客户端证书执行任何操作,而只会将它转发到你的应用。App Service does not do anything with this client certificate other than forwarding it to your app. 应用代码负责验证客户端证书。Your app code is responsible for validating the client certificate.

对于 ASP.NET,可以通过 HttpRequest.ClientCertificate 属性提供客户端证书。For ASP.NET, the client certificate is available through the HttpRequest.ClientCertificate property.

对于其他应用程序堆栈(Node.js、PHP 等),可以通过 X-ARR-ClientCert 请求标头中的 base64 编码值在应用中提供客户端证书。For other application stacks (Node.js, PHP, etc.), the client cert is available in your app through a base64 encoded value in the X-ARR-ClientCert request header.

ASP.NET 示例ASP.NET sample

    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. Depending on your application logic and security requirements, 
            // you should modify this method
            //
            private bool IsValidClientCertificate()
            {
                // In this example we will only accept the certificate as a valid certificate if all the conditions below are met:
                // 1. The certificate is not expired and is active for the current time on server.
                // 2. The subject name of the certificate has the common name nildevecc
                // 3. The issuer name of the certificate has the common name nildevecc and organization name Microsoft Corp
                // 4. The thumbprint of the certificate is 30757A2E831977D8BD9C8496E4C99AB26CB9622B
                //
                // This example does NOT test that this certificate is chained to a Trusted Root Authority (or revoked) on the server 
                // and it allows for self signed certificates
                //

                if (certificate == null || !String.IsNullOrEmpty(errorString)) return false;

                // 1. Check time validity of certificate
                if (DateTime.Compare(DateTime.Now, certificate.NotBefore) < 0 || DateTime.Compare(DateTime.Now, certificate.NotAfter) > 0) return false;

                // 2. Check subject name of 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 issuer name of 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 thumprint of certificate
                if (String.Compare(certificate.Thumbprint.Trim().ToUpper(), "30757A2E831977D8BD9C8496E4C99AB26CB9622B") != 0) return false;

                return true;
            }
        }
    }

Node.js 示例Node.js sample

以下 Node.js 示例代码获取 X-ARR-ClientCert 标头,并使用 node-forge 将 base64 编码的 PEM 字符串转换为证书对象,然后验证该对象:The following Node.js sample code gets the X-ARR-ClientCert header and uses node-forge to convert the base64-encoded PEM string into a certificate object and validate it:

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.CERT
            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 sample

以下 Java 类将证书从 X-ARR-ClientCert 编码为 X509Certificate 实例。The following Java class encodes the certificate from X-ARR-ClientCert to an X509Certificate instance. certificateIsValid() 验证证书的指纹是否与构造函数中提供的指纹匹配,并且该证书尚未过期。certificateIsValid() validates that the certificate's thumbprint matches the one given in the constructor and that certificate has not expired.

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 cannot 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 has not expired.
     * @return True if the certificate's thumbprint matches and has not expired. False otherwise.
     */
    public boolean certificateIsValid() throws NoSuchAlgorithmException, CertificateEncodingException {
        return certificateHasNotExpired() && thumbprintIsValid();
    }

    /**
     * Check certificate's timestamp.
     * @return Returns true if the certificate has not expired. Returns false if it has expired.
     */
    private boolean certificateHasNotExpired() {
        Date currentTime = new java.util.Date();
        try {
            this.getCertificate().checkValidity(currentTime);
        } catch (CertificateExpiredException | CertificateNotYetValidException e) {
            return false;
        }
        return true;
    }

    /**
     * Check the certificate's thumbprint matches the given one.
     * @return Returns 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;
    }
}