Identity(四)

本文摘自:ASP.NET MVC 随想录——探索ASP.NET Identity 身份验证和基于角色的受权,中级篇html

探索身份验证与受权web


 

在这一小节中,我将阐述和证实ASP.NET 身份验证和受权的工做原理和运行机制,而后介绍怎样使用Katana Middleware 和 ASP.NET Identity 进行身份验证。数据库

1. 理解ASP.NET 表单身份验证与受权机制 浏览器

谈到身份验证,咱们接触的最多的可能就是表单身份验证(Form-based Authentication)。为了更好的去理解ASP.NET 表单身份验证与受权机制,我搬出几年前的一张旧图,表示HttpApplication 19个事件,它们分别在HttpModule 中被注册,这又被称为ASP.NET 管道(Pipeline)事件。通俗的讲,当请求到达服务器时,ASP.NET 运行时会依次触发这些事件:安全

身份验证故名思义,验证的是用户提供的凭据(Credentials)。一旦验证经过,将产生惟一的Cookie标识并输出到浏览器。来自浏览器的下一次请求将包含此Cookie,对于ASP.NET 应用程序,咱们熟知的FormsAuthenticationModule会对HttpApplication 的管道(Pipeline)事件AuthenticateRequest 进行注册,当请求通过ASP.NET Pipeline时,由ASP.NET Runtime 触发它,在该事件中,它会验证并解析该Cookie为对应的用户对象,它是一个实现了 IPrincipal接口的对象。PostAuthenticateRequest 事件在AuthenticateRequest 事件以后触发,表示用户身份已经检查完成 ,检查后的用户能够经过HttpContextUser属性获取而且HttpContext.User.Identity.IsAuthenticated属性为True。服务器

若是将身份验证看做是"开门"的话,主人邀请你进屋,但这并不意味着你能够进入到卧室或者书房,可能你的活动场所仅限书房——这就是受权。在PostAuthenticateRequest事件触发事后,会触发AuthorizeRequest 事件,它在UrlAuthorizationModule 中被注册(题外插一句:UrlAuthorizationModule 以及上面提到的FormsAuthenticationModule你能够在IIS 级别的.config文件中找到,这也是ASP.NET 和 IIS紧耦合关系的体现)。在该事件中,请求的URL会依据web.config中的authorization 配置节点进行受权,以下所示授予Kim以及全部Role为Administrator的成员具备访问权限,而且拒绝John以及匿名用户访问。mvc

<authorization> 
   <allow users="Kim"/> 
   <allow roles="Administrator"/> 
   <deny users="John"/> 
   <deny users="?"/> 
</authorization>

经过身份验证和受权,咱们能够对应用程序敏感的区域进行受限访问,这确保了数据的安全性。app

2.使用Katana进行身份验证 less

到目前为止,你可能已经对OWIN、Katana 、 Middleware 有了基本的了解,若是不清楚的话,请移步到浏览。async

使用Katana,你能够选择几种不一样类型的身份验证方式,咱们能够经过Nuget来安装以下类型的身份验证:

  • 表单身份验证
  • 社交身份验证(Twitter、Facebook、Google、Microsoft Account…)
  • Windows Azure
  • Active Directory
  • OpenID

其中又以表单身份验证用的最为普遍,正如上面提到的那样,传统ASP.NET MVC 、Web Form 的表单身份验证明际由FormsAuthenticationModule 处理,而Katana重写了表单身份验证,因此有必要比较一下传统ASP.NET MVC & Web Form 下表单身份验证与OWIN下表单身份验证的区别:

Features

ASP.NET MVC & Web Form Form Authentication

OWIN Form Authentication

Cookie Authentication

Cookieless Authentication

×

Expiration

Sliding Expiration

Token Protection

Claims Support

×

Unauthorized Redirection

从上表对比能够看出,Katana几乎实现了传统表单身份验证全部的功能,那咱们怎么去使用它呢?仍是像传统那样在web.config中指定吗?

