通过 OAuth 2.0、Azure Active Directory B2C 和 Azure API 管理保护 SPA 后端Protect SPA backend with OAuth 2.0, Azure Active Directory B2C and Azure API Management

此方案说明如何配置 Azure API 管理实例以保护 API。This scenario shows you how to configure your Azure API Management instance to protect an API. 我们将使用 Azure AD B2C SPA(身份验证代码 + PKCE)流获取令牌,同时使用 EasyAuth 通过 API 管理来确保 Azure Functions 后端的安全。We'll use the Azure AD B2C SPA (Auth Code + PKCE) flow to acquire a token, alongside API Management to secure an Azure Functions backend using EasyAuth.

目的Aims

我们将了解如何通过 Azure Functions 和 Azure AD B2C 在简化方案中使用 API 管理。We're going to see how API Management can be used in a simplified scenario with Azure Functions and Azure AD B2C. 你需要创建一个调用 API 的 JavaScript (JS) 应用,它使用 Azure AD B2C 登录用户。You'll create a JavaScript (JS) app calling an API, that signs in users with Azure AD B2C. 然后,你将使用 API 管理服务的 validate-jwt、CORS 和“按密钥限制速率”策略功能来保护后端 API。Then you'll use API Management's validate-jwt, CORS, and Rate Limit By Key policy features to protect the Backend API.

为了深度防御,我们随后会使用 EasyAuth 在后端 API 中再次验证令牌,并确保只有 API 管理服务可调用 Azure Functions 后端。For defense in depth, we then use EasyAuth to validate the token again inside the back-end API and ensure that API management is the only service that can call the Azure Functions backend.

学习的内容What will you learn

  • 在 Azure Active Directory B2C 中设置单页应用和后端 APISetup of a Single Page App and backend API in Azure Active Directory B2C
  • 创建 Azure Functions 后端 APICreation of an Azure Functions Backend API
  • 将 Azure Functions API 导入 Azure API 管理Import of an Azure Functions API into Azure API Management
  • 保护 Azure API 管理中的 APISecuring the API in Azure API Management
  • 通过 Microsoft 标识平台库 (MSAL.js) 调用 Azure Active Directory B2C 授权终结点Calling the Azure Active Directory B2C Authorization Endpoints via the Microsoft Identity Platform Libraries (MSAL.js)
  • 存储 HTML/Vanilla JS 单页应用程序,并从 Azure Blob 存储终结点提供该应用Storing a HTML / Vanilla JS Single Page Application and serving it from an Azure Blob Storage Endpoint

先决条件Prerequisites

若要执行本文中的步骤,必须提供:To follow the steps in this article, you must have:

  • 一个 Azure (StorageV2) 常规用途 V2 存储帐户,用于托管前端 JS 单页应用。An Azure (StorageV2) General Purpose V2 Storage Account to host the frontend JS Single Page App.
  • 一个 Azure API 管理实例。任何层都适用,包括“消耗”层,不过某些适用于完整方案的功能在此层中不可用(按密钥限制速率和专用虚拟 IP)- 本文稍后部分将在适当的地方阐述这些限制。An Azure API Management instance (Any tier will work, including 'Consumption', however certain features applicable to the full scenario are not available in this tier (rate-limit-by-key and dedicated Virtual IP), these restrictions are called out below in the article where appropriate).
  • 一个空的 Azure 函数应用(在消耗计划中运行 V3.1 .NET Core 运行时),用于托管调用 APIAn empty Azure Function app (running the V3.1 .NET Core runtime, on a Consumption Plan) to host the called API
  • 一个 Azure AD B2C 租户,它与某订阅相关联。An Azure AD B2C tenant, linked to a subscription.

尽管在实际情况下,会在生产工作负载中使用同一区域中的资源,但是在本操作指南文章中,部署的区域并不重要。Although in practice you would use resources in the same region in production workloads, for this how-to article the region of deployment isn't important.

概述Overview

下图显示了正在使用的组件以及该过程完成后这些组件之间的流。Here's an illustration of the components in use and the flow between them once this process is complete. 正在使用的组件和流Components in use and flow

下面是简要的步骤概述:Here's a quick overview of the steps:

  1. 创建具有范围的 Azure AD B2C 调用(前端、API 管理)和 API 应用程序,并授予 API 访问权限Create the Azure AD B2C Calling (Frontend, API Management) and API Applications with scopes and grant API Access

  2. 创建注册和登录策略,以允许用户使用 Azure AD B2C 进行登录Create the sign up and sign in policies to allow users to sign in with Azure AD B2C

  3. 使用新的 Azure AD B2C 客户端 ID 和密钥配置 API 管理,以在开发人员控制台中启用 OAuth2 用户授权Configure API Management with the new Azure AD B2C Client IDs and keys to Enable OAuth2 user authorization in the Developer Console

  4. 生成函数 APIBuild the Function API

  5. 配置函数 API 以使用新的 Azure AD B2C 客户端 ID 和密钥启用 EasyAuth 并锁定到 APIM VIPConfigure the Function API to enable EasyAuth with the new Azure AD B2C Client ID’s and Keys and lock down to APIM VIP

  6. 在 API 管理中生成 API 定义Build the API Definition in API Management

  7. 为 API 管理 API 配置设置 Oauth2Set up Oauth2 for the API Management API configuration

  8. 设置 CORS 策略,并添加 validate-jwt 策略以验证每个传入请求的 OAuth 令牌 Set up the CORS policy and add the validate-jwt policy to validate the OAuth token for every incoming request

  9. 生成调用应用程序以使用 APIBuild the calling application to consume the API

  10. 上传 JS SPA 示例Upload the JS SPA Sample

  11. 使用新的 Azure AD B2C 客户端 ID 和密钥配置示例 JS 客户端应用Configure the Sample JS Client App with the new Azure AD B2C Client ID’s and keys

  12. 测试客户端应用程序Test the Client Application

    提示

    在演练此文档时,我们将捕获大量信息和密钥等内容,你可能会发现有一个文本编辑器打开来临时存储以下几项配置有方便。We're going to capture quite a few pieces of information and keys etc as we walk this document, you might find it handy to have a text editor open to store the following items of configuration temporarily.

    B2C 后端客户端 ID:B2C BACKEND CLIENT ID:
    B2C 后端客户端密钥:B2C BACKEND CLIENT SECRET KEY:
    B2C 后端 API 范围 URI:B2C BACKEND API SCOPE URI:
    B2C 前端客户端 ID:B2C FRONTEND CLIENT ID:
    B2C 用户流终结点 URI:B2C USER FLOW ENDPOINT URI:
    B2C 已知的 OpenID 终结点:B2C WELL-KNOWN OPENID ENDPOINT:
    B2C 策略名称:Frontendapp_signupandsignin 函数 URL:B2C POLICY NAME: Frontendapp_signupandsignin FUNCTION URL:
    APIM API 基 URL:存储主终结点 URL:APIM API BASE URL: STORAGE PRIMARY ENDPOINT URL:

