匯出 (0) 列印
全部展開

使用 Azure AD 保護 Windows 市集應用程式和 REST Web 服務 (預覽)

發佈時間: 2013年4月

更新日期: 2014年5月

note附註
此範例已過時。其中的技術、方法和 (或) 使用者介面指示已被更新的功能取代。若要查看可建立類似應用程式的更新版範例,請參閱 NativeClient-WindowsStore

此逐步解說會說明如何開發獲授權可代表 Azure AD 使用者呼叫 Web API 的 Windows 市集應用程式。原生用戶端應用程式和 Web API 都由租用戶管理員登錄至 Azure AD。此系統管理員可以控制用戶端和服務之間的關係,包括為用戶端和服務指派個別權限。授權則使用 OAuth 2.0 承載授與類型來處理,而 Azure AD 會發出重新整理權杖,將使用者需重新輸入認證的頻率降至最低。

此逐步解說完成的產品很類似「Windows 市集適用的 AAL」程式碼範例,您可從 MSDN 程式碼庫取得:程式碼範例:Windows 市集應用程式到 REST 服務 - 透過瀏覽器對話方塊與 AAD 進行驗證 (英文)

此逐步解說涵蓋:

  • 建置資源 Web API。此服務將是要求 Azure AD 驗證的 MVC 4 Web API。

  • 建置用戶端應用程式。此原生用戶端是 Windows 市集應用程式。

  • 將原生用戶端和 Web API 登錄至 Azure AD。也會授與呼叫 Web API 的存取權給原生用戶端。

note附註
此逐步解說使用「Windows 市集適用的 AAL」,它目前仍處於開發人員預覽階段。由於是發行前產品,其功能在未來版本中可能有所變更。

開始進行逐步解說前,必須先符合下列必要條件:

  • 網際網路連線

  • 有效的 Azure 訂閱:您可在此取得 90 天免費試用:Azure 免費試用

  • Visual Studio 2012 Professional 或 Visual Studio 2012 Ultimate:您可在此下載免費試用:Visual Studio 免費試用

  • Windows 8:若要開發此應用程式,您必須使用 Windows 8 並已註冊取得開發人員授權。詳細資訊

為了示範如何搭配 Azure AD 使用原生用戶端,我們將使用可讓使用者透過其 Azure AD 帳戶授權 REST 服務存取權的「Azure Active Directory 驗證程式庫 (AAL)」,來建置 Windows 市集應用程式。

在此逐步解說中,我們也將建置一個簡單服務,供用戶端呼叫。此服務將成為供使用者儲存其 ToDo 清單的中央位置,此服務可以裝載於內部部署或 Azure。

下圖說明相關各方的角色。

  1. Contoso ToDo 清單服務,用於儲存使用者的 ToDo 清單項目。這是可讓使用者儲存和取得項目的 Web API。使用者不能管理其他使用者的項目。

  2. Contoso ToDo 清單用戶端應用程式,可讓使用者透過 Azure AD 存取服務,以及檢視和建立 ToDo 清單項目。

  3. 由組織建立的 Azure AD 租用戶,在此例中是 Contoso。此租用戶包含使用者、應用程式和服務的安全性主體。

解決方案架構

在此逐步解說中需要做的第一件事,是確定您已具備可在 Azure 管理入口網站中使用的 Azure AD 租用戶。

Azure Active Directory 是可提供 Office 365 和 Intune 等 Microsoft 服務項目的識別骨幹的服務。如果您有訂閱這其中任一服務,您就已具備使用者可用來登入的 Azure AD 租用戶。如果想重複利用該租用戶,目前為止您可以建立新的 Azure 訂閱,並在註冊時使用該租用戶中的系統管理員使用者。此逐步解說不提供如何執行該程序的詳細指導方針。

在此逐步解說中,我們將著重在您沒有現有 Azure AD 租用戶的案例。我們要做的第一件事是確定您已具備可在 Azure 管理入口網站使用的 Azure AD 租用戶。Azure 管理入口網站提供從管理入口網站頁面直接建立新租用戶的機制。

  1. 瀏覽至 http://manage.windowsazure.com,以與您 Azure 訂閱關聯的 Microsoft 帳戶登入。登入後,查看畫面左側的索引標籤清單,找到 [Active Directory] 索引標籤並按一下。



    加入企業目錄

    在此處看到的畫面是 Active Directory 在 Azure 管理入口網站中的首頁。上方區域提供兩個不同的標頭。[存取控制命名空間] 標頭是指透過 ACS 2.0 服務建立的命名空間,此逐步解說不討論這一部份。[目錄] 標頭會導向管理入口網站中操作目錄租用戶的區域,這是我們大部分活動進行的位置。

  2. 如上方螢幕擷取畫面所示,Azure 管理入口網站會偵測您的使用者是否擁有關聯的目錄租用戶,若無則提供建立選項。按一下 [建立您的目錄]。



    加入企業租用戶

    接著會出現一個對話方塊,用於為您收集建立目錄租用戶所需的必要資訊。讓我們由下往上看看每個欄位:

    • 組織名稱:此為必要欄位,如果需要顯示公司名稱,便會使用其值做為 Moniker。

    • 國家或地區:在此下拉式清單中選取的值將決定在哪裡建立您的租用戶。假設目錄將會儲存機密資訊,因此請衡量您公司營運所在國家/地區的隱私相關規範。

    • 網域名稱:此欄位代表一項重要資訊:這是您租用戶專屬之目錄租用戶網域名稱的一部分,可將您的租用戶與其他目錄租用戶區別開來。

      在建立期間,每個目錄租用戶都以此形式的網域加以識別:<tenantname>.onmicrosoft.com。該網域將用於所有目錄使用者的 UPN 以及一般需要識別目錄租用戶的位置。建立之後,您可以登錄您擁有的其他網域。如需詳細資訊,請參閱網際網路網域

      [網域名稱] 必須是唯一的:UI 驗證邏輯會協助您挑選唯一值。建議您選擇可以代表您公司的控制代碼,因為這樣可以協助使用者和合作夥伴與目錄租用戶互動。

    只需填寫該對話方塊,就能建立目錄租用戶。按一下右下角的核取按鈕後,Azure AD 就會根據所指定的參數,建立新的租用戶。您會在下方看到新項目。



    企業目錄

    note附註
    目錄租用戶建立後,會設定為將使用者和認證儲存在雲端。如果您要將目錄租用戶與內部部署 Windows Server Active Directory 整合,您可以在此處找到詳細指示。

  3. 按一下新建立的目錄項目,顯示使用者管理 UI。



    所有租用戶使用者

    目錄租用戶最初是空的,除了管理新租用戶建立位置之 Azure 訂閱的 Microsoft 帳戶。

    將該 Microsoft 帳戶列在此處,表示它具有租用戶的 [全域管理員] 權限,不過這只限於透過 Azure 管理入口網站 UI 執行的作業。該 Microsoft 帳戶無法針對網頁 SSO 等流程對目錄租用戶實際進行驗證,因此不能用做網頁 SSO 逐步解說的測試使用者。

    讓我們新增使用者至目錄,以便可以執行本文稍後的網頁 SSO 案例。

  4. 按一下畫面底部命令列的 [加入使用者] 命令。接著會顯示如下所示的對話方塊。



    加入使用者

    [加入使用者] 對話方塊一開始會詢問您是要建立目錄使用者,還是要新增現有的 Microsoft 帳戶 (新增此類帳戶與目前用於管理訂閱的 Microsoft 帳戶有相同限制)。我們的工作流程需要一個目錄使用者,因此請選取 [組織中的新使用者] 項目。

  5. 選擇使用者名稱,然後按一下右下角的箭頭。



    加入使用者

    在下一個畫面中,您可以選擇一些基本的使用者屬性。您指派給使用者的角色將決定使用者存取目錄時可以做哪些事。

    您需要提供帳戶的有效電子郵件。您也可以選擇嘗試 Multi-Factor Authentication 預覽版。

  6. 填入值後,按一下右下角的箭頭移往下一個畫面。



    暫時密碼

    在上一個步驟中,管理入口網站會產生暫時密碼,在第一次登入時必須使用。到時候會強制使用者變更密碼。請保存暫時密碼,因為所有元件設定好以後,我們需要用它來測試案例。

    至此,我們已具備在網頁 SSO 案例中提供驗證授權單位所需的一切:即目錄租用戶和位於其中的有效使用者。