非也非也,Katana 彻底抛弃了FormsAuthenticationModule,其实是经过Middleware来实现身份验证。默认状况下,Middleware在HttpApplication的PreRequestHandlerExecute 事件触发时链式执行,固然咱们也能够将它指定在特定的阶段执行,经过使用UseStageMarker方法,咱们能够在AuthenticateRequest 阶段执行Middleware 进行身份验证。

那咱们要怎样去实现呢?幸运的是,Katana已经帮助咱们封装好了一个扩展方法,以下所示,

app.UseCookieAuthentication(new CookieAuthenticationOptions 
 { 
           AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, 
           LoginPath = new PathString("/Account/Login") 
  }); 

app.UseCookieAuthentication 是一个扩展方法,它的内部帮咱们作了以下几件事:

  • 使用app.Use(typeof(CookieAuthenticationMiddleware), app, options) 方法,将CookieAuthenticationMiddleware 中间件注册到OWIN Pipeline中
  • 经过app.UseStageMarker(PipelineStage.Authenticate)方法,将前面添加的CookieAuthenticationMiddleware指定在 ASP.NET 集成管道(ASP.NET integrated pipeline)的AuthenticateRequest阶段执行

当调用(Invoke)此Middleware时,将调用CreateHandler方法返回CookieAuthenticationHandler对象,它包含 AuthenticateCoreAsync方法,在这个方法中,读取而且验证Cookie,而后经过AddUserIdentity方法建立ClaimsPrincipal对象并添加到Owin环境字典中,能够经过OwinContext对象Request.User能够获取当前用户。

这是一个典型Middleware中间件使用场景,说白了就是去处理Http请求并将数据存储到OWIN环境字典中进行传递。而CookieAuthenticationMiddleware所作的事其实和FormsAuthenticationModule作的事相似。

那咱们怎么产生Cookie呢?使用ASP.NET Identity 进行身份验证,若是验证经过,产生Cookie并输出到客户端浏览器, 这样一个闭环就造成了,我将在下一小节实施这一步骤。

3.使用Authorize特性进行受权

ASP.NET Identity已经集成到了ASP.NET Framework中,在ASP.NET MVC 中,咱们可使用Authorize 特性进行受权,以下代码所示:

[Authorize] 
public ActionResult Index() 
{ 
    return View(); 
} 

 上述代码中,Index Action 已被设置了受限访问,只有身份验证经过才能访问它,若是验证不经过,返回401.0 – Unauthorized,而后请求在EndRequest 阶段被 OWIN Authentication Middleware 处理,302 重定向到/Account/Login 登陆。

 

使用ASP.NET Identity 身份验证


有了对身份验证和受权机制基本了解后,那么如今就该使用ASP.NET Identity 进行身份验证了。

1. 实现身份验证所需的准备工做

当咱们匿名访问受权资源时,会被Redirect 到 /Account/Login 时,此时的URL结构以下:

http://localhost:60533/Account/Login?ReturnUrl=%2Fhome%2Findex

由于须要登录,因此能够将Login 设置为容许匿名登录,只须要在Action的上面添加 [AllowAnonymous] 特性标签,以下所示:

[AllowAnonymous] 
public ActionResult Login(string returnUrl) 
{ 
    //若是登陆用户已经Authenticated,提示请勿重复登陆 
    if (HttpContext.User.Identity.IsAuthenticated) 
    { 
        return View("Error", new string[] {"您已经登陆!"}); 
    } 
    ViewBag.returnUrl = returnUrl; 
    return View(); 
} 

注意,在这儿我将ReturnUrl 存储了起来,ReturnUrl 顾名思义,当登陆成功后,重定向到最初的地址,这样提升了用户体验。

因为篇幅的限制,Login View 我不将代码贴出来了,事实上它也很是简单,包含以下内容:

  • 用户名文本框
  • 密码框
  • 存储ReturnUrl的隐藏域
  • @Html.AntiForgeryToken(),用来防止CSRF跨站请求伪造