配置后端应用程序Configure the backend application

在门户中打开“Azure AD B2C”边栏选项卡,然后执行以下步骤。Open the Azure AD B2C blade in the portal and do the following steps.

  1. 选择“应用注册”选项卡Select the App Registrations tab

  2. 单击“新建注册”按钮。Click the 'New Registration' button.

  3. 从“重定向 URI”选择框中,选择“Web”。Choose 'Web' from the Redirect URI selection box.

  4. 现在设置显示名称,请选择一个与正在创建的服务相关的唯一名称。Now set the Display Name, choose something unique and relevant to the service being created. 在本示例中,使用的名称是“后端应用程序”。In this example, we will use the name "Backend Application".

  5. 使用回复 URL 的占位符,例如“https://jwt.ms”(Microsoft 拥有的令牌解码网站),我们稍后将更新这些 URL。Use placeholders for the reply urls, like 'https://jwt.ms' (A Microsoft owned token decoding site), we’ll update those urls later.

  6. 确保已选择“任何标识提供者或组织目录中的帐户(用于通过用户流对用户进行身份验证)”选项Ensure you have selected the "Accounts in any identity provider or organizational directory (for authenticating users with user flows)" option

  7. 对于本示例,请取消选中“授予管理员同意”框,因为目前不需要 offline_access 权限。For this sample, uncheck the "Grant admin consent" box, as we won't require offline_access permissions today.

  8. 单击“注册”。Click 'Register'.

  9. 记录后端应用程序客户端 ID 供以后使用,它显示在“应用程序(客户端)ID”下。Record the Backend Application Client ID for later use (shown under 'Application (client) ID').

  10. 在“管理”下选择“证书和密码”选项卡,然后单击“新建客户端密码”以生成身份验证密钥(接受默认设置,再单击“添加”)。Select the Certificates and Secrets tab (under Manage) then click 'New Client Secret' to generate an auth key (Accept the default settings and click 'Add').

  11. 单击“添加”时,将“值”下的密钥复制到安全的位置,以供稍后用作“后端客户端密码”- 请注意,此对话框是复制此密钥的唯一机会。Upon clicking 'Add', copy the key (under 'value') somewhere safe for later use as the 'Backend client secret' - note that this dialog is the ONLY chance you'll have to copy this key.

  12. 现在,在“管理”下选择“公开 API”选项卡。Now select the Expose an API Tab (Under Manage).

  13. 系统将提示你设置 AppID URI,此时请选择并记录默认值。You will be prompted to set the AppID URI, select and record the default value.

  14. 为函数 API 创建范围并将其命名为“Hello”,随后你可对所有可输入内容的选项使用“Hello”短语,记录填充的完整范围值 URI,再单击“添加范围”。Create and name the scope "Hello" for your Function API, you can use the phrase 'Hello' for all of the enterable options, recording the populated Full Scope Value URI, then click 'Add Scope'.

  15. 选择门户左上角的“Azure AD B2C”痕迹导航栏,返回到 Azure AD B2C 边栏选项卡的根目录。Return to the root of the Azure AD B2C blade by selecting the 'Azure AD B2C' breadcrumb at the top left of the portal.

    备注

    Azure AD B2C 范围是 API 中的有效权限,其他应用程序可以从其应用程序的“API 访问权限”边栏选项卡请求该 API 的访问权限,因此,只需为所调用的 API 创建应用程序权限。Azure AD B2C scopes are effectively permissions within your API that other applications can request access to via the API access blade from their applications, effectively you just created application permissions for your called API.