在本節中,您將建立 ToDo 清單服務,用於維護 Contoso 員工的 ToDo 項目。服務將包含 ASP.NET Web API 並使用「JSON Web 權杖處理常式」,確保只有獲授權的使用者和用戶端可以存取服務。

  1. 啟動 Visual Studio,依序按一下[檔案]、[加入] 和 [專案]。

  2. 在 [新增專案] 對話方塊中,從左側功能表的 Visual C# 範本中選取 [Web],然後選取 [ASP.NET MVC 4 Web 應用程式] 範本。將新專案命名為 "ToDoListService"、將方案命名為 "ToDoListApp",然後按一下 [確定]。

  3. [新增 ASP.NET MVC 4 專案] 對話方塊隨即出現。選取 [Web API] 範本,然後按一下 [確定]。[Web API] 範本會建立簡易 Web API 專案的必要 Scaffolding。



    新的 ASP.NET MVC 4 專案

  4. 新專案建立後,按 F5 執行專案。此時應該會開啟瀏覽器,並顯示一個類似以下螢幕擷取畫面的頁面:



    產生新的 ASP.NET 專案

    在開啟的 Web 瀏覽器中,您會注意到 Visual Studio 自動指派 URL 給應用程式,例如上圖中的 http://localhost:29062/。您稍後會用到此值,因為它代表下列組態值:

    • Todo 清單服務專案中的 Audience

    • 在 Azure AD 中登錄應用程式識別碼 URI,並供 ToDo 清單用戶端用來識別服務

    對於實際執行應用程式,建議使用此值做為裝載應用程式之網站的基礎位址,例如:https://todolistservice.azurewebsites.net。不過,它也可以是租用戶中任何唯一的 URI。

    Important重要事項
    實際執行服務必須使用 SSL 來保護連接至服務的通訊,防止攻擊者竊取驗證權杖與執行提高權限攻擊。

  5. 關閉 Web 瀏覽器,繼續下一個步驟。

現在您可以對專案加入必要的相依性。

  1. 使用滑鼠右鍵按一下 [方案總管] 中的 [參考],然後按一下 [加入參考]。

  2. 在 [參考管理員] 對話方塊中,選取 System.IdentityModel 組件,然後按一下 [確定]。參考就會加入至您的專案。

  3. 以滑鼠右鍵按一下 [參考] 資料夾,然後按一下 [管理 NuGet 封裝...] 來安裝 JSON Web Token 處理常式 NuGet 封裝。

  4. 在 [管理 NuGet 封裝] 對話方塊中,搜尋 'jwt handler'。出現結果後,選取 [Microsoft .NET Framework 適用的 JSON Web 權杖處理常式] 然後按一下 [安裝]。

  5. [授權接受] 對話方塊隨即出現。如果您同意授權條款,請按一下 [我接受]。接著會下載「JWT 處理常式」,並在您的專案中參考。

  6. 在相同的 [管理 NuGet 封裝] 對話方塊中,搜尋 'token validation extension'。出現結果後,選取 [Microsoft .NET Framework 4.5 適用的 Microsoft 權杖驗證延伸模組] 然後按一下 [安裝]。

  7. [授權接受] 對話方塊隨即出現。如果您同意授權條款,請按一下 [我接受]。接著會下載「權杖驗證延伸模組」,並在您的專案中參考。

既然我們已加入必要的相依性,現在需要更新 Global.asax.cs 檔案,以便開始使用「JWT 處理常式」來驗證 Azure AD 發出的權杖。當用戶端呼叫 Web API 服務時,會從授權標頭讀取權杖。

隨即驗證權杖,以確保:

  • 權杖簽發者正確,正是您 Azure AD 租用戶的簽發者

  • 權杖採用正確的 x509 憑證加密編譯簽署

  • 權杖會授與必要的權限給呼叫的應用程式

更新後的 Global.asax.cs 檔案的完整內容如下所示,隨後將說明各項變更。

using System;
using System.Collections.Generic;
using System.IdentityModel.Metadata;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using System.Xml;
using System.Xml.Linq;

namespace ToDoListService
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode, 
    // visit http://go.microsoft.com/?LinkId=9394801

    public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
            GlobalConfiguration.Configuration.MessageHandlers.Add(new TokenValidationHandler());
        }
    }

    internal class TokenValidationHandler : DelegatingHandler
    {
        // Domain name or Tenant name
        const string domainName = "[company/domain name like treyresearch.onmicrosoft.com]";
        const string audience = "[service App ID URI configured in tenant/domain for the Service App ex: http://localhost:40316/]";

        static DateTime _stsMetadataRetrievalTime= DateTime.MinValue;
        static List<X509SecurityToken> _signingTokens = null;
        static string _issuer = string.Empty;
        

        // SendAsync is used to validate incoming requests contain a valid access token, and sets the current user identity 
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            string jwtToken;
            string issuer;
            List<X509SecurityToken> signingTokens;

            if (!TryRetrieveToken(request, out jwtToken))
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
            }

            try
            {
                // Get tenant information that's used to validate incoming jwt tokens
                GetTenantInformation(string.Format("https://login.windows.net/{0}/federationmetadata/2007-06/federationmetadata.xml", domainName), out issuer, out signingTokens);
            }
            catch (Exception)
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.InternalServerError));
            }

            JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler()
            {
                CertificateValidator = X509CertificateValidator.None
            };

            TokenValidationParameters validationParameters = new TokenValidationParameters
            {
                AllowedAudience = audience,
                ValidIssuer = issuer,
                SigningTokens = signingTokens
            };

            try
            {

                 // Validate token
                ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwtToken,
                                                validationParameters);

                //set the ClaimsPrincipal on the current thread.
                Thread.CurrentPrincipal = claimsPrincipal;

                // set the ClaimsPrincipal on HttpContext.Current if the app is running in web hosted environment.
                if (HttpContext.Current != null)
                {
                    HttpContext.Current.User = claimsPrincipal;
                }            

                // Verify that required permission is set in the scope claim
                if (ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/scope").Value != "user_impersonation")
                {
                    return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
                }

                return base.SendAsync(request, cancellationToken);
            }
            catch (SecurityTokenValidationException)
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
            }
            catch (Exception)
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.InternalServerError));
            }
        }

        // Reads the token from the authorization header on the incoming request
        private static bool TryRetrieveToken(HttpRequestMessage request, out string token)
        {
            token = null;
            string authzHeader;

            if (!request.Headers.Contains("Authorization"))
            {
                return false;
            }

            authzHeader = request.Headers.GetValues("Authorization").First<string>();

            // Verify Authorization header contains 'Bearer' scheme
            token = authzHeader.StartsWith("Bearer ") ? authzHeader.Split(' ')[1] : null;

            if (null == token)
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// Parses the federation metadata document and gets issuer Name and Signing Certificates
        /// </summary>
        /// <param name="metadataAddress">URL of the Federation Metadata document</param>
        /// <param name="issuer">Issuer Name</param>
        /// <param name="signingTokens">Signing Certificates in the form of X509SecurityToken</param>
        static void GetTenantInformation(string metadataAddress, out string issuer, out List<X509SecurityToken> signingTokens)
        {
            signingTokens = new List<X509SecurityToken>();

            // The issuer and signingTokens are cached for 24 hours. They are updated if any of the conditions in the if condition is true.            
            if (DateTime.UtcNow.Subtract(_stsMetadataRetrievalTime).TotalHours > 24
                || string.IsNullOrEmpty(_issuer)
                || _signingTokens == null)
            {
                MetadataSerializer serializer = new MetadataSerializer()
                {
                    CertificateValidationMode = X509CertificateValidationMode.None
                };
                MetadataBase metadata = serializer.ReadMetadata(XmlReader.Create(metadataAddress));

                EntityDescriptor entityDescriptor = (EntityDescriptor)metadata;

                // get the issuer name
                if (!string.IsNullOrWhiteSpace(entityDescriptor.EntityId.Id))
                {
                    _issuer = entityDescriptor.EntityId.Id;
                }

                // get the signing certs
                _signingTokens = ReadSigningCertsFromMetadata(entityDescriptor);

                _stsMetadataRetrievalTime = DateTime.UtcNow;
            }

            issuer = _issuer;
            signingTokens = _signingTokens;
        }

        static List<X509SecurityToken> ReadSigningCertsFromMetadata(EntityDescriptor entityDescriptor)
        {
            List<X509SecurityToken> stsSigningTokens = new List<X509SecurityToken>();

            SecurityTokenServiceDescriptor stsd = entityDescriptor.RoleDescriptors.OfType<SecurityTokenServiceDescriptor>().First();

            if (stsd != null && stsd.Keys != null)
            {
                IEnumerable<X509RawDataKeyIdentifierClause> x509DataClauses = stsd.Keys.Where(key => key.KeyInfo != null && (key.Use == KeyType.Signing || key.Use == KeyType.Unspecified)).
                                                             Select(key => key.KeyInfo.OfType<X509RawDataKeyIdentifierClause>().First());

                stsSigningTokens.AddRange(x509DataClauses.Select(clause => new X509SecurityToken(new X509Certificate2(clause.GetX509RawData()))));
            }
            else
            {
                throw new InvalidOperationException("There is no RoleDescriptor of type SecurityTokenServiceType in the metadata");
            }

            return stsSigningTokens;
        }
    }
}