2.添加用户并实现身份验证

当输入了凭据以后,POST Form 表单到/Account/Login 下,具体代码以下:

[HttpPost] 
[AllowAnonymous] 
[ValidateAntiForgeryToken] 
public async Task<ActionResult> Login(LoginModel model,string returnUrl) 
{ 
    if (ModelState.IsValid) 
    { 
        AppUser user = await UserManager.FindAsync(model.Name, model.Password); 
        if (user==null) 
        { 
            ModelState.AddModelError("","无效的用户名或密码"); 
        } 
        else 
        { 
            var claimsIdentity = 
                await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie); 
            AuthManager.SignOut(); 
            AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false}, claimsIdentity); 
            return Redirect(returnUrl); 
        } 
    } 
    ViewBag.returnUrl = returnUrl; 
 
    return View(model); 
} 

上述代码中,首先使用 ASP.NET Identity 来验证用户凭据,这是经过 AppUserManager 对象的FindAsync 方法来实现,若是你不了解ASP.NET Identity 基本API ,请参考这篇文章

AppUser user = await UserManager.FindAsync(model.Name, model.Password); 

FindAsync 方法接受两个参数,分别是用户名和密码,若是查找到,则返回AppUser 对象,不然返回NULL。

若是FindAsync 方法返回AppUser 对象,那么接下来就是建立Cookie 并输出到客户端浏览器,这样浏览器的下一次请求就会带着这个Cookie,当请求通过AuthenticateRequest 阶段时,读取并解析Cookie。也就是说Cookie 就是咱们的令牌, Cookie如本人,咱们没必要再进行用户名和密码的验证了。

使用ASP.NET Identity 产生Cookie 其实很简单,就3行代码,以下所示:

var claimsIdentity =await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie); 
AuthManager.SignOut(); 
AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false}, claimsIdentity); 

对代码稍做分析,第一步建立了用来表明当前登陆用户的ClaimsIdentity 对象,ClaimsIndentity 是 ASP.NET Identity 中的类,它实现了IIdentity 接口。

ClaimsIdentity 对象实际上由AppUserManager 对象的CreateIdentityAsync 方法建立,它须要接受一个AppUser 对象和身份验证类型,在这儿选择ApplicationCookie。

接下来,就是让已存在的Cookie 失效,并产生新Cookie。我预先定义了一个AuthManager 属性,它是IAuthenticationManager 类型的对象,用来作一些通用的身份验证操做。它 包含以下重要的操做:

  • SignIn(options,identity) 故名思意登陆,用来产生身份验证事后的Cookie
  • SignOut() 故名思意登出,让已存在的Cookie 失效

SignIn 须要接受两个参数,AuthenticationProperties 对象和ClaimsIdentity 对象,AuthticationProperties 有众多属性,我在这儿只设置IsPersistent=true ,意味着Authentication Session 被持久化保存,当开启新Session 时,该用户没必要从新验证了。

最后,重定向到ReturnUrl:

return Redirect(returnUrl); 

使用角色进行受权


 在前一小节中,使用了Authorize 特性对指定区域进行受限访问,只有被身份验证经过后才能继续访问。在这一小节将更细粒度进行受权操做,在ASP.NET MVC Framework 中,Authorize 每每结合User 或者 Role 属性进行更小粒度的受权操做,正如以下代码所示:

[Authorize(Roles = "Administrator")] 
public class RoleController : Controller 
{ 
} 

1.使用ASP.NET Identity 管理角色

对Authorize 有了基本的了解以后,将关注点转移到角色Role的管理上来。ASP.NET Identity 提供了一个名为RoleManager<T> 强类型基类用来访问和管理角色,其中T 实现了IRole 接口,IRole 接口包含了持久化Role 最基础的字段(Id和Name)。

Entity Framework 提供了名为IdentityRole 的类,它实现了IRole 接口,因此它不只包含Id、Name属性,还增长了一个集合属性Users。IdentityRole重要的属性以下所示:

Id

定义了Role 惟一的Id

