[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();
}
程式執行的結果如下:
與 AppFabric 的帳戶資料庫比對,密碼 (Key) 欄位確實也變了:
而另外一個重設密碼行為,只要使用亂數取得 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