執行此程式碼前,您必須設定下列常數:

  • domainName:這是您為 Azure AD 租用戶登錄的網域名稱。例如:contoso.onmicrosoft.com

  • audience:這是 ToDo 清單服務的應用程式識別碼 URI,稍後將用來對 Azure AD 登錄。此常數的標準值是 ToDo 清單服務執行所在的基底位址。例如,在 Visual Studio 中開發時:http://localhost:29062/

建議使用基底位址做為服務的 audience 值,因為此值可讓呼叫者從服務位址衍生出目標服務的識別。

現在我們將更詳細討論以上所示的程式碼。

Global.asax.cs 中,權杖處理常式是在管線中設定,以便:

  • 從要求的授權標頭取得權杖

  • 從租用戶的安全性權杖服務擷取中繼資料

  • 使用中繼資料中的簽署金鑰和簽發者來驗證權杖

  • 將目前執行緒的識別設定為權杖中的使用者識別

  • 驗證呼叫應用程式 (例如 ToDo 清單用戶端) 已獲授與必要權限,可以代表使用者呼叫 Web API

已將下列 using 指示詞加入至 Global.asax.cs 檔案:

using System;
using System.Collections.Generic;
using System.IdentityModel.Metadata;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using System.Xml;
using System.Xml.Linq;

接著加入了 TokenValidationHandler 類別,這是從 DelegatingHandler 類別衍生。由 TokenValidationHandler 執行大部分工作,驗證用於向服務進行驗證的權杖。

若要開始編寫處理常式,請加入下列程式碼:

internal class TokenValidationHandler : DelegatingHandler
    {
        // Domain name or Tenant name
        const string domainName = "[company/domain name like treyresearch.onmicrosoft.com]";
        const string audience = "[service App ID URI configured in tenant/domain for the Service App ex: http://localhost:40316/]";

        static DateTime _stsMetadataRetrievalTime= DateTime.MinValue;
        static List<X509SecurityToken> _signingTokens = null;
        static string _issuer = string.Empty;

請注意,必須設定 domainNameaudience

domainName 是您在註冊 Azure AD 時所登錄的網域名稱。範例值為:contoso.onmicrosoft.com

audience 是 ToDo 清單服務的應用程式識別碼 URI。此值衍生自服務執行位置的本機位址,例如:http://localhost:29062/。應用程式識別碼 URI 將設為用於向服務進行驗證之權杖的 audience 宣告,表示權杖的預定目標。這可讓 ToDo 清單服務確認權杖不是用於另一個服務並加以拒絕,有助於預防轉送攻擊。

請注意,_issuer_signingTokens 欄位未設定。稍後將使用 Azure AD 主控之同盟中繼資料文件中的資訊來設定這些欄位,未來可以動態更新其值。應用程式務必要能夠更新此項資訊,尤其是針對簽署憑證,因為當發生憑證變換時,這些資訊會隨時間改變。

為了要讀取同盟中繼資料文件,以及擷取簽發者和憑證資訊,加入了下列程式碼:

        /// <summary>
        /// Parses the federation metadata document and gets issuer Name and Signing Certificates
        /// </summary>
        /// <param name="metadataAddress">URL of the Federation Metadata document</param>
        /// <param name="issuer">Issuer Name</param>
        /// <param name="signingTokens">Signing Certificates in the form of X509SecurityToken</param>
        static void GetTenantInformation(string metadataAddress, out string issuer, out List<X509SecurityToken> signingTokens)
        {
            signingTokens = new List<X509SecurityToken>();

            // The issuer and signingTokens are cached for 24 hours. They are updated if any of the conditions in the if condition is true.            
            if (DateTime.UtcNow.Subtract(_stsMetadataRetrievalTime).TotalHours > 24
                || string.IsNullOrEmpty(_issuer)
                || _signingTokens == null)
            {
                MetadataSerializer serializer = new MetadataSerializer()
                {
                    // turning off certificate validation for demo. Don't use this in production code.
                    CertificateValidationMode = X509CertificateValidationMode.None
                };
                MetadataBase metadata = serializer.ReadMetadata(XmlReader.Create(metadataAddress));

                EntityDescriptor entityDescriptor = (EntityDescriptor)metadata;

                // get the issuer name
                if (!string.IsNullOrWhiteSpace(entityDescriptor.EntityId.Id))
                {
                    _issuer = entityDescriptor.EntityId.Id;
                }

                // get the signing certs
                _signingTokens = ReadSigningCertsFromMetadata(entityDescriptor);

                _stsMetadataRetrievalTime = DateTime.UtcNow;
            }

            issuer = _issuer;
            signingTokens = _signingTokens;
        }

提供同盟中繼資料檔案的 URL,此程式碼就能讀取檔案的內容並傳回所需資訊。

只需定期讀取同盟中繼資料,不需針對每個要求都加以讀取,依此方式實作簡單的中繼資料快取。中繼資料會在初次使用時進行擷取,或在自上次讀取後 24 小時後再次擷取。最重要的同盟中繼資料更新發生在簽署憑證變換時。變換期間,過期憑證和新憑證會有數天時間都可供使用。此延長期限讓服務有足夠時間可以取得這兩個憑證。

下一步,我們將看看位於 TokenValidationHandler 類別中的 SendAsync() 方法。

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)

每個對 ToDo 清單服務發出的要求都會呼叫 SendAsync()。透過確保要求包含有效權杖,然後將目前使用者識別設為已驗證使用者,藉此驗證每個連入要求。下列程式碼會從向 ToDo 清單服務發出之要求的授權標頭中讀取權杖。

if (!TryRetrieveToken(request, out jwtToken))
{
    return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}

上述程式碼使用 TryRetieveToken() 來從標頭讀取權杖,如以下程式碼所示。

// Reads the token from the authorization header on the incoming request
        private static bool TryRetrieveToken(HttpRequestMessage request, out string token)
        {
            token = null;
            string authzHeader;

            if (!request.Headers.Contains("Authorization"))
            {
                return false;
            }

            authzHeader = request.Headers.GetValues("Authorization").First<string>();

            // Verify Authorization header contains 'Bearer' scheme
            token = authzHeader.StartsWith("Bearer ") ? authzHeader.Split(' ')[1] : null;

            if (null == token)
            {
                return false;
            }

            return true;
        }

上述程式碼會檢查授權標頭字串中是否有權杖。標頭值的開頭必須是 "Bearer",才能符合 RFC 6750「OAuth 2.0 授權架構:承載權杖用法」(該文件說明如何在 Web API 呼叫中傳遞權杖)。如果在標頭中找到權杖,就會傳回 true