Name

定义了Role的名称

Users

返回隶属于Role的全部成员

我不想在应用程序中直接使用IdentityRole,由于咱们还可能要去扩展其余字段,故定义一个名为AppRole的类,就像AppUser那样,它继承自IdentityRole:

public class AppRole:IdentityRole 
{ 
    public AppRole() : base() { } 
    public AppRole(string name) : base(name) { } 
    // 在此添加额外属性 
} 

同时,再定义一个AppRoleManager 类,如同AppUserManager 同样,它继承RoleManager<T>,提供了检索和持久化Role的基本方法:

public class AppRoleManager:RoleManager<AppRole> 
{ 
    public AppRoleManager(RoleStore<AppRole> store):base(store) 
    { 
    } 
 
    public static AppRoleManager Create(IdentityFactoryOptions<AppRoleManager> options, IOwinContext context) 
    { 
        return new AppRoleManager(new RoleStore<AppRole>(context.Get<AppIdentityDbContext>())); 
    } 
} 

最后,别忘了在OWIN Startup类中初始化该实例,它将存储在OWIN上下文环境字典中,贯穿了每一次HTTP请求:

app.CreatePerOwinContext(AppIdentityDbContext.Create); 
app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create); 
app.CreatePerOwinContext<AppRoleManager>(AppRoleManager.Create); 

2.建立和删除角色

使用ASP.NET Identity 建立和删除角色很简单,经过从OWIN 上下文中获取到AppRoleManager,而后Create 或者 Delete,以下所示:

[HttpPost] 
public async Task<ActionResult> Create(string name) 
{ 
    if (ModelState.IsValid) 
    { 
        IdentityResult result = await RoleManager.CreateAsync(new AppRole(name)); 
        if (result.Succeeded) 
        { 
            return RedirectToAction("Index"); 
        } 
        else 
        { 
            AddErrorsFromResult(result); 
        } 
    } 
    return View(name); 
} 
 
[HttpPost] 
public async Task<ActionResult> Delete(string id) 
{ 
    AppRole role = await RoleManager.FindByIdAsync(id); 
    if (role != null) 
    { 
        IdentityResult result = await RoleManager.DeleteAsync(role); 
        if (result.Succeeded) 
        { 
            return RedirectToAction("Index"); 
        } 
        else 
        { 
            return View("Error", result.Errors); 
        } 
    } 
    else 
    { 
        return View("Error", new string[] { "没法找到该Role" }); 
    } 
}

3.管理角色 MemberShip

要对用户受权,除了建立和删除角色以外,还须要对角色的MemberShip 进行管理,即经过Add /Remove 操做,能够向用户添加/删除角色。

为此,我添加了两个ViewModel,RoleEditModel和RoleModificationModel,分别表明编辑时展现字段和表单 Post时传递到后台的字段:

public class RoleEditModel 
{ 
    public AppRole Role { get; set; } 
    public IEnumerable<AppUser> Members { get; set; } 
    public IEnumerable<AppUser> NonMembers { get; set; } 
} 
public class RoleModificationModel 
{ 
    public string RoleName { get; set; } 
    public string[] IDsToAdd { get; set; } 
    public string[] IDsToDelete { get; set; } 
} 

在对角色进行编辑时,获取全部隶属于Role的成员和非隶属于Role的成员:

/// <summary> 
/// 编辑操做,获取全部隶属于此Role的成员和非隶属于此Role的成员 
/// </summary> 
/// <param name="id"></param> 
/// <returns></returns> 
public async Task<ActionResult> Edit(string id) 
{ 
    AppRole role = await RoleManager.FindByIdAsync(id); 
    string[] memberIDs = role.Users.Select(x => x.UserId).ToArray(); 
    IEnumerable<AppUser> members = UserManager.Users.Where(x => memberIDs.Any(y => y == x.Id)); 
    IEnumerable<AppUser> nonMembers = UserManager.Users.Except(members); 
    return View(new RoleEditModel() 
    { 
        Role = role, 
        Members = members, 
        NonMembers = nonMembers 
    }); 
} 

