IAbpSession
在AbpController中,通过属性注入的方式注入IAbpSession对象。
IAbpSession的实例ClaimsAbpSession。
public class ClaimsAbpSession : IAbpSession, ISingletonDependency { public virtual long? UserId { get { var userIdClaim = PrincipalAccessor.Principal?.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier); if (string.IsNullOrEmpty(userIdClaim?.Value)) { return null; } long userId; if (!long.TryParse(userIdClaim.Value, out userId)) { return null; } return userId; } } public virtual int? TenantId { get { if (!MultiTenancy.IsEnabled) { return MultiTenancyConsts.DefaultTenantId; } var tenantIdClaim = PrincipalAccessor.Principal?.Claims.FirstOrDefault(c => c.Type == AbpClaimTypes.TenantId); if (string.IsNullOrEmpty(tenantIdClaim?.Value)) { return null; } return Convert.ToInt32(tenantIdClaim.Value); } }
PrincipalAccessor.Principal指的是ClaimsPrincipal类型的对象。该对象获得的方式如下:
public virtual ClaimsPrincipal Principal => Thread.CurrentPrincipal as ClaimsPrincipal;
public static IPrincipal CurrentPrincipal { [System.Security.SecuritySafeCritical] // auto-generated get { lock (CurrentThread) { IPrincipal principal = (IPrincipal) CallContext.Principal; if (principal == null) { principal = GetDomain().GetThreadPrincipal(); CallContext.Principal = principal; } return principal; } } [System.Security.SecuritySafeCritical] // auto-generated [SecurityPermissionAttribute(SecurityAction.Demand, Flags=SecurityPermissionFlag.ControlPrincipal)] set { CallContext.Principal = value; } }
Thread.CurrentPrincipal通过CallContext.Principal或GetDomain().GetThreadPrincipal(). 获得IPrincipal对象。
无论通过哪种方式,该IPrincipal对象,都是从请求上下文获得的。
因此:IAbpSession保存的值是从请求上下文获得的,应用程序必然在某个时刻,将值保存到请求上下文中。这一步是通过OWIN实现的。
与IAbpSession相关的OWIN
在MVC项目App_Start文件夹添加Startup文件。
public class Startup { public void Configuration(IAppBuilder app) { app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/"), CookieSecure = CookieSecureOption.Never, }); }
这一步的作用是向请求管道注册了一个事件。该事件在请求处理的Authenticate阶段执行,执行的内容是获得请求上下文的cookie(string字符串),经过解码和解密,将键值对保存成多个Claim类型的对象,并将这些Claim对象集合保存到ClaimsIdentity类型的对象Claims属性中。
ClaimsIdentity类型中的Claims属性:public virtual IEnumerable<Claim> Claims { get; }
ClaimsIdentity类型的对象又被上下文所引用。
因此可理解:Thread.CurrentPrincipal as ClaimsPrincipal 即是指这个解密出来的ClaimsIdentity对象。
如何将用户信息保存到上下文,并加密到cookie
AuthenticationManager类型
internal class AuthenticationManager : IAuthenticationManager
为控制器添加如下属性:
private IAuthenticationManager AuthenticationManager { get { return HttpContext.GetOwinContext().Authentication; } }
HttpContext.GetOwinContext().Authentication返回的即是AuthenticationManager类型的对象;
该类型有两个方法,负责登录和登出。
public void SignIn(AuthenticationProperties properties, params ClaimsIdentity[] identities)
public void SignOut(AuthenticationProperties properties, string[] authenticationTypes)
SignIn和SignOut还有分别有一个重载,只是 AuthenticationProperties properties参数是new AuthenticationProperties();
在这里,只要为SignIn方法成功传入一个对象ClaimsIdentity即可能登录成功。但如何构建ClaimsIdentity对象。
微软的Identity实现:
Microsoft.AspNet.Identity.Core程序集下UserManager<TUser, TKey>实现如下方法。
public virtual Task<ClaimsIdentity> CreateIdentityAsync(TUser user, string authenticationType)
TUser需实现IUser<out Tkey>接口,该接口包含Id和UserName两个属性。
public interface IUser<out TKey> { TKey Id { get; } string UserName { get; set; } }
因此可以理解为:用户输入用户名和密码→应用程序向数据库查询该用户,返回一个实现IUser接口的对象→调用UserManager.CreateIdentityAsync方法,将IUser对象传入方法中→该方法返回一个ClaimsIdentity对象→将该对象传给AuthenticationManager.SignIn方法。
SignIn会写入到上下文并在某个“时间地点”加密、编码成cookie. 反过来讲,SignOut会清除上下文中的ClaimsIdentity对象。
自定义构建ClaimsIdentity对象:
仿照UserManager.CreateIdentityAsync内的代码,如下写法:
在这里,创建ClaimsIdentityFactory对象仅仅只为使用其内的常量字符串,并无其它意义,不能修改替换ClaimsIdentityFactory内的常量字符串,否则会出错。(new Claim时构造函数传入的factory的几个属性均是常量字符串)
IdentityProviderClaimType和DefaultIdentityProviderClaimValue 也均为ClaimsIdentityFactory内的internal修饰的常量字符串,因在本处引用不到,所以拿出来了。
user = new Entitys.User { Id = 123, TenantId = 1, Email = "[email protected]", UserName = "admin" }; var factory = new Microsoft.AspNet.Identity.ClaimsIdentityFactory<User, long>(); string IdentityProviderClaimType = "http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider"; string DefaultIdentityProviderClaimValue = "ASP.NET Identity"; ClaimsIdentity id = new ClaimsIdentity(DefaultAuthenticationTypes.ApplicationCookie, factory.UserNameClaimType, factory.RoleClaimType); id.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(), ClaimValueTypes.String)); id.AddClaim(new Claim(factory.UserNameClaimType, user.UserName, ClaimValueTypes.String)); id.AddClaim(new Claim(IdentityProviderClaimType, DefaultIdentityProviderClaimValue, ClaimValueTypes.String)); id.AddClaim(new Claim(AbpClaimTypes.TenantId, user.TenantId.ToString(CultureInfo.InvariantCulture))); AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie); AuthenticationManager.SignIn(new AuthenticationProperties { IsPersistent = rememberMe }, id);
如上所示:应用程序就可以实现登录了。登录后就可以访问[Authorize]特性控制的Action。如果调用SignOut方法,即登出,访问[Authorize]特性控制的Action将需要登录。
简要说明的是,在Abp工作单元中也实现了IAbpSession属性,从而得到TenantId的值,以致能在查询中自动添加TenantId条件过滤。
OWIN学习参考博文:
http://www.cnblogs.com/jesse2013/p/aspnet-identity-claims-based-authentication-and-owin.html
分享一个Asp.net 源码网站,同时感谢介绍给我的那位好人。
https://referencesource.microsoft.com/#q=Identity
需要参考的源码:
katana项目,包含Owin相关几个程序集源码。
https://katanaproject.codeplex.com/SourceControl/latest#README
Identity源码:
https://github.com/aspnet/Identity