從標頭中取得權杖後,必須加以驗證。驗證程序需要針對從 Azure AD 租用戶中繼資料、簽發者和對象擷取到的憑證驗證權杖簽章。JWT 處理常式會根據 TokenValidationParameters 物件上設定的值來執行此驗證程序,此物件在 SendAsync() 方法中建立,如下所示。

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            string jwtToken;
            string issuer;
            List<X509SecurityToken> signingTokens;

            if (!TryRetrieveToken(request, out jwtToken))
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
            }

            try
            {
                // Get tenant information that's used to validate incoming jwt tokens
                GetTenantInformation(string.Format("https://login.windows.net/{0}/federationmetadata/2007-06/federationmetadata.xml", domainName), out issuer, out signingTokens);
            }
            catch (Exception)
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.InternalServerError));
            }

            JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler()
            {
                CertificateValidator = X509CertificateValidator.None
            };

            TokenValidationParameters validationParameters = new TokenValidationParameters
            {
                AllowedAudience = audience,
                ValidIssuer = issuer,
                SigningTokens = signingTokens
            };

            try
            {

                 // Validate token
                ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwtToken,
                                                validationParameters);

                //set the ClaimsPrincipal on the current thread.
                Thread.CurrentPrincipal = claimsPrincipal;

                // set the ClaimsPrincipal on HttpContext.Current if the app is running in web hosted environment.
                if (HttpContext.Current != null)
                {
                    HttpContext.Current.User = claimsPrincipal;
                }            

                // Verify that required permission is set in the scope claim
                if (ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/scope").Value != "user_impersonation")
                {
                    return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
                }

                return base.SendAsync(request, cancellationToken);
            }
            catch (SecurityTokenValidationException)
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
            }
            catch (Exception)
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.InternalServerError));
            }
        }

權杖驗證成功後,會使用來自權杖的宣告,在目前執行緒和 HttpContext 上設定呼叫者的識別 (兩者都需要設定,以便確保在 ASP.NET 和 IIS 之間轉換時不會遺失內容)。宣告會包含使用者以及呼叫者 (亦即 ToDo 清單用戶端) 的相關資訊。

如果權杖驗證失敗,則會傳回「HTTP 未授權」錯誤。最後,在方法傳回前,會先檢查權杖中的權限。檢查方法是檢查目前宣告主體中的 "scp" (scope) 宣告。必要值是 "user_impersonation",可讓呼叫者建立和讀取 ToDo 項目。

                 // Verify that required permission is set in the scope claim
                if (ClaimsPrincipal.Current.FindFirst("scp").Value != "user_impersonation")
                {
                    return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
                }

                return base.SendAsync(request, cancellationToken);

當租用戶管理員同意將用戶端應用程式加入其租用戶時,就會設定要授與給 ToDo 清單用戶端的權限。此程序將在稍後步驟中處理。

現在您已學會 TokenValidationHandler 的運作方式,我們還需要將它登錄至 Application_Start() 方法。

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    WebApiConfig.Register(GlobalConfiguration.Configuration);
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
            GlobalConfiguration.Configuration.MessageHandlers.Add(new TokenValidationHandler());
        }

上方程式碼片段中的最後一行將處理常式加入至每個連入要求的要求管線。

接下來,我們要加入 ToDo 項目的模型。這是一個包含 ToDo 項目資料的小型類別。

  1. 在 [方案總管] 中,以滑鼠右鍵按一下 [模型] 資料夾,然後依序按一下 [加入] 和 [類別]。



    加入新的類別

  2. 在 [加入新項目] 對話方塊中,輸入名稱 ToDoItem 並按一下 [加入]。



    加入新的類別

  3. 新類別就會加入專案中。使用下列項目取代預設程式碼:

    namespace ToDoListService.Models
    {
        public class ToDoItem
        {
            public string Title { get; set; }
            public string Owner { get; set; }
        }
    }
    
    

  1. 在 [方案總管] 中,以滑鼠右鍵按一下 [控制器] 資料夾,依序按一下 [加入] 和 [控制器]。



    加入新的控制器

  2. 在 [加入控制器] 對話方塊中,輸入名稱 ToDoListController。從 [範本] 下拉式功能表中選取 [具有空白讀取/寫入動作的 API 控制器],然後按一下 [加入]。



    加入控制器

  3. 開啟 ToDoListController.cs 並使用下列項目取代現有程式碼:

    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Claims;
    using System.Web.Http;
    using ToDoListService.Models;
    
    namespace ToDoListService.Controllers
    {
        public class ToDoListController : ApiController
        {
            // ToDo items list for all users
            static ConcurrentBag<ToDoItem> todoBag = new ConcurrentBag<ToDoItem>();
    
            // GET api/todolist returns 
            public IEnumerable<ToDoItem> Get()
            {
                Claim subject = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier);
    
                return from todo in todoBag
                       where todo.Owner == subject.Value
                       select todo;
            }
    
            // POST api/todolist
            public void Post(ToDoItem todo)
            {
                if (null != todo && !string.IsNullOrWhiteSpace(todo.Title))
                {
                    todoBag.Add(new ToDoItem { Title = todo.Title, Owner = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value });
                }
            }
        }
    }
    
    現在我們要討論您已加入的程式碼。ConcurrentBag 物件是安全執行緒物件,用於儲存所有使用者的 ToDo 項目。在實際服務中,這些項目會儲存在資料庫裡,但為求簡化,我們僅將資料儲存在記憶體中。

    // Todo items list for all users
    static ConcurrentBag<ToDoItem> todoBag = new ConcurrentBag<ToDoItem>();
    
    現在,我們已有了可以儲存 ToDo 項目的位置,Post() 方法也更新為包含將項目加入 ConcurrentBag 的功能。

    // POST api/todolist
    public void Post(ToDoItem todo)
    {
        if (null != todo && !string.IsNullOrWhiteSpace(todo.Title))
        {
            todoBag.Add(new ToDoItem { Title = todo.Title, Owner = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value });
        }
    }
    
    請注意,方法簽章已更新為接受 ToDoItem 物件,這會序列化呼叫者向物件發出的表單 POST。此外,此方法可確定當 ToDoItem 的標題空白時,不會將其加入。

    隨即建立新的 ToDoItem,並儲存至 ToDo 清單。項目擁有者已設為主旨識別項,且保證對於使用者是唯一的。此方法中的 ClaimsPrincipal 先前設定在 Global.asax.cs 中,反映來自 Azure AD 之權杖中的使用者識別。因此可以確保使用者只能存取他們自己的 ToDo 項目。

    用來確認使用者識別的物件識別碼 (即 oid) 宣告類型是 Azure AD 中使用者帳戶的物件識別碼。

    接下來,我們要討論 Get() 方法:

            // GET api/todolist returns 
            public IEnumerable<ToDoItem> Get()
            {
                Claim subject = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier);
    
                return from todo in todoBag
                       where todo.Owner == subject.Value
                       select todo;
            }
    
    
    簽章已更新以傳回 IEnumerable<ToDoItem>,此值會設定為目前使用者擁有的 ToDo 項目。

    目前使用者的識別由查看目前的 ClaimsPrincipal 決定,然後僅傳回使用者擁有的項目。

  4. F5 確認服務可以執行。

現在我們已編寫好 Web API,可以開始編寫用戶端應用程式。此應用程式將是 ToDo 清單用戶端,可顯示使用者的 ToDo 項目並允許使用者新增項目。

我們即將開始撰寫用戶端,但請先切換到在 Azure AD 租用戶中登錄用戶端和服務。為了向 Azure AD 取得將用於 ToDo 清單用戶端的用戶端識別碼,這是必要步驟。

下圖顯示我們即將建置的應用程式,此應用程式可允許使用者授與授權給 ToDo 清單用戶端以呼叫服務,以及新增或檢視工作。

待辦事項清單應用程式

建置應用程式的步驟為:

  1. 建立 Windows 市集應用程式的專案

  2. 以 XAML 編寫使用者介面

  3. 在程式碼後置中,使用 AAL 向 Web API 進行驗證

  4. 加入移除與應用程式關聯之帳戶的選項,讓其他使用者可以檢視他們的 ToDo 項目