最终呈现的视图以下所示:

当点击保存,提交表单时,经过模型绑定,将数据Post 到Edit Action,实现了对角色的MemberShip 进行管理,即经过Add /Remove 操做,能够向用户添加/删除角色。

,以下所示:

[HttpPost] 
public async Task<ActionResult> Edit(RoleModificationModel model) 
{ 
    IdentityResult result; 
    if (ModelState.IsValid) 
    { 
        foreach (string userId in model.IDsToAdd??new string[] {}) 
        { 
            result = await UserManager.AddToRoleAsync(userId, model.RoleName); 
            if (!result.Succeeded) 
            { 
                return View("Error", result.Errors); 
            } 
        } 
        foreach (var userId in model.IDsToDelete??new string[] {}) 
        { 
            result = await UserManager.RemoveFromRoleAsync(userId, model.RoleName); 
            if (!result.Succeeded) 
            { 
                return View("Error", result.Errors); 
            } 
        } 
        return RedirectToAction("Index"); 
    } 
    return View("Error",new string[] {"没法找到此角色"}); 
} 

在上述代码中,你可能注意到了UserManager 类,它包含了若干与角色相关的操做方法:

AddToRoleAsync(string userId,string role)

添加用户到指定的角色中

GetRolesAsync(string userId)

获取User对应的角色列表

IsInRoleAsync(string userId,string role)

判断用户是否隶属于指定的角色

RemoveFromRoleAsync(string userId,string role)

将用户从指定角色中排除

 

初始化数据,Seeding 数据库


在上一小节中,经过Authorize 标签将Role 控制器受限访问,只有Role=Administrator的用户才能访问和操做。

[Authorize(Roles = "Administrator")] 
public class RoleController : Controller 
{ 
} 

但当咱们的应用程序部署到新环境时,是没有具体的用户数据的,这就致使咱们没法访问Role Controller。这是一个典型的 "鸡生蛋仍是蛋生鸡"问题。

要解决这个问题,咱们通常是在数据库中内置一个管理员角色,这也是咱们熟知的超级管理员角色。经过Entity Framework Seed,咱们能够轻松实现数据的初始化:

public class IdentityDbInit 
    : DropCreateDatabaseIfModelChanges<AppIdentityDbContext> 
{ 
    protected override void Seed(AppIdentityDbContext context) 
    { 
        PerformInitialSetup(context); 
        base.Seed(context); 
    } 
 
    public void PerformInitialSetup(AppIdentityDbContext context) 
    { 
        // 初始化 
        AppUserManager userMgr = new AppUserManager(new UserStore<AppUser>(context)); 
        AppRoleManager roleMgr = new AppRoleManager(new RoleStore<AppRole>(context)); 
 
        string roleName = "Administrators"; 
        string userName = "Admin"; 
        string password = "Password2015"; 
        string email = "admin@jkxy.com"; 
 
        if (!roleMgr.RoleExists(roleName)) 
        { 
            roleMgr.Create(new AppRole(roleName)); 
        } 
 
        AppUser user = userMgr.FindByName(userName); 
        if (user == null) 
        { 
            userMgr.Create(new AppUser { UserName = userName, Email = email }, 
                password); 
            user = userMgr.FindByName(userName); 
        } 
 
        if (!userMgr.IsInRole(user.Id, roleName)) 
        { 
            userMgr.AddToRole(user.Id, roleName); 
        } 
    } 
} 

在这儿实例化了AppUserManager和AppRoleManager实例,这是由于PerformInitialSetup 方法比OWIN 配置先执行。


小结

在这篇文章中,探索了使用ASP.NET Identity 进行身份验证以及联合ASP.NET MVC 基于角色的受权。最后实现了对角色的管理。在下一篇文章中,继续ASP.NET Identity之旅,探索ASP.NET Identity 的高级应用——基于声明的受权。

相关文章
相关标签/搜索