配置前端应用程序Configure the frontend application

  1. 选择“应用注册”选项卡Select the App Registrations tab
  2. 单击“新建注册”按钮。Click the 'New Registration' button.
  3. 从“重定向 URI”选择框中,选择“单页应用程序(SPA)”。Choose 'Single Page Application (SPA)' from the Redirect URI selection box.
  4. 现在设置显示名称和 AppID URI,请选择一个与将使用此 AAD B2C 应用注册的前端应用程序相关的唯一名称。Now set the Display Name and AppID URI, choose something unique and relevant to the Frontend application that will use this AAD B2C app registration. 在此示例中,可使用“前端应用程序”In this example, you can use "Frontend Application"
  5. 如同首次应用注册一样,将支持的帐户类型选择保留为默认值(通过用户流对用户进行身份验证)As per the first app registration, leave the supported account types selection to default (authenticating users with user flows)
  6. 使用回复 URL 的占位符,例如“https://jwt.ms”(Microsoft 拥有的令牌解码网站),我们稍后将更新这些 URL。Use placeholders for the reply urls, like 'https://jwt.ms' (A Microsoft owned token decoding site), we’ll update those urls later.
  7. 将“授予管理员同意”框保留在勾选状态Leave the grant admin consent box ticked
  8. 单击“注册”。Click 'Register'.
  9. 记录前端应用程序客户端 ID 供以后使用,它显示在“应用程序(客户端)ID”下。Record the Frontend Application Client ID for later use (shown under 'Application (client) ID').
  10. 切换到“API 权限”选项卡。Switch to the API Permissions tab.
  11. 授予对后端应用程序的访问权限,方法是依次单击“添加权限”、“我的 API”、“后端应用程序”和“权限”,再选择上一部分中创建的范围,然后单击“添加权限”Grant access to the backend application by clicking 'Add a permission', then 'My APIs', select the 'Backend Application', select 'Permissions', select the scope you created in the previous section, and click 'Add permissions'
  12. 单击“为{租户}授予管理员同意”,然后在弹出对话框中单击“是”。Click 'Grant admin consent for {tenant} and click 'Yes' from the popup dialog. 此弹出窗口同意“前端应用程序”使用在之前创建的“后端应用程序”中定义的“hello”权限。This popup consents the "Frontend Application" to use the permission "hello" defined in the "Backend Application" created earlier.
  13. 现在,应用的所有权限应会在状态列下显示为绿色勾号All Permissions should now show for the app as a green tick under the status column

创建“注册和登录”用户流Create a "Sign up and Sign in" user flow

  1. 选择 Azure AD B2C 痕迹导航栏,返回到 B2C 边栏选项卡的根目录。Return to the root of the B2C blade by selecting the Azure AD B2C breadcrumb.

  2. 切换到“策略”下的“用户流”选项卡。Switch to the 'User Flows' (Under Policies) tab.

  3. 单击“新建用户流”Click "New user flow"

  4. 选择“注册和登录”用户流类型,选择“推荐”,然后选择“创建”Choose the 'Sign up and sign in' user flow type, and select 'Recommended' and then 'Create'

  5. 为策略指定一个名称并记录,以供以后使用。Give the policy a name and record it for later. 在本例中,可使用“Frontendapp_signupandsignin”;请注意,这将带有“B2C_1_”前缀,构成“B2C_1_Frontendapp_signupandsignin”For this example, you can use "Frontendapp_signupandsignin", note that this will be prefixed with "B2C_1_" to make "B2C_1_Frontendapp_signupandsignin"

  6. 在“标识提供者”和“本地帐户”下,选中“电子邮件注册”(或“用户 ID 注册”,具体取决于 B2C 租户的配置),然后单击“确定”。Under 'Identity providers' and "Local accounts", check 'Email sign up' (or 'User ID sign up' depending on the config of your B2C tenant) and click OK. 使用此配置是因为我们将注册本地 B2C 帐户,而不是推延给其他标识提供者(如社交标识提供者)来使用用户的现有社交媒体帐户。This configuration is because we'll be registering local B2C accounts, not deferring to another identity provider (like a social identity provider) to use an user's existing social media account.

  7. 将 MFA 和条件访问设置保留在默认状态下。Leave the MFA and conditional access settings at their defaults.

  8. 在“用户属性和声明”下,单击“显示更多…”,然后选择希望用户输入并在令牌中返回的声明选项。Under 'User Attributes and claims', click 'Show More...' then choose the claim options that you want your users to enter and have returned in the token. 至少勾选“显示名称”(Display Name) 和“电子邮件地址”(Email Address) 进行收集,要返回的是“Display Name”和“Email Addresses”(请注意,要收集的是 emailaddress(单数),而要求返回的是 email addresses(复数)),然后依次单击“确定”和“创建”。Check at least 'Display Name' and 'Email Address' to collect, with 'Display Name' and 'Email Addresses' to return (pay careful attention to the fact that you are collecting emailaddress, singular, and asking to return email addresses, multiple), and click 'OK', then click 'Create'.

  9. 在列表中选择你创建的用户流,然后单击“运行用户流”按钮。Click on the user flow that you created in the list, then click the 'Run user flow' button.

  10. 此操作将打开“运行用户流”边栏选项卡,此时请选择前端应用程序,复制用户流终结点,并将其保存供以后查看。This action will open the run user flow blade, select the frontend application, copy the user flow endpoint and save it for later.

  11. 复制并存储顶部的链接,将其记录为“已知的 OpenID 配置终结点”供以后使用。Copy and store the link at the top, recording as the 'well-known openid configuration endpoint' for later use.

    备注

    使用 B2C 策略可以公开 Azure AD B2C 登录终结点,使其能够捕获不同的数据组件,并以不同的方式使用户登录。B2C Policies allow you to expose the Azure AD B2C login endpoints to be able to capture different data components and sign in users in different ways.

    在本例中,我们配置了注册或登录流(策略)。In this case we configured a sign up or sign in flow (policy). 这也公开了已知的配置终结点,在这两种情况下,我们创建的策略都通过“p=”查询字符串参数在 URL 中进行了标识。This also exposed a well-known configuration endpoint, in both cases our created policy was identified in the URL by the "p=" query string parameter.

    完成此操作后,你现在就有了一个功能性的企业对消费者标识平台,可将用户登录到多个应用程序。Once this is done, you now have a functional Business to Consumer identity platform that will sign users into multiple applications.