開始編寫應用程式前,您必須先取得 Windows 8 開發人員授權 (如果還未取得的話)。這是免費授權,可依照這些步驟來取得。

  1. 在 Visual Studio 中,於現有方案中建立新專案。在 [方案總管] 中,以滑鼠右鍵按一下方案,依序按一下 [加入] 和 [新增專案]。

  2. 在 [加入新的專案] 對話方塊中,從左側的 [Visual C#] 下拉式清單中選取 [Windows 市集],然後選取 [空白的應用程式 (XAML)]。



    加入新的專案



    加入新的專案

  3. 將專案命名為 "TodoListClient",然後按一下 [確定]。

您需要為 ToDo 清單用戶端專案安裝 Windows 市集適用的 Azure 驗證程式庫 (AAL) NuGet 封裝。開啟 NuGet 封裝管理員並在線上搜尋 "AAL",然後選取 [Windows 市集適用的 AAL 封裝]。

為了讓 AAL 能夠在 ToDo 清單用戶端中運作,必須授與下列功能給應用程式。

  • 企業驗證

  • 網際網路 (用戶端)

  • 私人網路 (用戶端和伺服器)

  • 共用使用者憑證

您可以開啟 Package.appxmanifest 檔案來設定這些功能。

開啟 Package.appxmanifest 檔案

接著瀏覽至 [功能] 索引標籤並選取必要的功能:

選取所需的功能

當 ToDo 清單用戶端呼叫 AAL 以取得權杖時,AAL 需要開啟「Windows Web 驗證代理人」,以便讓使用者向 AAD 進行驗證並授與存取權給 ToDo 清單用戶端以呼叫服務。「Web 驗證代理人」是用來在 Windows 市集應用程式中呈現登入網頁。

當代理人上裝載的網頁瀏覽至向代理人登錄的回呼 URI 時,「Web 驗證代理人」就會關閉。您可透過呼叫 WebAuthenticationBroker.GetCurrentApplicationCallbackUri() 找出預設的回呼 URI。URI 包含應用程式的 SID,外觀類似下列字串:ms-app://s-1-15-2-1482697178-942858973-646103894-873360561-3829487091-4151517581-178579545/

這將是 Azure AD 用來回應來自 ToDo 清單用戶端之 OAuth 2.0 要求的 URI,所以必須登錄至 Azure AD 以避免將回應傳回至錯誤的應用程式。

  1. 將下列 using 指示詞加入至 MainPage.xaml.cs 檔案:using Windows.Security.Authentication.Web;

  2. 將下列程式碼加入至 MainPage.xaml.cs 中的 OnNavigatedTo() 方法,讓方法看起來類似下列:

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        string redirectUri = WebAuthenticationBroker.GetCurrentApplicationCallbackUri().ToString();
    }
    
  3. 在您剛加入的這一行設定中斷點,然後執行 ToDo 清單用戶端專案:



    OnNavigatedTo 方法

  4. 遇到中斷點後,進入下一行 (F11) 並儲存 redirectUri 的值。這是 ToDo 清單用戶端的重新導向 URI,將用戶端登錄至 Azure AD 時將需要它。

    note附註
    若想在 Azure AD 中正確設定應用程式的服務主體,ToDo 清單用戶端應用程式 URL 是必要項目。例如:ms-app://s-1-15-2-1482697178-942858973-646103894-873360561-3829487091-4151517581-178579545/

至此,您已建立 ToDo 清單 Web API 專案和 ToDo 清單用戶端。現在需使用 Azure 管理入口網站,將它們登錄至 Azure AD。此登錄程序的結果是授權 ToDo 清單用戶端代表用戶端的使用者呼叫 ToDo 清單 Web API。在更深的層次上,因為用戶端的使用者向 Azure AD 通過驗證後,會傳回包含 scp 宣告和 user_impersonation 權限的權杖,因此用戶端獲授權可以呼叫 Web API。

  1. 在 Web 瀏覽器中,前往 https://manage.windowsazure.com

  2. 在 Azure 管理入口網站中,按一下左側功能表中的 [Active Directory] 圖示,然後按一下您的組織名稱。

  3. 從 [快速入門] 功能表中,按一下 [應用程式] 索引標籤。

  4. 在命令列上按一下 [加入]。

  5. 在精靈的第一個頁面上,於 [名稱] 欄位中輸入 Web API 的使用者易記名稱。當使用者在 Azure AD 目錄中尋找已登錄的用戶端應用程式時,這個名稱可讓使用者輕易識別,因此在我們的案例中,將其命名為 ToDo List Web API。[類型] 下方的 [Web 應用程式和/或 Web API] 選項應該已被選取,因此按一下箭頭繼續。

  6. 在下一個頁面上,輸入 Web API 的 [應用程式 URL] 和 [應用程式識別碼 URI]。[應用程式 URL] 是 Web API 的位址,而 [應用程式識別碼 URI] 是應用程式的邏輯識別碼。在我們的案例中,這兩個值一樣,因此將其設為 http://localhost:29062/,然後按下一步箭頭。

  7. 在下一個頁面上,選取您的 Web API 需要的目錄存取類型。在我們的案例中,我們不會存取 Graph API,因此應選取 [單一登入],然後按一下核取記號按鈕來完成。

    加入 Web API 後,您需要登錄用戶端應用程式。下列步驟將說明如何完成此程序。

  8. 在 Azure 管理入口網站中,返回您組織目錄的 [應用程式] 索引標籤。

  9. 在命令列上按一下 [加入]。

  10. 在精靈的第一個頁面上,輸入用戶端應用程式的使用者易記名稱 (例如 ToDo List Client),然後在 [類型] 下方選取 [原生用戶端應用程式] 選項。按一下箭頭以繼續。

  11. 在下一個頁面上,輸入您的用戶端應用程式的 [重新導向 URI]。這是 Azure AD 為回應 OAuth 2.0 要求而重新導向的位置。這是您將使用上一節取得之 ms-app://… 值 (例如 ms-app://s-1-15-2-1482697178-942858973-646103894-873360561-3829487091-4151517581-178579545/) 的位置。按一下核取記號按鈕。

    您的用戶端應用程式現在已加入 Azure AD 中。下列步驟將說明如何設定用戶端應用程式對 Web API 的存取權。

  12. 應該會顯示用戶端應用程式的 [快速入門] 頁面。按一下上方功能表上的 [設定],接著會顯示應用程式組態頁面。

  13. 在組態頁面的 [Web API] 區段中,展開 [設定此原生用戶端應用程式的 Web API 存取] 的下拉式功能表,然後按一下您先前加入的 [ToDo List Web API]。

  14. 選取服務後,按一下命令列上的 [儲存]。

現在我們將繼續建置 ToDo 清單用戶端。

我們要編寫的用戶端第一部份是 ToDo 清單用戶端的使用者介面。

開啟 MainPage.xaml.cs 檔案並使用下列項目取代預設標記:

<Page
    x:Class="ToDoListClient.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:ToDoListClient"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="120"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="120"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <TextBlock Grid.Column="1" Grid.Row="1" Style="{StaticResource PageHeaderTextStyle}" Text="ToDo List"/>
        <Grid Grid.Column="1" Grid.Row="2">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="150"/>
                <ColumnDefinition Width="300" />
            </Grid.ColumnDefinitions>
            <TextBox x:Name="ToDoText" Grid.Column="0" Grid.Row="2"  Margin="5" />
            <Button Grid.Column="1" Margin="10,0,0,0" Click="Button_Click_Add_Todo" >Add ToDo Item</Button>
            <HyperlinkButton Grid.Column="2" HorizontalAlignment="Right" Content="Remove Account" Click="HyperlinkButton_Click_Remove_Account"/>
        </Grid>
        <GridView x:Name="ToDoList" Grid.Column="1" Grid.Row="3" Margin="0,10,0,0" >
            <GridView.ItemTemplate>
                <DataTemplate>
                    <StackPanel Width="200" Height="150" Margin="10" Background="#FFA2A2A4" >
                        <TextBlock Text="{Binding Title}" FontSize="24" TextWrapping="Wrap" Margin="10"/>
                    </StackPanel>
                </DataTemplate>
            </GridView.ItemTemplate>
        </GridView>
    </Grid>
