ACS有個很有趣的機制,那就是ACS可以設定某個群組的Identity Providers為信任同盟,
文/黃忠成
一個場景
ACS有個很有趣的機制,那就是ACS可以設定某個群組的Identity Providers為信任同盟,
舉例來說,B Mall是一家小型的購物網站,他們擁有自己的使用者資料庫,驗證帳密等動作是透過B Mall自身完成的。
A Mall則是一家大型的購物網站,與B Mall一樣,擁有自己的使用者資料庫,也自己處理了帳密的驗證動作。
A Mall與B Mall談了一個合作案,希望B Mall的使用者可以使用同一組帳密就能登入A Mall來購物,但來自B Mall的使用者僅能在A Mall所限定的區域中進行購物。
這是很常見的情況,很多購物網站都有所謂的某某專區,但問題是,A Mall現在該如何處理這種情況,最直接的答案是把B Mall所有的使用者資料庫匯入A Mall的使用者資料庫中,
但這會有很多問題,例如使用者重複,或是某個使用者被B Mall刪除後,也要通知A Mall做同樣的事,另外,基於保障自己,B Mall自然不希望既有的使用者資料全部被A Mall所掌握,
還有,當合作案中止時,A Mall也要自行刪除這些使用者,所以,匯入B Mall使用者資料至A Mall的做法是短線的手法,會引發很多問題。
透過ACS的信任同盟機制,可以快速且簡單的解決這個問題,如下圖所示。
圖1
在這種情況下,A Mall與B Mall先使用WIF修改自身的Login頁面,令其相容於ACS的信任同盟模式。
B Mall使用者可以透過ACS選擇ABMall的Identity Provider來登入,此時ACS會把使用者導向B Mall的Login頁面,待取得Secure Token後,B Mall的使用者就可以在A Mall及B Mall
進行購物動作,只是受限於A Mall所開放的部分專區。
A Mall的使用者同樣透過ACS登入,此時會轉往A Mall的Login頁面,取得Secure Token後,A Mall的使用者可以自由地在A Mall或是B Mall進行購物。
簡單的說,在不共享使用者帳密的情況下,A Mall與B Mall透過ACS整合成了SSO(單一簽入)的信任同盟。
Creating Custom Identity Providers
要完成上述的任務,A Mall與B Mall的驗證頁面都得進行修改,WIF SDK提供了簡單的Template讓我們完成這個動作。
圖2
先修改Login.aspx.cs,此處只是個範例,所以沒連資料庫來驗證使用者。
//-----------------------------------------------------------------------------
//
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//
//-----------------------------------------------------------------------------
using System;
using System.Web.Security;
using System.Web.UI;
public partial class Login : System.Web.UI.Page
{
protected void Page_Load( object sender, EventArgs e )
{
// Note: Add code to validate user name, password. This code is for illustrative purpose only.
// Do not use it in production environment.
if ( !string.IsNullOrEmpty( txtUserName.Text ) )
{
if (!CheckUser())
{
ClientScript.RegisterStartupScript(typeof(Page), "alert", "alert('wrong user');", true);
return;
}
if ( Request.QueryString["ReturnUrl"] != null )
{
FormsAuthentication.RedirectFromLoginPage( txtUserName.Text, false );
}
else
{
FormsAuthentication.SetAuthCookie( txtUserName.Text, false );
Response.Redirect( "default.aspx" );
}
}
else if ( !IsPostBack )
{
txtUserName.Text = "Adam Carter";
}
}
private bool CheckUser()
{
if (txtUserName.Text == "code6421_amall" && txtPassword.Text == "1234")
return true;
return false;
}
}
接著修改App_Code目錄下的CustomSecurityTokenService.cs。
//…………………………..
protected override Scope GetScope( IClaimsPrincipal principal, RequestSecurityToken request )
{
ValidateAppliesTo( request.AppliesTo );
//
// Note: The signing certificate used by default has a Distinguished name of "CN=STSTestCert",
// and is located in the Personal certificate store of the Local Computer. Before going into production,
// ensure that you change this certificate to a valid CA-issued certificate as appropriate.
//
Scope scope = new Scope( request.AppliesTo.Uri.OriginalString, SecurityTokenServiceConfiguration.SigningCredentials );
string encryptingCertificateName = WebConfigurationManager.AppSettings[ "EncryptingCertificateName" ];
if ( !string.IsNullOrEmpty( encryptingCertificateName ) )
{
// Important note on setting the encrypting credentials.
// In a production deployment, you would need to select a certificate that is specific to the RP that is requesting the token.
// You can examine the 'request' to obtain information to determine the certificate to use.
scope.EncryptingCredentials = new X509EncryptingCredentials( CertificateUtil.GetCertificate( StoreName.My, StoreLocation.LocalMachine, encryptingCertificateName ) );
}
else
{
// If there is no encryption certificate specified, the STS will not perform encryption.
// This will succeed for tokens that are created without keys (BearerTokens) or asymmetric keys.
scope.TokenEncryptionRequired = false;
}
if (!string.IsNullOrEmpty(request.ReplyTo))
{
scope.ReplyToAddress = request.ReplyTo;
}
else
{
scope.ReplyToAddress = scope.AppliesToAddress;
}
// Set the ReplyTo address for the WS-Federation passive protocol (wreply). This is the address to which responses will be directed.
// In this template, we have chosen to set this to the AppliesToAddress.
//scope.ReplyToAddress = scope.AppliesToAddress;
return scope;
}
//……………………………………..
然後修改web.config中的Issuer Name
……… <addkey="IssuerName"value="http://localhost:8028/STSWebSite1/"/> |
這個value需對應FederationMetadata目錄中FederationMetadata.xml的entityID。
完成後進入ACS Portal,添加Identity Provider。
圖3
圖4
此處上傳FederationMetadata中的FederationMetadata.xml,接著修改規則群組。
圖5
接著循A Mall模式建立另一個Identity Provider網站,修改其Login.aspx.cs如下。
//-----------------------------------------------------------------------------
//
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//
//-----------------------------------------------------------------------------
using System;
using System.Web.Security;
using System.Web.UI;
public partial class Login : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
// Note: Add code to validate user name, password. This code is for illustrative purpose only.
// Do not use it in production environment.
if (!string.IsNullOrEmpty(txtUserName.Text))
{
if (!CheckUser())
{
ClientScript.RegisterStartupScript(typeof(Page), "alert", "alert('wrong user');", true);
return;
}
if (Request.QueryString["ReturnUrl"] != null)
{
FormsAuthentication.RedirectFromLoginPage(txtUserName.Text, false);
}
else
{
FormsAuthentication.SetAuthCookie(txtUserName.Text, false);
Response.Redirect("default.aspx");
}
}
else if (!IsPostBack)
{
txtUserName.Text = "Adam Carter";
}
}
private bool CheckUser()
{
if (txtUserName.Text == "code6421_bmall" && txtPassword.Text == "1234")
return true;
return false;
}
}
同樣的,需修改App_Code中的CustomSecurityTokenService.cs及web.config中的IssuerName,然後上傳FederationMetadata.xml至ACS。
修改ACS中的信賴憑證者應用程式設定如下。
規則群組改成下面這樣。
圖7
到此,ACS的信任同盟設定就完成了。
Creating Secure WCF Service
接下來,我們修改A Mall的WCF Service,讓其對B Mal使用者做出限制。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.ServiceModel.Activation;
using Microsoft.IdentityModel.Claims;
using System.Web;
namespace WebApplication15
{
// NOTE: You can use the "Rename" command on the "Refactor" menu to change the class name "Service1" in code, svc and config file together.
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
public class Service1 : IService1
{
private bool IsAuthenticated()
{
return HttpContext.Current.User.Identity.IsAuthenticated;
}
public string HelloWorld()
{
if (IsAuthenticated())
return "hello world.";
else
throw new Exception("Error.");
}
public string HelloWorldAMallOnly()
{
if (IsAuthenticated())
{
IClaimsPrincipal principal = (IClaimsPrincipal)HttpContext.Current.User;
string provider = ((IClaimsIdentity)principal.Identity).Claims.FirstOrDefault(
c => c.ClaimType == "http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider").Value;
if(provider.Contains("STSWebSite1"))
return "hello world.";
throw new Exception("Error.");
}
else
throw new Exception("Error.");
}
}
}
對於B Mall的WCF Service也做同樣的修改,但沒有對A Mall使用者作出使用限制。
OK,我知道,現在可能有點亂,讓我們整理一下。
- A Mall與B Mall都擁有自己的應用程式,使用者可以透過這個應用程式來進行購物,其後端為WCF Service。
- 現在,A Mall與B Mall形成同盟,所以A Mall的使用者可以透過B Mall的應用程式,此時登入時,ACS會將A Mall使用者導向A Mall的Login頁面。
- 當A Mall登入後,可以完全正常的使用B Mall的應用程式。
- B Mall使用者除了可用原來的B Mall應用程式進行購物外,也能使用A Mall的應用程式進行購物,此時ACS會把B Mall使用者導向B Mall的登入頁面,登入成功後,
B Mall的使用者可以正常的使用A Mall的應用程式,但受限於後端的WCF Service,所以只能使用一小部分。
5. 在正常情況下, 你應該會有兩個WCF Secure Service , 一個是A Mall的,一個是BMall的.
6. 在正常情況下, 你應該會有兩個Windows Phone應程式,一個是for AMall, 一個是for BMall,分別呼叫不同的WCF Service
我們用先前的DemoACS(Windows Phone)來測試,下面是修改後的程式碼。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using Microsoft.Phone.Controls;
using SL.Phone.Federation.Utilities;
using System.ServiceModel;
using System.ServiceModel.Channels;
namespace DemoACS
{
public partial class MainPage : PhoneApplicationPage
{
RequestSecurityTokenResponseStore _rstrStore = null;
// Constructor
public MainPage()
{
InitializeComponent();
_rstrStore = (RequestSecurityTokenResponseStore)App.Current.Resources["rstrStore"];
}
private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
{
if (!_rstrStore.ContainsValidRequestSecurityTokenResponse())
{
NavigationService.Navigate(new Uri("/SignInControl.xaml", UriKind.Relative));
}
else
{
ServiceReference1.Service1Client client = new ServiceReference1.Service1Client();
var store = App.Current.Resources["rstrStore"] as SL.Phone.Federation.Utilities.RequestSecurityTokenResponseStore;
using (OperationContextScope scope = new OperationContextScope(client.InnerChannel))
{
var httpRequestProperty = new HttpRequestMessageProperty();
httpRequestProperty.Headers[System.Net.HttpRequestHeader.Authorization] = "OAuth " + store.SecurityToken;
OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = httpRequestProperty;
client.HelloWorldCompleted += (s, args) =>
{
Dispatcher.BeginInvoke(() =>
{
MessageBox.Show(args.Result);
});
};
client.HelloWorldAsync();
}
textBlock1.Text = "thanks for your login";
}
}
private void button1_Click(object sender, RoutedEventArgs e)
{
ServiceReference1.Service1Client client = new ServiceReference1.Service1Client();
var store = App.Current.Resources["rstrStore"] as SL.Phone.Federation.Utilities.RequestSecurityTokenResponseStore;
using (OperationContextScope scope = new OperationContextScope(client.InnerChannel))
{
var httpRequestProperty = new HttpRequestMessageProperty();
httpRequestProperty.Headers[System.Net.HttpRequestHeader.Authorization] = "OAuth " + store.SecurityToken;
OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = httpRequestProperty;
client.HelloWorldAMallOnlyCompleted += (s, args) =>
{
if (args.Error != null)
{
Dispatcher.BeginInvoke(() =>
{
MessageBox.Show(args.Error.Message);
});
return;
}
Dispatcher.BeginInvoke(() =>
{
MessageBox.Show(args.Result);
});
};
client.HelloWorldAMallOnlyAsync();
}
}
}
}
執行程式,會出現如下圖的選擇頁面。
圖8
當使用A Mall登入時,按下按鈕會正常的呼叫HelloWorldAsync。
圖9
當使用B Mall登入時,按下按鈕會出現錯誤,因為B Mall使用者被限制不能呼叫HelloWorldAsync。
圖10