生成函数 APIBuild the function API

  1. 切换回到 Azure 门户中的标准 Azure AD 租户,以便我们可再次配置你订阅中的项。Switch back to your standard Azure AD tenant in the Azure portal so we can configure items in your subscription again.

  2. 转到 Azure 门户的“函数应用”边栏选项卡,打开空函数应用,然后依次单击“函数”和“添加”。Go to the Function Apps blade of the Azure portal, open your empty function app, then click 'Functions', click 'Add'.

  3. 在出现的浮出控件中选择“在门户中开发”,在“选择模板”下选择“HTTP 触发器”,接着在“模板详细信息”中,将其命名为“hello”(授权级别设为“函数”),然后选择“添加”。In the flyout that appears, choose 'Develop in portal', under 'select a template' then choose 'HTTP trigger', under Template details name it 'hello' with authorization level 'Function', then select Add.

  4. 切换到“代码 + 测试”边栏选项卡,并将下面的示例代码复制粘贴到显示的现有代码的上方。Switch to the Code + Test blade and copy-paste the sample code from below over the existing code that appears.

  5. 选择“保存”。Select Save.

    
    using System.Net;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Primitives;
    
    public static async Task<IActionResult> Run(HttpRequest req, ILogger log)
    {
       log.LogInformation("C# HTTP trigger function processed a request.");
    
       return (ActionResult)new OkObjectResult($"Hello World, time and date are {DateTime.Now.ToString()}");
    }
    
    

    提示

    刚粘贴的 c # 脚本函数代码仅向函数日志中记录一行内容,并返回文本“Hello World”,其中包含一些动态数据(日期和时间)。The c# script function code you just pasted simply logs a line to the functions logs, and returns the text "Hello World" with some dynamic data (the date and time).

  6. 从左侧边栏选项卡中选择“集成”,然后在“触发器”框中单击 HTTP(请求)链接。Select “Integration” from the left-hand blade, then click the http (req) link inside the 'Trigger' box.

  7. 从“选定的 HTTP 方法”下拉列表中取消选中 HTTP POST 方法,只保留选择 GET,然后单击“保存”。From the 'Selected HTTP methods' dropdown, uncheck the http POST method, leaving only GET selected, then click Save.

  8. 切换回到“代码 + 测试”选项卡,单击“获取函数 URL”,然后复制显示的 URL 并将其保存供以后查看。Switch back to the Code + Test tab, click 'Get Function URL', then copy the URL that appears and save it for later.

    备注

    你刚才创建的绑定只会指示 Functions 对你刚才复制的 URL (https://yourfunctionappname.chinacloudsites.cn/api/hello?code=secretkey) 的匿名 HTTP GET 请求做出响应。The bindings you just created simply tell Functions to respond on anonymous http GET requests to the URL you just copied (https://yourfunctionappname.chinacloudsites.cn/api/hello?code=secretkey). 现在,我们有一个可缩放的无服务器 HTTP API,它能够返回非常简单的有效负载。Now we have a scalable serverless https API, that is capable of returning a very simple payload.

    你现可使用上面刚复制和保存的 URL 版本,测试从 Web 浏览器调用此 API 的情况。You can now test calling this API from a web browser using your version of the URL above that you just copied and saved. 你还可删除 URL 的查询字符串参数的“?code=secretkey”部分,并再次测试,证明 Azure Functions 将返回 401 错误。You can also remove the query string parameters "?code=secretkey" portion of the URL , and test again, to prove that Azure Functions will return a 401 error.

配置和保护函数 APIConfigure and secure the function API

  1. 需要配置函数应用中的两个额外区域(身份验证和网络限制)。Two extra areas in the function app need to be configured (Authorization and Network Restrictions).

  2. 首先,让我们来配置身份验证/授权,请通过痕迹导航栏导航回到函数应用的根边栏选项卡。Firstly Let's configure Authentication / Authorization, so navigate back to the root blade of the function app via the breadcrumb.

  3. 然后,在“设置”下选择“身份验证/授权”。Next select 'Authentication / Authorization' (under 'Settings').

  4. 启用“应用服务身份验证”功能。Turn on the App Service Authentication feature.

  5. 将“请求未经身份验证时需执行的操作”下拉列表设置为“使用 Azure Active Directory 登录”。Set the Action to take when request is not authenticated dropdown to "Log in with Azure Active Directory".

  6. 在“身份验证提供程序”下,选择“Azure Active Directory”。Under 'Authentication Providers', choose ‘Azure Active Directory’.

  7. 从“管理模式”开关中选择“高级”。Choose ‘Advanced’ from the Management Mode switch.

  8. 将后端应用程序的[应用程序]客户端 ID(来自 Azure AD B2C)粘贴到“客户端 ID”框中Paste the Backend application's [Application] Client ID (from Azure AD B2C) into the ‘Client ID’ box

  9. 将已知的 OpenID 配置终结点从注册和登录策略粘贴到“颁发者 URL”框中(前面已记录此配置)。Paste the Well-known open-id configuration endpoint from the sign up and sign in policy into the Issuer URL box (we recorded this configuration earlier).

  10. 单击“显示密码”,将后端应用程序的客户端密码粘贴到相应的框中。Click 'Show Secret' and paste the Backend application's client secret into the appropriate box.

  11. 选择“确定”,这会返回到“标识提供者选择”边栏选项卡/屏幕。Select OK, which takes you back to the identity provider selection blade/screen.

  12. 将高级设置下的令牌存储保留在启用状态(默认设置)。Leave Token Store enabled under advanced settings (default).

  13. 在边栏选项卡左上方单击“保存”。Click 'Save' (at the top left of the blade).

    重要

    现在,你的函数 API 已部署。如果未提供正确的 JWT 作为 Authorization: Bearer 标头,该 API 应引发 401 响应;而在出现有效请求时,它应返回数据。Now your Function API is deployed and should throw 401 responses if the correct JWT is not supplied as an Authorization: Bearer header, and should return data when a valid request is presented.
    通过配置“使用 Azure AD 登录”选项,已在 EasyAuth 中添加了其他深层防御安全性,以处理未经身份验证的请求。You added additional defense-in-depth security in EasyAuth by configuring the 'Login With Azure AD' option to handle unauthenticated requests. 请注意,这会更改后端函数应用和前端 SPA 之间未经授权的请求行为,因为 EasyAuth 将发出到 AAD 的 302 重定向,而不是 401“未授权”响应,我们将在稍后使用 API 管理来更正此问题。Be aware that this will change the unauthorized request behavior between the Backend Function App and Frontend SPA as EasyAuth will issue a 302 redirect to AAD instead of a 401 Not Authorized response, we will correct this by using API Management later.

    我们仍未应用 IP 安全性,如果拥有有效的密钥和 OAuth2 令牌,则任何人都可以从任何位置调用它,理想情况下,我们希望强制所有请求都通过 API 管理传入。We still have no IP security applied, if you have a valid key and OAuth2 token, anyone can call this from anywhere - ideally we want to force all requests to come via API Management.

    如果使用的是 APIM 消耗层,则没有专用的 Azure API 管理虚拟 IP 列入具有函数访问限制的允许列表中。If you're using APIM Consumption tier then there isn't a dedicated Azure API Management Virtual IP to allow-list with the functions access-restrictions. 在 Azure API 管理标准 SKU 及更高版本中,VIP 是单租户且针对资源生存期In the Azure API Management Standard SKU and above the VIP is single tenant and for the lifetime of the resource. 对于 Azure API 管理消耗层,可通过上面复制的 URI 部分中的共享机密函数密钥锁定 API 调用。For the Azure API Management Consumption tier, you can lock down your API calls via the shared secret function key in the portion of the URI you copied above. 此外,对于消耗层,下面的步骤 12-17 不适用。Also, for the Consumption tier - steps 12-17 below do not apply.

  14. 关闭“身份验证/授权”边栏选项卡Close the 'Authentication / Authorization' blade

  15. 打开门户的“API 管理”边栏选项卡,然后打开你的实例 。Open the API Management blade of the portal, then open your instance.

  16. 记录“概述”选项卡上显示的专用 VIP。Record the Private VIP shown on the overview tab.

  17. 返回到门户的“Azure Functions”边栏选项卡,然后再次打开你的实例 。Return to the Azure Functions blade of the portal then open your instance again.

  18. 选择“网络”,然后选择“配置访问限制”Select 'Networking' and then select 'Configure access restrictions'

  19. 单击“添加规则”,然后输入在上面步骤 3 中复制的 VIP,格式为 xx.xx.xx.xx/32。Click 'Add Rule', and enter the VIP copied in step 3 above in the format xx.xx.xx.xx/32.

  20. 如果要继续与函数门户交互,并执行以下可选步骤,还应在此处添加自己的公共 IP 地址或 CIDR 范围。If you want to continue to interact with the functions portal, and to carry out the optional steps below, you should add your own public IP address or CIDR range here too.

  21. 列表中有允许条目后,Azure 会添加隐式拒绝规则以阻止所有其他地址。Once there’s an allow entry in the list, Azure adds an implicit deny rule to block all other addresses.

需要将 CIDR 格式的地址块添加到 IP 限制面板。You'll need to add CIDR formatted blocks of addresses to the IP restrictions panel. 如果需要添加单个地址(例如 API 管理 VIP),需要按 xx.xx.xx.xx/32 格式添加它。When you need to add a single address such as the API Management VIP, you need to add it in the format xx.xx.xx.xx/32.

备注

现在,除了 API 管理或你的地址之外,应不能从任何位置调用函数 API。Now your Function API should not be callable from anywhere other than via API management, or your address.

  1. 打开“API 管理”边栏选项卡,然后打开你的实例 。Open the API Management blade, then open your instance.

  2. 在“API”下选择“API”边栏选项卡。Select the APIs Blade (under APIs).

  3. 从“添加新 API”窗格中选择“函数应用”,然后从弹出窗口顶部选择“完整”。From the 'Add a New API' pane, choose 'Function App', then select 'Full' from the top of the popup.

  4. 单击“浏览”,选择要在其中托管 API 的函数应用,然后单击“选择”。Click Browse, choose the function app you're hosting the API inside, and click select. 接下来,再次单击“选择”。Next, click select again.

  5. 为 API 提供名称和描述,以便内部使用 API 管理,并将其添加到“无限制”产品。Give the API a name and description for API Management's internal use and add it to the ‘unlimited’ Product.

  6. 复制并记录 API 的“基 URL”,然后单击“创建”。Copy and record the API's 'base URL' and click 'create'.

  7. 单击“设置”选项卡,然后在“订阅”下关闭“需要订阅”复选框,因为在本例中我们将使用 Oauth JWT 令牌来限制速率。Click the 'settings' tab, then under subscription - switch off the 'Subscription Required' checkbox as we will use the Oauth JWT token in this case to rate limit. 请注意,如果使用的是消耗层,则在生产环境中仍然需要此项。Note that if you are using the consumption tier, this would still be required in a production environment.

    提示

    如果使用 APIM 的消耗层,则不受限的产品将无法开箱即用。If using the consumption tier of APIM the unlimited product won't be available as an out of the box. 而是要在“API”下导航到“产品”,然后点击“添加”。Instead, navigate to "Products" under "APIs" and hit "Add".
    键入“无限制”作为产品名称和说明,并从屏幕左下角的“+”API 标注中选择刚添加的 API。Type "Unlimited" as the product name and description and select the API you just added from the "+" APIs callout at the bottom left of the screen. 选择“已发布”复选框。Select the "published" checkbox. 将其余设置保留为默认值。Leave the rest as default. 最后,点击“创建”按钮。Finally, hit the "create" button. 这会创建“不受限”产品并将其分配给 API。This created the "unlimited" product and assigned it to your API. 随后可自定义你的新产品。You can customize your new product later.

配置并捕获正确的存储终结点设置Configure and capture the correct storage endpoint settings

  1. 在 Azure 门户中打开“存储帐户”边栏选项卡Open the storage accounts blade in the Azure portal

  2. 选择所创建的帐户,然后从“设置”部分选择“静态网站”边栏选项卡(如果看不到“静态网站”选项,请检查是否已创建 V2 帐户)。Select the account you created and select the 'Static Website' blade from the Settings section (if you don't see a 'Static Website' option, check you created a V2 account).

  3. 将静态 Web 托管功能设置为“已启用”,将索引文档名称设置为“index.html”,然后单击“保存”。Set the static web hosting feature to 'enabled', and set the index document name to 'index.html', then click 'save'.

  4. 记下“主终结点”的内容供以后使用,因为此位置是托管前端站点的位置。Note down the contents of the 'Primary Endpoint' for later, as this location is where the frontend site will be hosted.

    提示

    可使用 Azure Blob 存储加 CDN 重写或使用 Azure 应用服务来托管 SPA,不过 Blob 存储的静态网站托管功能提供了一个默认容器来从 Azure 存储中提供静态 Web 内容/HTML/JS/CSS,该功能还将推断默认页面,而无需我们执行任何操作。You could use either Azure Blob Storage + CDN rewrite, or Azure App Service to host the SPA - but Blob Storage's Static Website hosting feature gives us a default container to serve static web content / html / js / css from Azure Storage and will infer a default page for us for zero work.

设置 CORS 和 validate-jwt 策略 Set up the CORS and validate-jwt policies

无论使用何种 APIM 层,都应遵循以下部分。The following sections should be followed regardless of the APIM tier being used. 存储帐户 URL 来自于在本文开头的先决条件中提供的存储帐户。The storage account URL is from the storage account you will have made available from the prerequisites at the top of this article.

  1. 切换到门户的“API 管理”边栏选项卡,然后打开你的实例。Switch to the API management blade of the portal and open your instance.

  2. 选择“API”,然后选择“所有 API”。Select APIs, then select “All APIs”.

  3. 在“入站处理”下,单击代码视图按钮“</>”以显示策略编辑器。Under "Inbound processing", click the code view button "</>" to show the policy editor.

  4. 编辑入站部分并粘贴以下 xml,使其类似于以下内容。Edit the inbound section and paste the below xml so it reads like the following.

  5. 在“策略”中,替换以下参数Replace the following parameters in the Policy

  6. 将 {PrimaryStorageEndpoint}(你在上一部分复制的“主存储终结点”)、{b2cpolicy-well-known-openid}(你之前复制的“已知的 OpenID 配置终结点”)和 {backend-api-application-client-id}(后端 API 的 B2C 应用程序/客户端 ID)替换为之前保存的正确值。{PrimaryStorageEndpoint} (The 'Primary Storage Endpoint' you copied in the previous section), {b2cpolicy-well-known-openid} (The 'well-known openid configuration endpoint' you copied earlier) and {backend-api-application-client-id} (The B2C Application / Client ID for the backend API) with the correct values saved earlier.

  7. 如果使用的是 API 管理的消耗层,则应删除“按密钥限制速率”策略,因为使用 Azure API 管理的消耗层时,此策略不可用。If you're using the Consumption tier of API Management, then you should remove both rate-limit-by-key policy as this policy is not available when using the Consumption tier of Azure API Management.

    <inbound>
       <cors allow-credentials="true">
             <allowed-origins>
                 <origin>{PrimaryStorageEndpoint}</origin>
             </allowed-origins>
             <allowed-methods preflight-result-max-age="120">
                 <method>GET</method>
             </allowed-methods>
             <allowed-headers>
                 <header>*</header>
             </allowed-headers>
             <expose-headers>
                 <header>*</header>
             </expose-headers>
         </cors>
       <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid." require-expiration-time="true" require-signed-tokens="true" clock-skew="300">
          <openid-config url="{b2cpolicy-well-known-openid}" />
          <required-claims>
             <claim name="aud">
                <value>{backend-api-application-client-id}</value>
             </claim>
          </required-claims>
       </validate-jwt>
       <rate-limit-by-key calls="300" renewal-period="120" counter-key="@(context.Request.IpAddress)" />
       <rate-limit-by-key calls="15" renewal-period="60" counter-key="@(context.Request.Headers.GetValueOrDefault("Authorization","").AsJwt()?.Subject)" />
    </inbound>
    

    备注

    现在,Azure API 管理能够响应来自 JavaScript SPA 应用的跨域请求,并且会在将请求转发到函数 API 之前,对正在传递的 JWT 身份验证令牌执行限制、限速和预验证。Now Azure API management is able respond to cross origin requests from your JavaScript SPA apps, and it will perform throttling, rate-limiting and pre-validation of the JWT auth token being passed BEFORE forwarding the request on to the Function API.

    恭喜,你现在拥有 Azure AD B2C、API 管理和 Azure Functions 协同配合来发布、保护和使用 API 了!Congratulations, you now have Azure AD B2C, API Management and Azure Functions working together to publish, secure AND consume an API!

    提示

    如果使用的是 API 管理消耗层,则不会通过 JWT 使用者或传入的 IP 地址来限制速率(“消耗”层目前不支持按密钥限制调用速率策略),你可按调用速率配额进行限制,具体请查看此处If you're using the API Management consumption tier then instead of rate limiting by the JWT subject or incoming IP Address (Limit call rate by key policy is not supported today for the "Consumption" tier), you can Limit by call rate quota see here.
    此示例是一个 JavaScript 单页应用程序,因此我们只对速率限制和计费调用使用 API 管理密钥。As this example is a JavaScript Single Page Application, we use the API Management Key only for rate-limiting and billing calls. 实际的授权和身份验证由 Azure AD B2C 处理并封装在 JWT 中,此过程会进行两次验证,先通过 API 管理验证一次,再通过后端 Azure 函数进行验证。The actual Authorization and Authentication is handled by Azure AD B2C, and is encapsulated in the JWT, which gets validated twice, once by API Management, and then by the backend Azure Function.

将 JavaScript SPA 示例上传到静态存储Upload the JavaScript SPA sample to static storage

  1. 依然是在“存储帐户”边栏选项卡中,从“Blob 服务”部分选择“容器”边栏选项卡,然后单击右侧窗格中显示的 $web 容器。Still in the storage account blade, select the 'Containers' blade from the Blob Service section and click on the $web container that appears in the right-hand pane.

  2. 将以下代码作为 index.html 保存到计算机上的本地文件中,然后将文件 index.html 上传到 $web 容器。Save the code below to a file locally on your machine as index.html and then upload the file index.html to the $web container.

     <!doctype html>
     <html lang="en">
     <head>
          <meta charset="utf-8">
          <meta http-equiv="X-UA-Compatible" content="IE=edge">
          <meta name="viewport" content="width=device-width, initial-scale=1">
          <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">
          <script type="text/javascript" src="https://alcdn.msauth.net/browser/2.11.1/js/msal-browser.min.js"></script>
     </head>
     <body>
          <div class="container-fluid">
              <div class="row">
                  <div class="col-md-12">
                     <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
                         <div class="container-fluid">
                             <a class="navbar-brand" href="#">Azure Active Directory B2C with Azure API Management</a>
                             <div class="navbar-nav">
                                 <button class="btn btn-success" id="signinbtn"  onClick="login()">Sign In</a>
                             </div>
                         </div>
                     </nav>
                  </div>
              </div>
              <div class="row">
                  <div class="col-md-12">
                      <div class="card" >
                         <div id="cardheader" class="card-header">
                             <div class="card-text"id="message">Please sign in to continue</div>
                         </div>
                         <div class="card-body">
                             <button class="btn btn-warning" id="callapibtn" onClick="getAPIData()">Call API</a>
                             <div id="progress" class="spinner-border" role="status">
                                 <span class="visually-hidden">Loading...</span>
                             </div>
                         </div>
                      </div>
                  </div>
              </div>
          </div>
          <script lang="javascript">
                 // Just change the values in this config object ONLY.
                 var config = {
                     msal: {
                         auth: {
                             clientId: "{CLIENTID}", // This is the client ID of your FRONTEND application that you registered with the SPA type in AAD B2C
                             authority:  "{YOURAUTHORITYB2C}", // Formatted as https://{b2ctenantname}.b2clogin.cn/tfp/{b2ctenantguid or full tenant name including partner.onmschina.cn}/{signuporinpolicyname}
                             redirectUri: "{StoragePrimaryEndpoint}", // The storage hosting address of the SPA, a web-enabled v2 storage account - recorded earlier as the Primary Endpoint.
                             knownAuthorities: ["{B2CTENANTDOMAIN}"] // {b2ctenantname}.b2clogin.cn
                         },
                         cache: {
                             cacheLocation: "sessionStorage",
                             storeAuthStateInCookie: false 
                         }
                     },
                     api: {
                         scopes: ["{BACKENDAPISCOPE}"], // The scope that we request for the API from B2C, this should be the backend API scope, with the full URI.
                         backend: "{APIBASEURL}/hello" // The location that we will call for the backend api, this should be hosted in API Management, suffixed with the name of the API operation (in the sample this is '/hello').
                     }
                 }
                 document.getElementById("callapibtn").hidden = true;
                 document.getElementById("progress").hidden = true;
                 const myMSALObj = new msal.PublicClientApplication(config.msal);
                 myMSALObj.handleRedirectPromise().then((tokenResponse) => {
                     if(tokenResponse !== null){
                         console.log(tokenResponse.account);
                         document.getElementById("message").innerHTML = "Welcome, " + tokenResponse.account.name;
                         document.getElementById("signinbtn").hidden = true;
                         document.getElementById("callapibtn").hidden = false;
                     }}).catch((error) => {console.log("Error Signing in:" + error);
                 });
                 function login() {
                     try {
                         myMSALObj.loginRedirect({scopes: config.api.scopes});
                     } catch (err) {console.log(err);}
                 }
                 function getAPIData() {
                     document.getElementById("progress").hidden = false; 
                     document.getElementById("message").innerHTML = "Calling backend ... "
                     document.getElementById("cardheader").classList.remove('bg-success','bg-warning','bg-danger');
                     myMSALObj.acquireTokenSilent({scopes: config.api.scopes, account: getAccount()}).then(tokenResponse => {
                         const headers = new Headers();
                         headers.append("Authorization", `Bearer ${tokenResponse.accessToken}`);
                         fetch(config.api.backend, {method: "GET", headers: headers})
                             .then(async (response)  => {
                                 if (!response.ok)
                                 {
                                     document.getElementById("message").innerHTML = "Error: " + response.status + " " + JSON.parse(await response.text()).message;
                                     document.getElementById("cardheader").classList.add('bg-warning');
                                 }
                                 else
                                 {
                                     document.getElementById("cardheader").classList.add('bg-success');
                                     document.getElementById("message").innerHTML = await response.text();
                                 }
                                 }).catch(async (error) => {
                                     document.getElementById("cardheader").classList.add('bg-danger');
                                     document.getElementById("message").innerHTML = "Error: " + error;
                                 });
                     }).catch(error => {console.log("Error Acquiring Token Silently: " + error);
                         return myMSALObj.acquireTokenRedirect({scopes: config.api.scopes, forceRefresh: false})
                     });
                     document.getElementById("progress").hidden = true;
              }
             function getAccount() {
                 var accounts = myMSALObj.getAllAccounts();
                 if (!accounts || accounts.length === 0) {
                     return null;
                 } else {
                     return accounts[0];
                 }
             }
         </script>
      </body>
     </html>
    
  3. 浏览到之前在上一部分存储的静态网站主终结点。Browse to the Static Website Primary Endpoint you stored earlier in the last section.

    备注

    恭喜,你刚才将一个 JavaScript 单页应用部署到了 Azure 存储静态内容托管。Congratulations, you just deployed a JavaScript Single Page App to Azure Storage Static content hosting.
    由于我们尚未使用你的 Azure AD B2C 详细信息配置 JS 应用,因此,如果你打开该页,该页将无法使用。Since we haven’t configured the JS app with your Azure AD B2C details yet - the page won't work yet if you open it.

为 Azure AD B2C 配置 JS SPAConfigure the JavaScript SPA for Azure AD B2C

  1. 现在我们知道了所有内容的位置:我们可通过相应的 API 管理 API 地址和正确的 Azure AD B2C 应用程序/客户端 ID 来配置 SPA。Now we know where everything is: we can configure the SPA with the appropriate API Management API address and the correct Azure AD B2C application / client IDs.
  2. 返回到 Azure 门户中的“存储”边栏选项卡Go back to the Azure portal storage blade
  3. 在“设置”下选择“容器”Select 'Containers' (under 'Settings')
  4. 从列表中选择“$web”容器Select the '$web' container from the list
  5. 从列表中选择 index.html blobSelect index.html blob from the list
  6. 单击“编辑”Click 'Edit'
  7. 更新 msal config 部分中的身份验证值,使其与你之前在 B2C 中注册的前端应用程序相匹配。Update the auth values in the msal config section to match your front-end application you registered in B2C earlier. 使用代码注释获取有关配置值应如何显示的提示。Use the code comments for hints on how the config values should look. 颁发机构值的格式应为: https://{b2ctenantname}.b2clogin.cn/tfp/{b2ctenantname}.partner.onmschina.cn}/{signupandsigninpolicyname}。如果使用的是示例名,且 B2C 租户名称为“contoso”,那么颁发机构应为“https://contoso.b2clogin.cn/tfp/contoso.partner.onmschina.cn}/Frontendapp_signupandsignin”。The authority value needs to be in the format:- https://{b2ctenantname}.b2clogin.cn/tfp/{b2ctenantname}.partner.onmschina.cn}/{signupandsigninpolicyname}, if you have used our sample names and your b2c tenant is called 'contoso' then you would expect the authority to be 'https://contoso.b2clogin.cn/tfp/contoso.partner.onmschina.cn}/Frontendapp_signupandsignin'.
  8. 设置 API 值,使其与后端地址相匹配(之前记录的 API 基 URL,以及之前为后端应用程序记录的“b2cScopes”值)。Set the api values to match your backend address (The API Base Url you recorded earlier, and the 'b2cScopes' values were recorded earlier for the backend application).
  9. 点击“保存”(Save)Click Save

设置 Azure AD B2C 前端应用的重定向 URISet the redirect URIs for the Azure AD B2C frontend app

  1. 打开“Azure AD B2C”边栏选项卡,然后导航到 JavaScript 前端应用程序的应用程序注册。Open the Azure AD B2C blade and navigate to the application registration for the JavaScript Frontend Application.

  2. 单击“重定向 URI”并删除之前输入的占位符“https://jwt.ms”。Click 'Redirect URIs' and delete the placeholder 'https://jwt.ms' we entered earlier.

  3. 为主(存储)终结点添加新的 URI(删除末尾的正斜杠)。Add a new URI for the primary (storage) endpoint (minus the trailing forward slash).

    备注

    此配置将导致前端应用程序的客户端收到具有 Azure AD B2C 中的相应声明的访问令牌。This configuration will result in a client of the frontend application receiving an access token with appropriate claims from Azure AD B2C.
    SPA 将能够将此作为持有者令牌添加到后端 API 调用的 https 标头中。The SPA will be able to add this as a bearer token in the https header in the call to the backend API.

    API 管理将先预验证令牌,速率限制 Azure ID(用户)和调用方 IP 地址发出的 JWT 的使用者对终结点的调用(取决于 API 管理的服务层,详见上面的说明),然后再通过请求传递给负责接收的 Azure 函数 API,添加函数安全密钥。API Management will pre-validate the token, rate-limit calls to the endpoint by both the subject of the JWT issued by Azure ID (the user) and by IP address of the caller (depending on the service tier of API Management, see the note above), before passing through the request to the receiving Azure Function API, adding the functions security key.
    SPA 将在浏览器中呈现响应。The SPA will render the response in the browser.

    恭喜,你已配置了可完美协作的 Azure AD B2C、Azure API 管理、Azure Functions、Azure 应用服务授权!Congratulations, you’ve configured Azure AD B2C, Azure API Management, Azure Functions, Azure App Service Authorization to work in perfect harmony!

现在,我们有了一个具有简单安全的 API 的简单应用,我们来对其进行测试。Now we have a simple app with a simple secured API, let's test it.

测试客户端应用程序Test the client application

  1. 打开在之前创建存储帐户中记下的示例应用 URL。Open the sample app URL that you noted down from the storage account you created earlier.
  2. 单击右上角的“登录”,单击后将弹出 Azure AD B2C 注册或登录配置文件。Click “Sign In” in the top-right-hand corner, this click will pop up your Azure AD B2C sign up or sign in profile.
  3. 该应用会按你的 B2C 资料名称来欢迎你。The app should welcome you by your B2C profile name.
  4. 现在单击“调用 API”,页面应会更新显示从你受保护的 API 发回的值。Now Click "Call API" and the page should update with the values sent back from your secured API.
  5. 如果反复单击“调用 API”按钮,并且你正在 API 管理的开发人员层或更高层级上运行,那么应注意,你的解决方案将开始限制 API 的速率,且此功能应会在应用中报告并显示相应的消息。If you repeatedly click the Call API button and you're running in the developer tier or above of API Management, you should note that your solution will begin to rate limit the API and this feature should be reported in the app with an appropriate message.

大功告成And we're done

可以调整和编辑上述步骤,以通过 API 管理实现 Azure AD B2C 的多种不同用途。The steps above can be adapted and edited to allow many different uses of Azure AD B2C with API Management.

后续步骤Next steps