</Page>

這可在格線中提供簡單格線版面配置,帶有三個欄和四個列。

  • <TextBox> 是使用者輸入 ToDo 項目標題的位置。

  • <Button> 用來提交 ToDo 項目。

  • <HyperlinkButton> 用來移除使用者工作階段資料。

  • <GridView> 用來顯示使用者的所有 ToDo 項目。程式碼後置檔案會將 ToDo 清單的資料繫結至 GridView。

如此即完成建立 UI 的標記。下一步,我們將加入程式碼後置檔案。

現在我們要加入程式碼後置檔案,以允許使用 Web API 加入和取得 ToDo 項目。用戶端程式碼會使用 AAL 來向 Azure AD 要求授權權杖,以便存取服務。

開啟 MainPage.xaml.cs 檔案並加入下列程式碼。

using Microsoft.Preview.WindowsAzure.ActiveDirectory.Authentication;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using Windows.Data.Json;
using Windows.Security.Authentication.Web;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;

// The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=234238

namespace TodoListClient
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainPage : Page
    {
        //Tenant name
        const string domainName = "[Your Domain Name. Example: contoso.onmicrosoft.com]";

        //Client app information
        const string clientID = "[Your client id. Example: de119d9a-c0b9-4ac0-b794-ca82e9d7dcd4]";

        //Resource information
        const string resourceAppIDUri = "[Your service application Uri. Example: http://localhost:29062/]";
const string resourceBaseAddress = "[The base address at which your service can be reached. Example: http://localhost:29062/]";

        private HttpClient httpClient = new HttpClient();

        private AuthenticationContext authenticationContext = new AuthenticationContext("https://login.windows.net/" + domainName);

        public MainPage()
        {
            this.InitializeComponent();
        }

        /// <summary>
        /// Invoked when this page is about to be displayed in a Frame.
        /// </summary>
        /// <param name="e">Event data that describes how this page was reached.  The Parameter
        /// property is typically used to configure the page.</param>
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            GetTodoList();
        }

        // Get the current user's todo items
        private async void GetTodoList()
        {
            HttpResponseMessage response;

            // Acquire a token from AAL. AAL will cache the authorization state in a persistent cache.
            AuthenticationResult result = await authenticationContext.AcquireTokenAsync(resourceAppIDUri, clientID);

            // Verify that an access token was successfully acquired
            if (AuthenticationStatus.Succeeded != result.Status)
            {
                DisplayErrorWhenAcquireTokenFails(result);
                return;
            }

            // Once the token has been returned by AAL, add it to the http authorization header, before making the call to access
            // the resoruce service.
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

            // Call the todolist web api
            response = await httpClient.GetAsync(resourceBaseAddress + "/api/todolist");

            // Verify the call to the todo list service succeeded
            if (response.IsSuccessStatusCode)
            {
                // Read the response as a Json Array and databind to the GridView to display todo items
                var todoArray = JsonArray.Parse(await response.Content.ReadAsStringAsync());

                TodoList.ItemsSource = from todo in todoArray
                                       select new
                                       {
                                           Title = todo.GetObject()["Title"].GetString()
                                       };
            }
            else
            {
                DisplayErrorWhenCallingResourceServiceFails(response.StatusCode);
            }
        }

        // Add a new todo item
        private async void Button_Click_Add_Todo(object sender, RoutedEventArgs e)
        {
            // Acquire a token from AAL. AAL will cache the authorization state
            AuthenticationResult result = await authenticationContext.AcquireTokenAsync(resourceAppIDUri, clientID);

            // Verify that an access token was successfully acquired
            if (AuthenticationStatus.Succeeded != result.Status)
            {
                DisplayErrorWhenAcquireTokenFails(result);
                return;
            }

            // Once the token has been returned by AAL, add it to the http authorization header, before making the call to access
            // the resoruce service.
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

            // Forms encode todo item, to POST to the todo list web api
            HttpContent content = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("Title", TodoText.Text) });

            // Call the todolist web api
            var response = await httpClient.PostAsync(resourceBaseAddress + "/api/todolist", content);

            if (response.IsSuccessStatusCode)
            {
                TodoText.Text = "";
                GetTodoList();
            }
            else
            {
                DisplayErrorWhenCallingResourceServiceFails(response.StatusCode);
            }
        }

        // Clear the authorization state in the application
        private void HyperlinkButton_Click_Remove_Account(object sender, RoutedEventArgs e)
        {
            // Clear session state from the token cache
            (AuthenticationContext.TokenCache as DefaultTokenCache).Clear();

            // Reset UI elements
            TodoList.ItemsSource = null;
            TodoText.Text = "";
        }

        // Displays appropriate error when acquiring a token fails
        private async void DisplayErrorWhenAcquireTokenFails(AuthenticationResult result)
        {
            MessageDialog dialog;

            switch (result.Error)
            {
                case "authentication_canceled":
                    // User cancelled, no need to display a message
                    break;
                case "temporarily_unavailable":
                case "server_error":
                    dialog = new MessageDialog("Please retry the operation. If the error continues, please contact your administrator.", "Sorry, an error has occurred.");
                    await dialog.ShowAsync();
                    break;
                default:
                    // An error occurred when acquiring a token, show the error description in a MessageDialog 
                    dialog = new MessageDialog(string.Format("If the error continues, please contact your administrator.\n\nError: {0}\n\nError Description:\n\n{1}", result.Error, result.ErrorDescription), "Sorry, an error has occurred.");
                    await dialog.ShowAsync();
                    break;
            }
        }

        // Displays appropriate error when calling a resource service fails
        private async void DisplayErrorWhenCallingResourceServiceFails(HttpStatusCode statuscode)
        {
            MessageDialog dialog;

            switch (statuscode)
            {
                case HttpStatusCode.Unauthorized:

                    // An unauthorized error occurred, indicating the security token provided did not satisfy the service requirements
                    // acquiring a new token may fix the issue, this requires clearing state in the AAL cache.
                    // 
                    dialog = new MessageDialog("Would you like to reset your connection? This may help fix the problem.", "Sorry, accessing your todo list has hit a problem.");

                    dialog.Commands.Add(new UICommand("Yes", (c) =>
                    {
                        (AuthenticationContext.TokenCache as DefaultTokenCache).Clear();
                        GetTodoList();
                    }));

                    dialog.Commands.Add(new UICommand("No"));

                    await dialog.ShowAsync();
                    break;

                default:
                    dialog = new MessageDialog("If the error continues, please contact your administrator.”, “Sorry, an error has occurred.");
                    await dialog.ShowAsync();
                    break;
            }
        }
    }
}
note附註
執行上述程式碼前,您必須設定下列三個欄位:

  1. domainName:Azure AD 租用戶的網域名稱,例如 contoso.onmicrosoft.com

  2. clientID:您將用戶端登錄至 Azure AD 時建立的用戶端識別碼,例如 3b616fda-41f9-47cc-8f88-35481e93c2dc。您可在 Azure 管理入口網站中,從應用程式的組態頁面取得此值。

  3. resourceAppIDUri:ToDo 清單 Web API 的應用程式識別碼 URI。標準作法是將此值設為與服務基底位址相同,例如 http://localhost:29062/

  4. resourceBaseAddress:ToDo 清單 Web API 的基底位址。此值必須符合服務裝載位置的基底位址,例如 http://localhost:29062/

接下來我們要討論加入至程式碼後置的程式碼。首先,已將下列 using 指示詞加入至 MainPage.xaml.cs 的頂端:

using Microsoft.Preview.WindowsAzure.ActiveDirectory.Authentication;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using Windows.Data.Json;
using Windows.Security.Authentication.Web;
using Windows.UI.Popups;

接著已加入 ToDo 清單用戶端和 ToDo 清單服務的方案專屬值。如需這些值的說明,請參閱前一節。

public sealed partial class MainPage : Page
{
    //Domain Name
    const string domainName = "[Your Domain Name. Example: contoso.onmicrosoft.com]";

