安全令牌服务 (STS) 是基于 WS-Trust协议构建、签署和颁发安全令牌的服务组件,可处理不同类型凭据的身份验证。
WS-Trust是WS-*规范族中的一员,也是OASIS其中的一项标准,专门处理有关安全tokens的发布,更新和验证,确保各方参与者的互操作处在一个可信任的安全数据交换环境中。
从较高层次看,WS-Trust使用四种服务操作来描述一个约定:颁发、验证、续订和取消。客户端分别调用这些操作来请求安全令牌、验证安全令牌、续订已过期的安全令牌以及取消不应再继续使用的安全令牌。WS-Trust规范定义了每个操作的语法:
请求安全性令牌时:使用 WS-Trust 规范中定义的<RequestSecurityToken> 消息进行请求的。
返回安全性令牌时:使用 WS-Trust 规范中定义的<RequestSecurityTokenResponse> 消息进行返回的。
本章只是通过代码实现STS的基本功能,后面会通过反编译截获生成的RST和RSTS消息文本,可以用来理解颁发的SAML令牌的结构,也可以对令牌进行本地持久化。
Geneva 框架是.NET3.5基础上的,.NET4下发布了新的框架WIF。
Geneva 框架可为开发人员提供相关工具来构建基于声明的应用程序和服务,还提供相关工具来构建自定义STS 和应用程序。使用 Geneva 框架构建自定义 STS,而无需编写用于公开 WS-Trust 终结点或构建包含声明的 SAML 令牌的所有探测功能。
利用Geneva框架开发Server端和Client端非常便利。只要按需继承关键的基类,并实现关键的函数即可。
这里采用IIS托管的WCF服务来发布STS。
WCF页面:
1 <% @ServiceHost language = C# Factory = " STS.Core.STSServiceHostFactory " Service = " STS.Core.STSServiceConfiguration " %>
为了自定义的需求,这里的工厂类和配置类都继承了Geneva框架的基类。
1 using Microsoft.IdentityModel.Protocols.WSTrust;
2 namespace STS.Core
3 {
4 public class STSServiceHostFactory : WSTrustServiceHostFactory
5 {
6 public override ServiceHostBase CreateServiceHost( string constructorString, Uri[] baseAddresses)
7 {
8 ServiceHostBase serviceHost = base .CreateServiceHost(constructorString, baseAddresses);
9 // 可以在这里通过代码增加配置项、终结点、服务行为等
10
11 return serviceHost;
12 }
13 public class STSServiceConfiguration : SecurityTokenServiceConfiguration
14 {
15 public STSServiceConfiguration () : base ()
16 {
17 // 这里可以修改STS的一些默认配置,如令牌的生存期等
18
19 // 配置STS服务实现类
20 this .SecurityTokenService = typeof (STSService);
21 // 设置用STS服务证书做安全令牌的签名证书
22 this .SigningCredentials = new X509SigningCredentials(STSConfiguration.Certificate);
23 }
24 }
25 }
26
可以看到需要一个类STSService,而这个类正是实现WS-Trust的核心,我们也继承自Geneva框架的基类,只需要实现几个比较关键的函数,就可以方便的实现自定义STS服务:
GetIssuerName():获取颁发者名称
GetScope():获取颁发策略
GetOutputClaimsIdentity():获取声明标识的集合,用来生成安全令牌主体
令牌默认生存期是10个小时,如果需要修改令牌生存期,可以实现下面的函数
GetTokenLifetime():获取令牌生存期
令牌是加密的,如果需要公开一些信息,供客户端直接使用,可以实现下面的函数
GetDisplayToken():获取展示令牌
1 using Microsoft.IdentityModel.Protocols.WSTrust;
2 namespace STS.Core
3 {
4 public class STSService : SecurityTokenService
5 {
6 public STSService(SecurityTokenServiceConfiguration configuration)
7 : base (configuration)
8 {
9 }
10
11 /// <summary> 返回颁发者名称 </summary>
12 protected override string GetIssuerName()
13 {
14 // 设置令牌颁发者名称
15 return STSConfiguration.SSO_STS_ISSUER_ADDRESS;
16 }
17
18 /// <summary> 分析令牌请求 </summary>
19 protected override Scope GetScope(IClaimsPrincipal principal, RequestSecurityToken request)
20 {
21 // 分析RST,逻辑直接在这里判断
22 PolicyOptions options = PolicyOptions.Create(request);
23
24 // 地址是否需要验证
25 if ( ! options.IsKnownRealm)
26 {
27 throw new InvalidRequestException( " 请求地址未通过验证 " );
28 }
29 // 返回地址能不能跨域
30 if ( ! options.ReplyToAddressIsWithinRealm)
31 {
32 throw new InvalidRequestException( " 不合法的返回地址 " );
33 }
34 // 令牌需不需要加密
35 if ( ! options.UsesEncryption)
36 {
37 throw new InvalidRequestException( " 没找到加密证书 " );
38 }
39 // 是否需要SSL传输 (passive模式有效)
40 // if (!options.UsesSsl)
41 // {
42 // if (!options.IsActive)
43 // {
44 // throw new InvalidRequestException("需要SSL");
45 // }
46 // }
47 // 构造 scope
48 return new PolicyScope(options, SecurityTokenServiceConfiguration.SigningCredentials);
49 }
50
51 /// <summary>
52 /// 生成安全令牌内容,返回声明标示的集合。
53 /// </summary>
54 protected override IClaimsIdentity GetOutputClaimsIdentity(IClaimsPrincipal principal, RequestSecurityToken request, Scope scope)
55 {
56 ClaimsIdentity outputIdentity = new ClaimsIdentity();
57
58 // 这里开始根据主体的标识信息获取用户的详细信息,并按业务逻辑写入要颁发的安全令牌,主体的标识信息是在身份验证的时候写入的,下面会说明
59 // 用户登录名
60 outputIdentity.Claims.Add( new Claim(System.IdentityModel.Claims.ClaimTypes.Name, principal.Identity.Name, ClaimValueTypes.String));
61 // 也可以写一些自定义的声明,比如
62 // 用户是否是管理员
63 outputIdentity.Claims.Add( new Claim( " http://sts-server/claims/isadmin " , " true " , ClaimValueTypes.Boolean));
64
65 return outputIdentity;
66 }
67
68 /// <summary>
69 /// 可选,生成显示令牌,只有在RST的RequestDisplayToken标记为true时才生成
70 /// </summary>
71 protected override DisplayToken GetDisplayToken( string requestedDisplayTokenLanguage, IClaimsIdentity subject)
72 {
73 var displayClaims = new List < DisplayClaim > ();
74 if (subject.Claims != null )
75 {
76 foreach (var claim in subject.Claims)
77 {
78 switch (claim.ClaimType)
79 {
80 case WSIdentityConstants.ClaimTypes.Name:
81 displayClaims.Add(GetStandardClaim(claim));
82 break ;
83 default :
84 break ;
85 }
86 }
87 }
88 return new DisplayToken(requestedDisplayTokenLanguage, displayClaims);
89 }
90
91 /// <summary> 标准声明类型的DisplayClaim </summary>
92 private DisplayClaim GetStandardClaim(Claim claim)
93 {
94 var displayClaim = DisplayClaim.CreateDisplayClaimFromClaimType(claim.ClaimType);
95 displayClaim.DisplayValue = claim.Value;
96 return displayClaim;
97 }
98 }
99 }
100
其中PolicyScope类也是继承了Geneva框架的基类Scope,用于对RST进行分析,包括:
请求地址是否为空;
验证请求地址并获取RP证书用来加密安全令牌;
请求是否主动模式;
如果是被动模式返回地址是否跨域;
是否启用SSL等。
代码略过。
这样STS的主体部分就完成了,但是还缺少一个类,用于验证客户端身份凭据。
在这里客户端身份验证的方式采用UserName,而这个处理类也可以继承Geneva框架既有的基类:
1 using Microsoft.IdentityModel.Protocols.WSTrust;
2 namespace STS.Core
3 {
4 public class STSUserNameSecurityTokenHandler : UserNameSecurityTokenHandler
5 {
6 /// <summary> 可以进行身份验证 </summary>
7 public override bool CanValidateToken
8 {
9 get
10 {
11 return true ;
12 }
13 }
14
15 /// <summary> 身份验证 </summary>
16 public override ClaimsIdentityCollection ValidateToken(SecurityToken token)
17 {
18 if (token == null )
19 {
20 ArgumentException e = new ArgumentException( " 无效的空令牌 " );
21 throw e;
22 }
23 UserNameSecurityToken usernameToken = token as UserNameSecurityToken;
24 if (usernameToken == null )
25 {
26 ArgumentException e = new ArgumentException( " 无效的UserName令牌 " );
27 throw e;
28 }
29
30 // 声明集合,主要记录身份验证的信息
31 ClaimsIdentityCollection cc = new ClaimsIdentityCollection();
32 IClaimsIdentity identity = new ClaimsIdentity();
33
34 // 验证客户端身份代码……….
35
36 // 将验证结果写入标识的声明集合内,供颁发令牌时使用
37 // 用户名
38 identity.Claims.Add( new Claim(System.IdentityModel.Claims.ClaimTypes.Name, usernameToken.UserName, " DoxtUserNameSecurityTokenHandler " ));
39 // 颁发时间
40 identity.Claims.Add( new Claim( " http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationinstant " , XmlConvert.ToString(DateTime.Now, " yyyy-MM-ddTHH:mm:ssZ " ), " http://www.w3.org/2001/XMLSchema#dateTime " ));
41 cc.Add(identity);
42
43 return cc;
44 }
45
46 /// <summary>
47 /// 拷贝
48 /// </summary>
49 /// <returns></returns>
50 public override SecurityTokenHandler Clone()
51 {
52 return new STSUserNameSecurityTokenHandler();
53 }
54 }
55 }
56
57
58
现在已经完成了Server端的代码,下面是WCF的配置文件,其中服务器证书和自定义的客户端身份验证类并没有在WCF的<behaviors>节点声明,而是放在Geneva框架的节点<microsoft.identityModel>内,需要注意:
1 < configSections >
2 < section name ="microsoft.identityModel" type ="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel, Version=0.6.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
3 </ configSections >
4 < microsoft.identityModel >
5 < service >
6 < securityTokenHandlers >
7 <!-- 自定义Username令牌处理类 -->
8 < remove type ="Microsoft.IdentityModel.Tokens.WindowsUserNameSecurityTokenHandler, Microsoft.IdentityModel, Version=0.6.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
9 < add type ="STS.Core.STSUserNameSecurityTokenHandler, STS.Core" />
10 </ securityTokenHandlers >
11 < serviceCertificate >
12 <!-- 指定服务端证书 -->
13 < certificateReference findValue ="CN=sts-server" storeLocation ="LocalMachine" storeName ="My" />
14 </ serviceCertificate >
15 </ service >
16 </ microsoft.identityModel >
17 < system.serviceModel >
18 < services >
19 < service name ="Microsoft.IdentityModel.Protocols.WSTrust.WSTrustServiceContract" >
20 < endpoint address ="wsHttp"
21 binding ="ws2007HttpBinding" bindingConfiguration ="wsHttpUserName"
22 contract ="Microsoft.IdentityModel.Protocols.WSTrust.IWSTrust13SyncContract" >
23 </ endpoint >
24 < endpoint address ="mex" binding ="mexHttpBinding" contract ="IMetadataExchange" />
25 </ service >
26 </ services >
27 < bindings >
28 < ws2007HttpBinding >
29 < binding name ="wsHttpUserName" >
30 < security mode ="Message" >
31 <!-- 指定使用Username进行客户端身份验证,并且需要建立安全上下文 -->
32 < message clientCredentialType ="UserName" negotiateServiceCredential ="false" establishSecurityContext ="true" />
33 </ security >
34 </ binding >
35 </ ws2007HttpBinding >
36 </ bindings >
37 < behaviors >
38 < serviceBehaviors >
39 < behavior name ="stsBehavior" >
40 < serviceMetadata httpGetEnabled ="false" />
41 < serviceDebug includeExceptionDetailInFaults ="false" />
42 </ behavior >
43 </ serviceBehaviors >
44 </ behaviors >
45 </ system.serviceModel >
46
47