使用OAuth方式保护你的WCF Service(通过Azure ACS)
大家好,今天我们来分享一下在Windows Azure平台上如何保护你部署的WCF Service。
之前我有过一篇文章介绍Azure的Access Control Service(http://blog.csdn.net/aa466564931/article/details/7546415), 这里我们将要介绍如何来使用他保护部署在Azure云计算平台上面的应用程序或者是对外的Service。
在开始介绍之前,先来说明一下我们这里使用的ACS OAuth方式,这里我们用到的是ACS的Service Identities,并且使用Service Identities的密码作为访问WCF service的凭据,客户端只有通过验证(这里将会生成一个基于此密码的OAuth令牌,令牌含有用户名,创建时间,过期时间,验证的唯一标识等信息)才能够访问到我们部署好的service,提供了一种较为安全的方式。
好了,下面开始如何创建这样一个service,当然我们必须要有的Azure的账号(可以申请免费试用的账号),现在Azure还没有在中国正式release,所以注册稍微会麻烦一些,需要一个境外的电话和用于支付的信用卡(信用卡无需是境外的),你可以提供一个香港的电话或者联系微软的support来完成注册。
拥有账号之后请访问(windows.azure.com)访问Azure的管理页面,在这里我们可以部署自己的application, service,提供证书,使用各种服务和功能等等。具体的大家可以边看边试试。这里我们点击“服务总线,访问控制和缓存”按钮,点击访问控制(Access Control),并且创建一个访问控制的命名空间(如果已经创建好了,跳过这一步),最后单击工具栏的访问控制跳转到ACS的配置页面。
在左边的panel中选择Service Identities, 点击添加来创建一个新的indentity,按照描述信息依次键入名称,描述信息。在类型这一栏,我们选择密码方式作为我们的验证的credential, 并且输入你想设置的密码,完了设置有效时期和过期时间,点击保存按钮。这样你在ACS上的配置工作就完成了。
下面我们来做具体的代码编写工作,首先我们创建一个WCF Service,这个service将被ACS保护,并且通过验证才能访问它,为了简便我们这里仅仅创建几个简单的方法,例如获取用户的个人信息,添加新用户等方法,下面贴一段给大家看看,interface和User类的定义就不列举了,非常简单,大家可以通过下载完整sample来看更详细的信息。
[本示例完整源码下载(0分)]http://download.csdn.net/detail/aa466564931/4455947
UserService.cs
public class UserService : IUserService { List<User> users = new List<User>(); /// <summary> /// Add default users. /// </summary> public UserService() { User user = new User(); user.UserId = "1"; user.FirstName = "Jim"; user.LastName = "James"; User user2 = new User(); user2.UserId = "2"; user2.FirstName = "Nancy"; user2.LastName = "Wang"; users.Add(user); users.Add(user2); } /// <summary> /// Return all users. /// </summary> /// <returns></returns> public List<User> GetAllUsers() { return users; } /// <summary> /// Add a new user in user list. /// </summary> /// <param name="u"></param> /// <returns></returns> public bool AddNewUser(User u) { if (!users.Exists(e => e.UserId == u.UserId)) { users.Add(u); return true; } else { return false; } } /// <summary> /// Get user info by id. /// </summary> /// <param name="userId"></param> /// <returns></returns> public User GetUser(string userId) { if (users.Exists(e => e.UserId == userId)) { return users.Find(f => f.UserId == userId); } else { User user = new User(); user.UserId = ""; return user; } } }
OK, Service创建完成了,你可以把它部署到ACS或者是其他Server。下面我们来创建一个安全模块来保护这个Service,这个模块有两个类,一个为SWTModule.cs,他用于设置SWT类型令牌的信任关系,这里需要你吧刚刚在Azure ACS管理页面上设置信息写入了,例如ACS的命名空间,你的密码和acs路径。他是继承与IHttpModule接口的,也就是说在调用Service之前,你需要通过这个HttpModule的验证。另外一个类时TokenValidator,如字面意思,这个类用来检查你的Token的正确性,包括检查你的Token的名字,信任的key,过期时间,SHA-256加密等等. 这两个类是验证功能的核心部分。
SWTMoudle.cs:
class SWTMoudle : IHttpModule { string serviceNamespace = "<Your ACS namespace>"; string acsHostName = "accesscontrol.windows.net"; // Certificate and keys string trustedTokenPolicyKey = "<Your Signing certificate symmetric key>"; // Service Realm string trustedAudience = "<Your ACS service path>"; void IHttpModule.Dispose() { } void IHttpModule.Init(HttpApplication context) { context.BeginRequest += new EventHandler(context_BeginRequest); } void context_BeginRequest(object sender, EventArgs e) { if (HttpContext.Current.Request.Url.AbsolutePath.EndsWith(".svc")) { // Get the authorization header string headerValue = HttpContext.Current.Request.Headers.Get("Authorization"); // Check that a value is there if (string.IsNullOrEmpty(headerValue)) { throw new ApplicationException("unauthorized"); } // Check that it starts with 'WRAP' if (!headerValue.StartsWith("WRAP ")) { throw new ApplicationException("unauthorized"); } string[] nameValuePair = headerValue.Substring("WRAP ".Length).Split(new char[] { '=' }, 2); if (nameValuePair.Length != 2 || nameValuePair[0] != "access_token" || !nameValuePair[1].StartsWith("\"") || !nameValuePair[1].EndsWith("\"")) { throw new ApplicationException("unauthorized"); } // Trim off the leading and trailing double-quotes string token = nameValuePair[1].Substring(1, nameValuePair[1].Length - 2); // Create a token validate TokenValidator validator = new TokenValidator( this.acsHostName, this.serviceNamespace, this.trustedAudience, this.trustedTokenPolicyKey); // Validate the token if (!validator.Validate(token)) { throw new ApplicationException("unauthorized"); } } } }
TokenValidator.cs
public class TokenValidator { private string issuerLabel = "Issuer"; private string expiresLabel = "ExpiresOn"; private string audienceLabel = "Audience"; private string hmacSHA256Label = "HMACSHA256"; private string acsHostName; private string trustedSigningKey; private string trustedTokenIssuer; private string trustedAudienceValue; /// <summary> /// Token validate constructor method. /// </summary> /// <param name="acsHostName"></param> /// <param name="serviceNamespace"></param> /// <param name="trustedAudienceValue"></param> /// <param name="trustedSigningKey"></param> public TokenValidator(string acsHostName, string serviceNamespace, string trustedAudienceValue, string trustedSigningKey) { this.trustedSigningKey = trustedSigningKey; this.trustedTokenIssuer = String.Format("https://{0}.{1}/", serviceNamespace.ToLowerInvariant(), acsHostName.ToLowerInvariant()); this.trustedAudienceValue = trustedAudienceValue; } public bool Validate(string token) { if (!this.IsHMACValid(token, Convert.FromBase64String(this.trustedSigningKey))) { return false; } if (this.IsExpired(token)) { return false; } if (!this.IsIssuerTrusted(token)) { return false; } if (!this.IsAudienceTrusted(token)) { return false; } return true; } public Dictionary<string, string> GetNameValues(string token) { if (string.IsNullOrEmpty(token)) { throw new ArgumentException(); } return token .Split('&') .Aggregate( new Dictionary<string, string>(), (dict, rawNameValue) => { if (rawNameValue == string.Empty) { return dict; } string[] nameValue = rawNameValue.Split('='); if (nameValue.Length != 2) { throw new ArgumentException("Invalid formEncodedstring - contains a name/value pair missing an = character"); } if (dict.ContainsKey(nameValue[0]) == true) { throw new ArgumentException("Repeated name/value pair in form"); } dict.Add(HttpUtility.UrlDecode(nameValue[0]), HttpUtility.UrlDecode(nameValue[1])); return dict; }); } private static ulong GenerateTimeStamp() { // Default implementation of epoch time TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0); return Convert.ToUInt64(ts.TotalSeconds); } private bool IsAudienceTrusted(string token) { Dictionary<string, string> tokenValues = this.GetNameValues(token); string audienceValue; tokenValues.TryGetValue(this.audienceLabel, out audienceValue); if (!string.IsNullOrEmpty(audienceValue)) { if (audienceValue.Equals(this.trustedAudienceValue, StringComparison.Ordinal)) { return true; } } return false; } private bool IsIssuerTrusted(string token) { Dictionary<string, string> tokenValues = this.GetNameValues(token); string issuerName; tokenValues.TryGetValue(this.issuerLabel, out issuerName); if (!string.IsNullOrEmpty(issuerName)) { if (issuerName.Equals(this.trustedTokenIssuer)) { return true; } } return false; } private bool IsHMACValid(string swt, byte[] sha256HMACKey) { string[] swtWithSignature = swt.Split(new string[] { "&" + this.hmacSHA256Label + "=" }, StringSplitOptions.None); if ((swtWithSignature == null) || (swtWithSignature.Length != 2)) { return false; } HMACSHA256 hmac = new HMACSHA256(sha256HMACKey); byte[] locallyGeneratedSignatureInBytes = hmac.ComputeHash(Encoding.ASCII.GetBytes(swtWithSignature[0])); string locallyGeneratedSignature = HttpUtility.UrlEncode(Convert.ToBase64String(locallyGeneratedSignatureInBytes)); return locallyGeneratedSignature == swtWithSignature[1]; } private bool IsExpired(string swt) { try { Dictionary<string, string> nameValues = this.GetNameValues(swt); string expiresOnValue = nameValues[this.expiresLabel]; ulong expiresOn = Convert.ToUInt64(expiresOnValue); ulong currentTime = Convert.ToUInt64(GenerateTimeStamp()); if (currentTime > expiresOn) { return true; } return false; } catch (KeyNotFoundException) { throw new ArgumentException(); } } }
好,创建完Security这块功能之后呢,现在我们要做的就是访问这个service了,这里仅仅为了测试,所以我会把Token的输出出来看看,如果实际运用的话则不能列出来了,因为Token还是包含了很多用户的信息的。
创建一个Silverlight客户端,添加一些基本的控件在MainPage.xaml上:
<Grid x:Name="LayoutRoot" Background="White"> <Grid.RowDefinitions > <RowDefinition Height="100"></RowDefinition> <RowDefinition Height="60"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <Button Content="Click for Token" Height="23" Grid.Row="1" HorizontalAlignment="Left" Name="btnLogin" VerticalAlignment="Top" Width="153" Click="btnToken_Click" /> <TextBox Grid.Row="0" Name="tbToken" HorizontalScrollBarVisibility="Visible" IsReadOnly="True" /> <Button Content="Click for Data" Height="23" HorizontalAlignment="Left" Margin="170,0,0,0" Name="btnData" VerticalAlignment="Top" Width="153" Grid.Row="1" Click="btnData_Click" /> <sdk:Label Grid.Row="1" Height="28" HorizontalAlignment="Left" Margin="1,29,0,0" Name="lbContent" VerticalAlignment="Top" Width="120" /> <sdk:DataGrid AutoGenerateColumns="True" Grid.Row="2" HorizontalAlignment="Left" Name="dtgContent" VerticalAlignment="Top" /> </Grid>
下面的后台代码,调用Service方需要输入你需要访问ACS的地址以及密码等信息:
MainPage.xaml.cs
public partial class MainPage : UserControl { /// <summary> /// Necessary variables from ACS Portal. /// </summary> const string serviceNamespace = "<Your ACS namespace>"; const string acsHostUrl = "accesscontrol.windows.net"; const string realm = "<Your ACS service path>"; const string userUrl = "<The user service path>"; const string uid = "<Your service identity name>"; const string pwd = "<Your service identity password>"; string variables; string tokenString; public MainPage() { InitializeComponent(); } /// <summary> /// Get Token from ACS. /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnToken_Click(object sender, RoutedEventArgs e) { string token = GetTokenFromACS(realm); } /// <summary> /// Access WCF service. /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnData_Click(object sender, RoutedEventArgs e) { if (!tbToken.Text.Trim().Equals(string.Empty)) { HttpWebRequest request = HttpWebRequest.Create(userUrl) as HttpWebRequest; string headerValue = string.Format("WRAP access_token=\"{0}\"", tokenString); request.Method = "GET"; request.Headers["Authorization"] = headerValue; AsyncCallback callBack = new AsyncCallback(LoginGetResponse); request.BeginGetResponse(callBack, request); } else { lbContent.Content = "Please get token first."; } } /// <summary> /// Display data from WCF service. /// </summary> /// <param name="result"></param> public void LoginGetResponse(IAsyncResult result) { HttpWebRequest request = result.AsyncState as HttpWebRequest; HttpWebResponse response = request.EndGetResponse(result) as HttpWebResponse; string obj = string.Empty; using (StreamReader reader = new StreamReader(response.GetResponseStream())) { obj = reader.ReadToEnd(); } XDocument document = XDocument.Parse(obj); var list = from d in document.Descendants("User") select new User { UserId = d.Element("UserId").Value, FirstName = d.Element("FirstName").Value, LastName = d.Element("LastName").Value }; ObservableCollection<User> collection = new ObservableCollection<User>(); foreach (User user in list) { collection.Add(user); } Dispatcher.BeginInvoke(() => { dtgContent.ItemsSource = collection; }); } /// <summary> /// Get Token from ACS portal. /// </summary> /// <param name="scope"></param> /// <returns></returns> private string GetTokenFromACS(string scope) { string wrapPassword = pwd; string wrapUsername = uid; // request a token from ACS string address = string.Format("https://{0}.{1}/WRAPv0.9", serviceNamespace, acsHostUrl); HttpWebRequest requestToken = (HttpWebRequest)HttpWebRequest.Create(address); variables = string.Format("{0}={1}&{2}={3}&{4}={5}", "wrap_name", wrapUsername, "wrap_password", wrapPassword, "wrap_scope", scope); requestToken.Method = "POST"; AsyncCallback callBack = new AsyncCallback(EndGetRequestStream); requestToken.BeginGetRequestStream(callBack, requestToken); return tokenString; } public void EndGetRequestStream(IAsyncResult result) { HttpWebRequest requestToken = result.AsyncState as HttpWebRequest; Stream stream = requestToken.EndGetRequestStream(result); byte[] bytes = Encoding.UTF8.GetBytes(variables); stream.Write(bytes, 0, bytes.Length); stream.Close(); requestToken.BeginGetResponse(TokenEndReponse, requestToken); } public void TokenEndReponse(IAsyncResult result) { HttpWebRequest requestToken = result.AsyncState as HttpWebRequest; HttpWebResponse responseToken = requestToken.EndGetResponse(result) as HttpWebResponse; using(StreamReader reader = new StreamReader(responseToken.GetResponseStream())) { tokenString = reader.ReadToEnd(); } string resultString = HttpUtility.UrlDecode( tokenString .Split('&') .Single(value => value.StartsWith("wrap_access_token=", StringComparison.OrdinalIgnoreCase)) .Split('=')[1]); tokenString = resultString; Dispatcher.BeginInvoke(() => { tbToken.Text = resultString; }); } }
OK, 现在你可以通过启动Silverlight项目来调用Service了,先点击Click for Token 后点击Click for data.
我还创建一个控制台程序来调用它,效果是一样的:
class Program { /// <summary> /// Necessary variables from ACS Portal. /// </summary> static string serviceNamespace = "<Your ACS namespace>"; static string acsHostUrl = "accesscontrol.windows.net"; static string realm = "<Your ACS service path>"; static string userUrl = "<Your user service path>"; static string uid = "<Your service identity name>"; static string pwd = "<Your service identity password>"; /// <summary> /// Access the service via ACS Token. /// </summary> /// <param name="args"></param> static void Main(string[] args) { string token = GetTokenFromACS(realm); WebClient client = new WebClient(); string headerValue = string.Format("WRAP access_token=\"{0}\"", token); HttpWebRequest request = HttpWebRequest.Create(userUrl) as HttpWebRequest; request.ContentLength = 0; request.Method = "GET"; request.Headers["Authorization"] = headerValue; HttpWebResponse response = request.GetResponse() as HttpWebResponse; using (StreamReader reader = new StreamReader(response.GetResponseStream())) { string obj = reader.ReadToEnd(); Console.Write(obj); Console.ReadLine(); } } /// <summary> /// Get Token from ACS. /// </summary> /// <param name="scope"></param> /// <returns></returns> private static string GetTokenFromACS(string scope) { string wrapPassword = pwd; string wrapUsername = uid; // request a token from ACS WebClient client = new WebClient(); client.BaseAddress = string.Format("https://{0}.{1}", serviceNamespace, acsHostUrl); NameValueCollection values = new NameValueCollection(); values.Add("wrap_name", wrapUsername); values.Add("wrap_password", wrapPassword); values.Add("wrap_scope", scope); byte[] responseBytes = client.UploadValues("WRAPv0.9/", "POST", values); string response = Encoding.UTF8.GetString(responseBytes); Console.WriteLine("\nreceived token from ACS: {0}\n", response); return HttpUtility.UrlDecode( response .Split('&') .Single(value => value.StartsWith("wrap_access_token=", StringComparison.OrdinalIgnoreCase)) .Split('=')[1]); } }