    //Client app information
    const string clientID = "[Your client id. Example: de119d9a-c0b9-4ac0-b794-ca82e9d7dcd4]";

    //Resource information
    const string resourceAppIDUri = "[Your service application URI. Example: http://localhost:29062/]";
    const string resourceBaseAddress = "[The base address at which your service can be reached. Example: http://localhost:29062/]";

已加入 HttpClient 的私人執行個體欄位,以向 ToDo 清單服務進行呼叫:private HttpClient httpClient;

接著已加入另一個 AuthenticationContext 的私人執行個體欄位,這是 AAL 類別。此類別是隨您的租用戶識別碼一併設定。它會指引 ADAL 針對正確的 Azure AD 租用戶來要求授權:private AuthenticationContext authenticationContext = new AuthenticationContext("https://login.windows.net/" + domainName);

現在查看 GetToDoList() 方法。此方法會向 ToDo 清單服務進行呼叫以擷取 ToDo 項目。為了向服務進行驗證,會使用 AAL AuthenticationContext 類別發出要求權杖的呼叫。

將 ToDo 清單服務的應用程式識別碼 URI 傳遞為第一個參數。第二個參數是 ToDo 清單用戶端的 clientID,用於識別權杖要求中的呼叫者。

這是非同步呼叫,因此使用 await 關鍵字封鎖執行緒,直到呼叫傳回為止。這需要 GetTodoList() 方法使用 async 修飾詞。

        // Get the current user's todo items
        private async void GetTodoList()
        {
            HttpResponseMessage response;

            // Acquire a token from AAL. AAL will cache the authorization state in a persistent cache.
            AuthenticationResult result = await authenticationContext.AcquireTokenAsync(resourceAppIDUri, clientID);

AcquireTokenAsync() 發出呼叫後,會發生幾件事。第一次使用時,此呼叫會導致「Windows Web 驗證代理人」開啟和顯示 Azure AD 登入頁面。如下所示:

登入組織帳戶

使用者登入一次後,ToDo 清單用戶端即會獲授與存取權,可以代表已通過 Azure AD 驗證的使用者呼叫服務。這會造成將重新整理權杖傳回給來自 Azure AD 的用戶端。

AAL 程式庫會快取 Azure AD 傳回的重新整理權杖。接著,如果快取中有重新整理權杖,則對 AcquireToken() 的呼叫就不需要再次提示使用者提供權杖。

當 AAL 使用重新整理權杖來要求用戶端的存取權杖時,使用者和用戶端的識別會被編碼至發出給用戶端向服務進行驗證的安全性權杖中。如此可讓服務授權對兩個識別進行呼叫。

傳回 AcquireToken() 呼叫後,會檢查結果以確認呼叫成功:

    // Verify that an access token was successfully acquired
    if (AuthenticationStatus.Succeeded != result.Status)
    {
    DisplayErrorWhenAcquireTokenFails(result);
    return;
    }

如果傳回的呼叫顯示錯誤,請使用呼叫傳回的 AuthorizationStatusDisplayErrorWhenAcquireTokenFails() 用於執行錯誤檢查並視需要顯示訊息給使用者。方法的本文如下所示。

        // Displays appropriate error when acquiring a token fails
        private async void DisplayErrorWhenAcquireTokenFails(AuthenticationResult result)
        {
            MessageDialog dialog;

            switch (result.Error)
            {
                case "authentication_canceled":
// User cancelled, no need to display a message
                    break;
                case "temporarily_unavailable":
                case "server_error":
                    dialog = new MessageDialog("Please retry the operation. If the error continues, please contact your administrator.", "Sorry, an error has occurred.");
                    await dialog.ShowAsync();
                    break;
                default:
                    // An error occurred when acquiring a token, show the error description in a MessageDialog 
                    dialog = new MessageDialog(string.Format("If the error continues, please contact your administrator.\n\nError: {0}\n\n Error Description: {1}",result.Error, result.ErrorDescription), "Sorry, an error has occurred.");
                    await dialog.ShowAsync();
                    break;
            }
        }

處理的錯誤有三個類別:

  • 使用者取消:當使用者在驗證對話方塊外取消時,會傳回 "authentication_canceled" 錯誤。因為是使用者起始這個動作的,所以不需要顯示訊息給使用者。

  • 可重試錯誤:傳回的是 "temporarily_unavailable" 或 "server_error" 時,Azure AD 可能因高承載或其他原因而發生暫時性問題。使用者可以重試作業。

  • 其他:包括其他各種錯誤,通常是由於組態問題,而重試動作不可能修正問題。

現在,回到 GetTodoList() 方法,假設沒有發生錯誤,且 AAL 傳回了權杖 (取自 AAL 快取或是來自 Azure AD 的新權杖)。接著使用權杖來設定對 ToDo 清單服務提出之要求的授權標頭。設定授權標頭時,會在權杖前面加上 "Bearer",以符合 RFC 6750「OAuth 2.0 授權架構:承載權杖用法」(該文件定義如何使用 OAuth 2.0 承載權杖來存取資源)。

// Once the token has been returned by AAL, add it to the http authorization header, before making the call to access
// the resource service.
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

接著向 ToDo 清單服務 Web API 發出 HTTP GET,為使用者取得 ToDo 項目的清單。

// Call the todolist web api
response = await httpClient.GetAsync(resourceBaseAddress + "/api/todolist");

如果呼叫成功,則使用 Linq 查詢 ToDo 清單服務傳回的 JSON 陣列來建構集合,並可用來做為 UI GridView 物件中的資料來源。這會使用資料繫結自動填入 GridViewMainPage.xaml 中定義的 GridView ItemTemplate 會將文字方塊繫結至服務傳回之 ToDo 項目的 Title 內容。

// Verify the call to the ToDo List Service succeeded
if (response.IsSuccessStatusCode)
{
    // Read the response as a Json Array and databind to the GridView to display todo items
    var todoArray = JsonArray.Parse(await response.Content.ReadAsStringAsync());

    TodoList.ItemsSource = from todo in todoArray
                                       select new
                                       {
                                           Title = todo.GetObject()["Title"].GetString()
                                       };
}
else
{
    DisplayErrorWhenCallingResourceServiceFails(response.StatusCode);
}

如果 Web API 呼叫失敗,則會呼叫 DisplayErrorWhenCallingResourceServiceFails() 以向使用者顯示訊息。錯誤處理會針對傳回未授權錯誤的特殊案例進行處理。當用來存取服務的權杖遭到拒絕時,ToDo 清單服務會處理這種錯誤。這包括權杖過期 (不過這不太可能,因為 AAL 會在權杖過期前先要求新權杖) 或權杖因某些其他原因而無法通過驗證檢查。在此案例中,使用者可以選擇重新開始取得權杖 (方法是讓程式碼清除 AAL 快取)。

您可以根據被呼叫之服務的行為,加入其他案例。

        // Displays appropriate error when calling a resource service fails
        private async void DisplayErrorWhenCallingResourceServiceFails(HttpStatusCode statuscode)
        {
            MessageDialog dialog;

            switch (statuscode)
            {
                case HttpStatusCode.Unauthorized:

                    // An unauthorized error occurred, indicating the security token provided did not satisfy the service requirements
                    // acquiring a new token may fix the issue, this requires clearing state in the AAL cache.
                    // 
                    dialog = new MessageDialog("Would you like to reset your connection? This may help fix the problem.", "Sorry, accessing your todo list has hit a problem.");

                    dialog.Commands.Add(new UICommand("Yes", (c) =>
                    {
                        (AuthenticationContext.TokenCache as DefaultTokenCache).Clear();
                        GetTodoList();
                    }));

                    dialog.Commands.Add(new UICommand("No"));

                    await dialog.ShowAsync();
                    break;

                default:
                    dialog = new MessageDialog("If the error continues, please contact your administrator.”, “Sorry, an error has occurred.");
                    await dialog.ShowAsync();
                    break;
            }
        }

接下來要看看將 ToDo 項目加入至使用者 ToDo 清單的程式碼。此方法很類似 GetTodoList() 方法呼叫。使用 AAL 的 AuthenticationContext 類別取得權杖。如果 AAL 已快取了重新整理權杖,則不會出現 UI 提示。接著使用權杖來對 ToDo 清單服務進行驗證,並使用包含 ToDo 項目的編碼訊息格式,傳送 POST 要求給 ToDo 清單服務。接著將 ToDo 項目加入至使用者在服務中的 ToDo 清單。

        // Add a new todo item
        private async void Button_Click_Add_Todo(object sender, RoutedEventArgs e)
        {
            // Aquire a token from AAL. AAL will cache the authorization state
            AuthenticationResult result = await authenticationContext.AcquireTokenAsync(resourceAppIDUri, clientID);

            // Verify that an access token was successfully acquired
            if (AuthenticationStatus.Succeeded != result.Status)
            {
                DisplayErrorWhenAcquireTokenFails(result);
                return;
            }

            // Once the token has been returned by AAL, add it to the http authorization header, before making the call to access
            // the resoruce service.
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

            // Forms encode todo item, to POST to the todolist web api
            HttpContent content = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("Title", TodoText.Text) });

