[VS2010] Visual Studio 2010 與 Windows Azure: 實作在 AppFabric Access Control 上的變更密碼功能

[VS2010] Visual Studio 2010 與 Windows Azure: 實作在 AppFabric Access Control 上的變更密碼功能

變更密碼 (Password Changing) 是在任何一個應用程式都可以看見的常用功能,作用是將帳戶的密碼做變更或重設,而密碼通常會存在資料庫,而且有可能是以純文字 (plain-text) 或是以加密過的字串 (例如 SHA384 雜湊) 來儲存,不同的密碼儲存方式會有不同的優缺點,基本上安全考量會擺在設計密碼儲存功能的第一位,畢竟誰都不想看到自己的密碼被人家盜用吧。一般來說,密碼變更行為有兩種,一種是由使用者自己或是管理人員進行的變更密碼 (Change Password),另一種則是由系統自行產生密碼的重設密碼 (Reset Password) 兩種行為,大多數的應用程式的帳戶管理系統都會同時提供這兩種功能。

如果應用程式打算要將使用者帳戶移轉到 Windows Azure 的 AppFabric Access Control 資料庫,由於 AppFabric Access Control 並不像一般的 .NET Framework 函式庫一樣會有 Class Library 能直接取用,就如同 Membership.ChangePassword() 這樣的函式,所以開發人員要自己想辦法,唯一可以應用的管道就是 AppFabric Access Control 的 Management Service 這組 REST API 了。在前一篇文章中,筆者介紹了如何透過 AppFabric Access Control 的 Management Service REST API 來建構大量建立使用者帳戶 (Issuer) 的應用程式,在本篇筆者就來介紹如何應用 REST API 來實作變更與重設密碼的功能。

在開始之前,筆者先簡單介紹一下 Management Service 中的 REST API 呼叫行為。在 Management Service 中可以執行針對 AC 的 service namespace 中的 token policy, scope, Issuer 以及 rule 四種不同資料的管理工作,而不同的管理動作由不同的 HTTP 動詞來指示,分別是:

  • GET:取得資訊。
  • PUT:更新資訊(除了 rule 是不可更新,若要變更只有刪除重建一途)。
  • POST:新增資訊或是要求存取權仗 (token) 時。
  • DELETE:刪除資訊。

而不同的資料管理,也有不同的 REST URL,像是:

  • 建立 rule:/rulesets/<ruleSetId>/rules
  • 刪除 scope:/scopes/<scopeId>
  • 更新 issuer:/issuers/<issuerId>

在不同的 URL 以及動詞設定下,開發人員在設定 HttpWebRequest 所需要的參數時,就需要特別注意這個部份。

接著,我們就來進行實作:

1. 首先,先取得存取的 token,程式碼為:

