不好意思,最近一段时间真是太忙了,导致一直没有更新,有好几位朋友都表示让我赶紧接着往下写,偶心里真是挺感动~~
好了,经过前两篇文章,估计大家对WIF已经有比较充分的认识了,估计大家在经过了枯燥的理论以后,肯定是摩拳擦掌赶紧付诸于行动了。没办法,咱们程序员就是这个毛病。那好吧,我也不那么多废话了,直接进入正题吧。
我们接下来的demo将包括以下的工程:
以管理员身份打开vs2012,在起始页上点击“新建项目”,在左边的“模板”树下,展开“其它项目类型”,然后选择“Visual Studio解决方案”,“名称”输入框里输入WIFSSO,然后选择解决方案的路径后点击”确定“,如图:
在”解决方案资源管理器“中,在新建好的解决方案上点右键,选择”添加“->”新建项目“。在弹出的对话框中选择”ASP.NET MVC 4 Web应用程序“,记得.Net Framework版本选4.5,名称起名为”SiteA“,然后点确定,如图:
在弹出的“新ASP.NET MVC 4项目”对话框中直接点“确定”,第一个RP项目新建完成后,添加以下两个引用:System.IdentityModel和System.IdentityModel.Services。这次的教程不使用Identity and Access Tool,而是直接修改web.config文件,这样能使大家对WIF的配置有更深入的了解。
打开web.config文件,将configSections节里的entityFramework配置节点删掉,因为我们不需要用到Entity Framework。最好把web.config中关于Entity Framework相关的配置全都删掉,因为我们都用不上。然后加上以下这两个节点:
我来详细解释一下这些节点的意义。audienceUris指定了一组可以被RP接受的身份标识URI,只有这些配置中的URI范围内的令牌才可以被接受。这里,我把siteA配置在这里。trustedIssuers就是受信任的发行者,由于我们这个demo没有用到SSL,所以这里我指定的thumbprint是IIS Express的指纹,这个指纹在哪里可以获得呢?打开IIS管理器,在左侧树点击根节点,然后在“功能视图”里双击“服务器证书",如下图:
在打开的证书列表里,找到IIS Express Development Certificate,双击,在弹出的”证书“对话框中点击“详细信息”页签,找到“指纹”然后点击,把框里的指纹拷下来,全都改成大写后粘贴到thumbnail的值里去:
接下来配置federationConfiguration节点,它表示配置WSFederationAuthenticationModule (WSFAM) 和SessionAuthenticationModule (SAM) 时使用联合身份验证通过的 WS 联合身份验证协议。这里我们使用WS 联合身份验证的身份验证模块 (WSFAM),关于该节点的详细配置信息,请参考:http://msdn.microsoft.com/zh-cn/library/office/apps/hh568665.aspx
好,这样一来,SiteA的配置就已经完成了,然后我们来加点代码。
打开/Views/Home/Index.cshtml,将原有的代码删掉,改为如下代码:
@using System.Security.Claims
@{
ViewBag.Title = "SiteA主页";
ClaimsIdentity ci = User.Identity as ClaimsIdentity;
if(ci!=null)
{
@ci.FindFirst(ClaimTypes.Name).Value
@ci.FindFirst(ClaimTypes.Email).Value
}
}
退出
至此,SiteA就已经完成了。你是不是迫不及待的想要运行了呢?别急,虽然有SiteA了,但还没有STS呢,现在启动SiteA,由于没登录,所以它会跳转到STS,但STS还不存在,所以会出错的。
接下来我们来创建STS,在解决方案上新建项目,新建一个名为STS的MVC 4应用程序,.Net Framework选择4.5,项目模板选择“Internet应用程序",确定。
添加System.IdentityModel和System.IdentityModel.Services这两个引用,打开web.config,为forms节点添加两个属性:
在AppSettings里增加如下三个节点:
同样禁止匿名用户访问:
public class CertificateUtil
{
public static X509Certificate2 GetCertificate(StoreName name, StoreLocation location, string subjectName)
{
X509Store store = new X509Store(name, location);
X509Certificate2Collection certificates = null;
store.Open(OpenFlags.ReadOnly);
try
{
X509Certificate2 result = null;
certificates = store.Certificates;
for (int i = 0; i < certificates.Count; i++)
{
X509Certificate2 cert = certificates[i];
if (cert.SubjectName.Name.ToLower() == subjectName.ToLower())
{
if (result != null)
throw new ApplicationException(string.Format("subject Name {0}存在多个证书", subjectName));
result = new X509Certificate2(cert);
}
}
if (result == null)
{
throw new ApplicationException(string.Format("没有找到用于 subject Name {0} 的证书", subjectName));
}
return result;
}
finally
{
if (certificates != null)
{
for (int i = 0; i < certificates.Count; i++)
{
certificates[i].Reset();
}
}
store.Close();
}
}
}
创建新类,名为Common,存放几个常量:
public class Common
{
public const string IssuerName = "IssuerName";
public const string SigningCertificateName = "SigningCertificateName";
public const string EncryptingCertificateName = "EncryptingCertificateName";
}
public class SingleSignOnManager
{
const string SITECOOKIENAME = "StsSiteCookie";
const string SITENAME = "StsSite";
///
/// Returns a list of sites the user is logged in via the STS
///
///
public static string[] SignOut()
{
if (HttpContext.Current != null &&
HttpContext.Current.Request != null &&
HttpContext.Current.Request.Cookies != null
)
{
HttpCookie siteCookie =
HttpContext.Current.Request.Cookies[SITECOOKIENAME];
if (siteCookie != null)
return siteCookie.Values.GetValues(SITENAME);
}
return new string[0];
}
public static void RegisterRP(string SiteUrl)
{
if (HttpContext.Current != null &&
HttpContext.Current.Request != null &&
HttpContext.Current.Request.Cookies != null
)
{
// get an existing cookie or create a new one
HttpCookie siteCookie =
HttpContext.Current.Request.Cookies[SITECOOKIENAME];
if (siteCookie == null)
siteCookie = new HttpCookie(SITECOOKIENAME);
siteCookie.Values.Add(SITENAME, SiteUrl);
HttpContext.Current.Response.AppendCookie(siteCookie);
}
}
}
public class CustomSecurityTokenService : SecurityTokenService
{
private readonly SigningCredentials signingCreds;
private readonly EncryptingCredentials encryptingCreds;
public CustomSecurityTokenService(SecurityTokenServiceConfiguration config)
: base(config)
{
this.signingCreds = new X509SigningCredentials(
CertificateUtil.GetCertificate(StoreName.My, StoreLocation.LocalMachine, WebConfigurationManager.AppSettings[Common.SigningCertificateName]));
if (!string.IsNullOrWhiteSpace(WebConfigurationManager.AppSettings[Common.EncryptingCertificateName]))
{
this.encryptingCreds = new X509EncryptingCredentials(
CertificateUtil.GetCertificate(StoreName.My, StoreLocation.LocalMachine, WebConfigurationManager.AppSettings[Common.EncryptingCertificateName]));
}
}
///
/// 此方法返回要发布的令牌内容。内容由一组ClaimsIdentity实例来表示,每一个实例对应了一个要发布的令牌。当前Windows Identity Foundation只支持单个令牌发布,因此返回的集合必须总是只包含单个实例。
///
/// 调用方的principal
/// 进入的 RST,我们这里不用它
/// 由之前通过GetScope方法返回的范围
///
protected override ClaimsIdentity GetOutputClaimsIdentity(ClaimsPrincipal principal, RequestSecurityToken request, Scope scope)
{
//返回一个默认声明集,里面了包含自己想要的声明
//这里你可以通过ClaimsPrincipal来验证用户,并通过它来返回正确的声明。
string identityName = principal.Identity.Name;
string[] temp = identityName.Split('|');
ClaimsIdentity outgoingIdentity = new ClaimsIdentity();
outgoingIdentity.AddClaim(new Claim(ClaimTypes.Email, temp[0]));
outgoingIdentity.AddClaim(new Claim(ClaimTypes.DateOfBirth, temp[1]));
outgoingIdentity.AddClaim(new Claim(ClaimTypes.Name, temp[2]));
SingleSignOnManager.RegisterRP(scope.AppliesToAddress);
return outgoingIdentity;
}
///
/// 此方法返回用于令牌发布请求的配置。配置由Scope类表示。在这里,我们只发布令牌到一个由encryptingCreds字段表示的RP标识 ///
///
///
///
protected override Scope GetScope(ClaimsPrincipal principal, RequestSecurityToken request)
{
// 使用request的AppliesTo属性和RP标识来创建Scope
Scope scope = new Scope(request.AppliesTo.Uri.AbsoluteUri, this.signingCreds);
if (Uri.IsWellFormedUriString(request.ReplyTo, UriKind.Absolute))
{
if (request.AppliesTo.Uri.Host != new Uri(request.ReplyTo).Host)
scope.ReplyToAddress = request.AppliesTo.Uri.AbsoluteUri;
else
scope.ReplyToAddress = request.ReplyTo;
}
else
{
Uri resultUri = null;
if (Uri.TryCreate(request.AppliesTo.Uri, request.ReplyTo, out resultUri))
scope.ReplyToAddress = resultUri.AbsoluteUri;
else
scope.ReplyToAddress = request.AppliesTo.Uri.ToString();
}
if (this.encryptingCreds != null)
{
// 如果STS对应多个RP,要选择证书指定到请求令牌的RP,然后再用 encryptingCreds
scope.EncryptingCredentials = this.encryptingCreds;
}
else
scope.TokenEncryptionRequired = false;
return scope;
}
}
public class CustomSecurityTokenServiceConfiguration : SecurityTokenServiceConfiguration
{
private static readonly object syncRoot = new object();
private const string CustomSecurityTokenServiceConfigurationKey = "CustomSecurityTokenServiceConfigurationKey";
public CustomSecurityTokenServiceConfiguration()
: base(WebConfigurationManager.AppSettings[Common.IssuerName])
{
this.SecurityTokenService = typeof(CustomSecurityTokenService);
}
public static CustomSecurityTokenServiceConfiguration Current
{
get
{
HttpApplicationState app = HttpContext.Current.Application;
CustomSecurityTokenServiceConfiguration config = app.Get(CustomSecurityTokenServiceConfigurationKey) as CustomSecurityTokenServiceConfiguration;
if (config != null)
return config;
lock (syncRoot)
{
config = app.Get(CustomSecurityTokenServiceConfigurationKey) as CustomSecurityTokenServiceConfiguration;
if (config == null)
{
config = new CustomSecurityTokenServiceConfiguration();
app.Add(CustomSecurityTokenServiceConfigurationKey, config);
}
return config;
}
}
}
}
public ActionResult Index()
{
FederatedPassiveSecurityTokenServiceOperations.ProcessRequest(
System.Web.HttpContext.Current.Request,
User as ClaimsPrincipal,
CustomSecurityTokenServiceConfiguration.Current.CreateSecurityTokenService(),
System.Web.HttpContext.Current.Response);
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model, string returnUrl)
{
var query = HttpUtility.ParseQueryString(Request.UrlReferrer.Query);
if (model.UserName == "[email protected]" && model.Password == "123456")
{
FormsAuthentication.SetAuthCookie("[email protected]|1983-10-22|oujian", false);
if (!string.IsNullOrEmpty(returnUrl))
return Redirect(returnUrl);
return RedirectToAction("Index", "Home");
}
return View(model);
}
public ActionResult LogOff()
{
FormsAuthentication.SignOut();
ViewData["AddressesExpected"] = SingleSignOnManager.SignOut().Distinct().ToArray();
return View("Login");
}
@{
ViewBag.Title = "登录";
var addressesExpected = ViewData["AddressesExpected"] as string[];
if (addressesExpected != null)
{
foreach (var address in addressesExpected)
{
}
}
}
OK,至此STS也已经完成了。把SiteA和STS都部署到IIS上,然后打开C:\Windows\System32\Drivers\etc\hosts文件,添加几个站点:
127.0.0.1 www.sitea.com
127.0.0.1 www.siteb.com
127.0.0.1 www.sitec.com
127.0.0.1 www.sited.com
127.0.0.1 www.sts.com
好了,在浏览器输入www.sitea.com,看看如何,它马上跳转到了www.sts.com的登录页面,输入[email protected],密码123456,确定,登录成功,跳回到了www.sitea.com,并显示出了用户名和Email:
点击退出,将注销当前用户,并跳转到登录页。
system.webServer里添加以下三个modules:
@using Microsoft.IdentityModel.Claims
@{
ViewBag.Title = "SiteB主页";
ClaimsIdentity ci = User.Identity as ClaimsIdentity;
if(ci!=null)
{
@ci.Claims.SingleOrDefault(c=>c.ClaimType == ClaimTypes.Email).Value
@ci.Claims.SingleOrDefault(c=>c.ClaimType == ClaimTypes.DateOfBirth).Value
}
}
退出
///
/// This SampleRequestValidator validates the wresult parameter of the
/// WS-Federation passive protocol by checking for a SignInResponse message
/// in the form post. The SignInResponse message contents are verified later by
/// the WSFederationPassiveAuthenticationModule or the WIF signin controls.
///
public class SampleRequestValidator : RequestValidator
{
protected override bool IsValidRequestString(HttpContext context, string value, RequestValidationSource requestValidationSource, string collectionKey, out int validationFailureIndex)
{
validationFailureIndex = 0;
if (requestValidationSource == RequestValidationSource.Form && collectionKey.Equals(WSFederationConstants.Parameters.Result, StringComparison.Ordinal))
{
return true;
}
return base.IsValidRequestString(context, value, requestValidationSource, collectionKey, out validationFailureIndex);
}
}