            // Call the todolist web api
            var response = await httpClient.PostAsync(resourceBaseAddress + "/api/todolist", content);

            if (response.IsSuccessStatusCode)
            {
                TodoText.Text = "";
                GetTodoList();
            }
            else
            {
                DisplayErrorWhenCallingResourceServiceFails(response.StatusCode);
            }
        }

此方法由 [加入 ToDo] 按鈕透過 UI 中的按一下事件來呼叫:

<Button Grid.Column="1" Margin="10,0,0,0" Click="Button_Click_Add_Todo" >Add ToDo</Button>

現在,使用者可以授與存取權給 ToDo 清單用戶端以呼叫 ToDo 清單服務,以及檢視和加入項目至其清單。接下來我們要看看從用戶端清除重新整理權杖的程式碼。這會清除 AAL 快取並允許其他使用者使用 ToDo 清單用戶端。權杖快取可以設定為自訂實作;其預設設為 DefaultTokenCache 類型的快取。

  // Clear the authorization state in the application
private void HyperlinkButton_Click_Remove_Account(object sender, RoutedEventArgs e)
{
    // Clear session state from the token cache
    (AuthenticationContext.TokenCache as DefaultTokenCache).Clear();

    // Reset UI elements
    TodoList.ItemsSource = null;
    TodoText.Text = "";
}

AAL 隨附的 DefaultTokenCacheAuthenticationContext 使用的預設權杖快取。DefaultTokenCache 會根據 Azure AAD 租用戶、資源和用戶端來快取權杖,所以快取中可能有多個重新整理權杖和存取權杖。

如果連接至 Microsoft 帳戶之 Windows 8 裝置上執行的應用程式使用 DefaultTokenCache,則快取內容會在連接至相同帳戶的機器之間漫遊。

上述程式碼會存取 AuthenticationContext TokenCache 並清除所有授權狀態。此事件處理常式必須連接至 UI 中的按一下事件:

<HyperlinkButton Grid.Column="2" HorizontalAlignment="Right" Content="Remove Account" Click="HyperlinkButton_Click_Remove_Account"/>

如此便完成 ToDo 清單用戶端程式碼。服務和用戶端已備妥可供測試。

現在讓我們來測試 ToDo 清單用戶端。確定用戶端和服務都在執行中。用戶端啟動時,您會看到下列頁面。第一次呼叫 AAL 時,Web 驗證代理人會開啟,而使用者會在 Azure AD 登入頁面中輸入其認證。

登入組織帳戶

使用者現在可以輸入標題並按一下 [加入 ToDo 項目] 按鈕,即可在 ToDo 清單中輸入項目。

待辦事項清單應用程式初次執行

按一下按鈕時,就會再次呼叫 AAL,但這次會傳回快取權杖並用來呼叫 ToDo 清單服務。隨即會加入項目,並傳回使用者 ToDo 項目的清單。

待辦事項清單應用程式初次執行

若要以其他使用者身分嘗試使用 ToDo 用戶端,請登入您的 Azure AD 租用戶。如果您是在 Azure 訂閱中建立目錄,請前往 Azure 管理入口網站,並使用您的租用戶管理員帳戶登入。

在 [Active Directory] 索引標籤上,選取您的目錄。

在底部工具列中選取 [加入使用者],建立新的使用者。

加入使用者

接著會顯示一個對話方塊,逐步引導您建立使用者。

加入新的使用者

加入帳戶後,返回 ToDo 清單用戶端並按一下 [移除帳戶] 連結。如此會從用戶端清除使用者狀態。

現在,每當按一下 [加入 ToDo 項目] 按鈕以及對 AAL 進行呼叫時,系統都會提示使用者進行驗證。這次改用您的新使用者帳戶。

登入應用程式

通過驗證後,您就能管理新使用者的 ToDo 清單。

已完成的應用程式

在使用者帳戶之間切換,便可以測試服務是否針對每個使用者傳回正確的項目。

現在您已完成建置 ToDo 清單應用程式。

在此選用小節中,我們將討論部署 ToDo 清單服務至 Azure 網站的必要步驟。如果您尚未建立 Azure 網站,請參閱這份文件做為入門。命名好網站的 URL 後,請停下來並依照下一個步驟,更新服務中的 audience。

由於服務的主機名稱變更,我們也要更新服務的應用程式識別碼 URI。做為示範,我們將使用 todolistservice.azurewebsites.net 做為 Azure 網站。如此也需要變更 TodoListService 專案。開啟專案中的 Global.asax.cs,並更新 audience 以符合 Azure 網站的 URL。例如,將 audience 更新為新的資源應用程式識別碼 URI:

const string audience = "https://todolistservice.azurewebsites.net";

對服務進行該變更後,您現在可以準備進行部署。

由於資源 (即 ToDo 清單服務) 在部署至 Azure 後具有新的應用程式識別碼 URI,因此也需要更新用戶端以符合此項變更。用戶端也需要更新為使用新的 URL。

MainPage.xaml.cs 中,更新資源常數為使用新的裝載位址,例如:

const string resourceAppIDUri = "https://todolistservice.azurewebsites.net";
const string resourceBaseAddress = "https://todolistservice.azurewebsites.net";

用戶端現在會連接至服務的正確 URL,向 AAL 要求用於此服務的存取權杖。

最後一個步驟是針對在 Azure 網站中執行之裝載於 Azure 的 ToDo 清單服務執行個體,加入服務登錄。ToDo 清單用戶端也必須獲權呼叫裝載於 Azure 的服務。

若要進行此項組態變更,需在 Azure 管理入口網站中更新 Web API 的資訊。

  1. 在 Web 瀏覽器中,前往 https://manage.windowsazure.com

  2. 在 Azure 管理入口網站中,按一下左側功能表中的 [Active Directory] 圖示,然後按一下想要的目錄。

  3. 從目錄的 [快速入門] 頁面中,按一下接近頁面頂端的 [應用程式] 索引標籤。

  4. 在已登錄應用程式的清單中按一下 ToDo 清單服務。

  5. 從應用程式的 [快速入門] 頁面中,按一下接近頁面頂端的 [設定] 索引標籤。

  6. 在單一登入區段中,使用 http://todolistservice.azurewebsites.net 位址更新 [應用程式識別碼 URI] 和 [回覆 URL],然後按一下命令列上的 [儲存]。

完成這些變更後,請執行 ToDo 清單用戶端,並儲存一些 ToDo 項目至 Azure 網站來試試看。請注意,由於 ToDo 項目儲存在服務的記憶體中,因此隨著回收服務,項目會消失。

您現在已完成此逐步解說。如需此逐步解說中所用技術的詳細資訊,請參閱下方的相關主題。此外,此逐步解說完成的產品很類似「Windows 市集適用的 AAL」程式碼範例,您可從 MSDN 程式碼庫取得:程式碼範例:Windows 市集應用程式到 REST 服務 - 透過瀏覽器對話方塊與 AAD 進行驗證 (英文)

另請參閱

社群新增項目

顯示:
© 2014 Microsoft