public static string getManagementToken(string ServiceName, string ServiceManagementKey)
{
    WebClient client = new WebClient();
    client.BaseAddress = "
https://[service namespace]-mgmt.accesscontrol.windows.net";
    NameValueCollection values = new NameValueCollection();

    values.Add("wrap_name", ServiceName);
    values.Add("wrap_password", ServiceManagementKey);
    values.Add("wrap_scope", "
https://[service namespace].accesscontrol.windows.net/mgmt/issuers");

    byte[] acsResponseInBytes = client.UploadValues("WRAPv0.9", values);
    return HttpUtility.UrlDecode(Encoding.UTF8.GetString(acsResponseInBytes)
        .Split('&')
        .Single(value => value.StartsWith("wrap_access_token=", StringComparison.OrdinalIgnoreCase))
        .Split('=')[1]);
}

 

傳入的參數是:

  • wrap_name:在 AppFabric Access Control 管理網站上可以得到的 Management name,預設是 owner (未來可能會變,以管理網站上可獲得的為準)。
  • wrap_password:在 AppFabric Access Control 管理網站上可以得到的 Management key,為一組 Base64 編碼字串。
  • wrap_scope:要準備存取的 REST API 的 URL。

當 AC 驗證成功時,會回傳 token 的訊息字串,將內部的 wrap_access_token 取出,準備交給呼叫 REST API 的 HttpWebRequest 作為驗證標頭。只要是針對 AC 做 REST API 呼叫,都必須要先取得這個 token。

 

2. 實作取得所有使用者帳戶清單的方法。因為在 REST API 中並沒有可以直接由名稱來查詢 Issuer ID 的方式,因此只能先撈回所有的資料,再做 XPath 查詢以取得正確的 Issuer ID。讀者也可以在建帳戶時就把 Issuer ID 存到資料庫,日後可以由資料庫直接抓。

 

public static IssuerData getIssuerFromName(string IssuerName)
{
    if (string.IsNullOrEmpty(issuersXML))
    {
        HttpWebRequest request = WebRequest.Create("
https://[service namespace].accesscontrol.windows.net/mgmt/issuers") as HttpWebRequest;

        if (string.IsNullOrEmpty(wrap_token))
            wrap_token = getManagementToken("[YOUR_AC_ACCOUNT]", "[YOUR_AC_PASSWORD]"); // 此方法實作請參照前一篇大量建立帳戶的文章。

        request.Method = "GET";
        request.Accept = "application/xml";
        request.Headers.Add("Authorization", string.Format("WRAP access_token=\"{0}\"", wrap_token));

        HttpWebResponse response = request.GetResponse() as HttpWebResponse;
        StreamReader reader = new StreamReader(response.GetResponseStream());

        issuersXML = reader.ReadToEnd();

        reader.Close();
        response.Close();
    }

    XmlDocument issuerDoc = new XmlDocument();
    XmlNamespaceManager xnm = null;
    issuerDoc.LoadXml(issuersXML);

    xnm = new XmlNamespaceManager(issuerDoc.NameTable);
    xnm.AddNamespace("q", "
http://schemas.microsoft.com/ws/2009/06/acs/rest/resources");

    XmlNode issuerNode = issuerDoc.DocumentElement.SelectSingleNode("//q:Issuers/q:Issuer[q:IssuerName = '" + IssuerName + "']", xnm);
    IssuerData issuerData = null;

    if (issuerNode != null)
    {
        issuerData = new IssuerData()
        {
            IssuerID = issuerNode.SelectSingleNode("q:Id", xnm).InnerText,
            DisplayName = issuerNode.SelectSingleNode("q:DisplayName", xnm).InnerText,
            IssuerName = issuerNode.SelectSingleNode("q:IssuerName", xnm).InnerText,
            Password = issuerNode.SelectSingleNode("q:Security/q:CurrentKey", xnm).InnerText
        };
    }

    issuerNode = null;
    issuerDoc = null;
    xnm = null;
    return issuerData;
}

在此程式中的 IssuerData 類別是一個很簡單的 POCO 物件:

public class IssuerData
{
    public string IssuerID { get; set; }
    public string DisplayName { get; set; }
    public string IssuerName { get; set; }
    public string Password { get; set; }
    public const string SecurityAlgorithm = "Symmetric256BitKey";
}

程式自 Management Service 中抓回 Issuer 清單並取出成 IssuerData 後,我們就可以來實作 Change Password 的方法了。

 

3. 實作 Change Password 的方法。

要變更密碼,必須要呼叫 REST API 中的 /issuers/[issuerid] URL,並且將方法改為 PUT,再將更新的資料包裝在指定的 XML 訊息即可 (訊息格式可參考 AppFabric SDK 中的 Management Service 一章)。因此我們撰寫了下面的程式碼:

public static bool changePassword(IssuerData issuerData, string OldPassword, string NewPassword)
{
    if (issuerData.Password != getSHA256Str(OldPassword)) // 變更密碼基本行為:先驗證原本的密碼以避免有心人士的利用。
        throw new InvalidOperationException("PASSWORD_IS_INCORRECT");

    HttpWebRequest request = WebRequest.Create(https://[service namespace].accesscontrol.windows.net/mgmt/issuers/ + issuerData.IssuerID) as HttpWebRequest;
    HttpWebResponse response = null;
    StreamReader reader = null;
    StreamWriter writer = null;
    bool result = false;

    try
    {
        if (string.IsNullOrEmpty(wrap_token))
            wrap_token = getManagementToken("[YOUR_AC_ACCOUNT]", "[YOUR_AC_PASSWORD]"); // 此方法實作請參照前一篇大量建立帳戶的文章。

        request.Method = "PUT";
        request.ContentType = "application/xml";
        request.Headers.Add("Authorization", string.Format("WRAP access_token=\"{0}\"", wrap_token));

        string requestData = string.Format(
            @"
            <Issuer xmlns='
http://schemas.microsoft.com/ws/2009/06/acs/rest/resources'>
              <DisplayName>{0}</DisplayName>
              <Id>{1}</Id>
              <IssuerName>{2}</IssuerName>
              <Security>
                <Algorithm>Symmetric256BitKey</Algorithm>
                <CurrentKey>{3}</CurrentKey>
                <PreviousKey>{3}</PreviousKey>
              </Security>
            </Issuer>
            ", issuerData.DisplayName, issuerData.IssuerID, issuerData.IssuerName, getSHA256Str(NewPassword), getSHA256Str(NewPassword));

        request.ContentLength = requestData.Length;
        writer = new StreamWriter(request.GetRequestStream());
        writer.Write(requestData);
        writer.Close();

        response = request.GetResponse() as HttpWebResponse;
        reader = new StreamReader(response.GetResponseStream());

        issuersXML = reader.ReadToEnd();

        reader.Close();
        response.Close();

        result = true;
    }
    catch (WebException ex)
    {
        result = false;
    }
    finally
    {
        request = null;
        response = null;
    }

    return result;
}

 

4. 最後,我們在主程式中撰寫呼叫程式:

static void Main(string[] args)
{
    IssuerData issuerData = getIssuerFromName("MyUser11"); // 這是筆者事先在 Access Control 上建的帳戶,你的可能和筆者的不同。

    Console.WriteLine("Issuer id: {0}", (issuerData == null) ? "Not Found" : issuerData.IssuerID);

    if (changePassword(issuerData, "[password_old]", "[password_new]")) // 重設密碼。
        Console.WriteLine("Password has been changed.");
    else
        Console.WriteLine("Password cannot be changed.");

    Console.ReadLine();
}

程式執行的結果如下:

image

與 AppFabric 的帳戶資料庫比對,密碼 (Key) 欄位確實也變了:

image

 

而另外一個重設密碼行為,只要使用亂數取得 ASCII 的指令,再強制使用 AppFabric 的 PUT 更新即可,程式碼如下:

public static string resetPassword(IssuerData issuerData)
{
    string newPassword = null;
    Random rnd = new Random();

    // 用亂數取得新密碼字串。
    for (int i = 0; i < 10; i++)

        newPassword += ((char)rnd.Next(97, 122)).ToString();

    rnd = null;

    HttpWebRequest request = WebRequest.Create(https://[service namespace].accesscontrol.windows.net/mgmt/issuers/ + issuerData.IssuerID) as HttpWebRequest;
    HttpWebResponse response = null;
    StreamReader reader = null;
    StreamWriter writer = null;
    string result = null;

    try
    {
        if (string.IsNullOrEmpty(wrap_token))
            wrap_token = getManagementToken("[YOUR_AC_ACCOUNT]", "[YOUR_AC_PASSWORD]");

        request.Method = "PUT";
        request.ContentType = "application/xml";
        request.Headers.Add("Authorization", string.Format("WRAP access_token=\"{0}\"", wrap_token));

        string requestData = string.Format(
            @"
            <Issuer xmlns='
http://schemas.microsoft.com/ws/2009/06/acs/rest/resources'>
              <DisplayName>{0}</DisplayName>
              <Id>{1}</Id>
              <IssuerName>{2}</IssuerName>
              <Security>
                <Algorithm>Symmetric256BitKey</Algorithm>
                <CurrentKey>{3}</CurrentKey>
                <PreviousKey>{3}</PreviousKey>
              </Security>
            </Issuer>
            ", issuerData.DisplayName, issuerData.IssuerID, issuerData.IssuerName, getSHA256Str(newPassword), getSHA256Str(newPassword));

        request.ContentLength = requestData.Length;
        writer = new StreamWriter(request.GetRequestStream());
        writer.Write(requestData);
        writer.Close();

        response = request.GetResponse() as HttpWebResponse;
        reader = new StreamReader(response.GetResponseStream());

        issuersXML = reader.ReadToEnd();

        reader.Close();
        response.Close();

        result = newPassword;
    }
    catch (WebException ex)
    {
        result = "[ERROR_OCCURRED]";
    }
    finally
    {
        request = null;
        response = null;
    }

    return newPassword;
}

當執行上述方法時,若沒有錯誤,會傳回新的密碼字串,此時就可以將它以 email 寄給帳戶所有人。當發生錯誤時,則會回傳 ERROR_OCCURRED 字串。

 

參考資料:

Windows Azure AppFabric SDK: Access Control – Management Service