您的位置:首页 > 编程语言 > ASP

坎坷路:ASP.NET 5 Identity 身份验证(上集)

2016-01-05 15:39 761 查看


之所以为上集,是因为我并没有解决这个问题,写这篇博文的目的是纪录一下我所遇到的问题,以免自己忘记,其实已经忘了差不多了,写的过程也是自己回顾的过程,并且之前收集有关 ASP.NET 5 身份验证的书签已经太多了,所以必须记录下来。

在前年(2014-12-10),我写了这篇博文《爱与恨的抉择:ASP.NET 5+EntityFramework 7》,背景是我当时打算用 ASP.NET 5 重写一个 Web 项目,因为那时候 ASP.NET 5 刚发布不久(之前叫 vNext),所以当时抱了很大的激情投入在上面,但最后的结果是给自己浇了一盆冷水,放弃的原因文章中已经总结了,关于为啥放弃 ASP.NET 5,就是因为身份验证的问题,现在时间过去一年多了,现在回过头来看,其实还是蛮有意思的,比如下面我说一个。


其实最后我想要的功能是不绑定 DbContext,在 ASP.NET 5 项目中,只进行判断操作,身份验证在另外服务中进行,然后在本项目中可以实现类似 FormsAuthentication.SetAuthCookie 操作就可以了,但最后做了几个 Demo 都不能实现,规定的一天时间,已经用完了,所以。。。


上面我前年想要实现的想法,其实我现在也在做这个工作,但中间已经过去一年多时间了,最后还是没有实现。

登录系统是一个独立的站点,这是一个老的项目,身份验证使用的是 Forms Authentication,因为涉及到其它站点,所以不能把登录系统的身份验证改写为 Claims-based 或者 OAuth,这就意味着你需要让其它站点的身份验证方式,来兼容 Forms Authentication,登录系统独立的好处是,其它站点不需要管理用户的登录和注销功能,只需要判断用户有没有通过身份验证即可,就像我当时说的一样,我只需要进行判断操作,但最后做了很多 Demo 研究,还是实现不了,现在回过头来看,当时如果实现了才真是见鬼了,因为 ASP.NET 5 根本就不支持 Forms Authentication(后面详细说),所以,懂得放弃也是好事,毕竟时间是宝贵的。

后来,那个 Web 项目放弃使用 ASP.NET 5 + EF 7,然后用 ASP.NET MVC 5 + EF 6 重写完成了,但心里面还是很不甘心,其实在当时我并不是很懂 ASP.NET Identity 身份验证,所以也导致浪费了很多时间,后来花了点时间重新学习了 ASP.NET Identity,也就是记录的这篇博文《跌倒了,再爬起来:ASP.NET 5 Identity》,这篇博文的主要内容是查看 ASP.NET 5 Identity 的源码,然后抛弃 ApplicationDbContext、UserManager、SignInManager 等等,直接实现用户的登录操作,并且成功实现验证,看到博文最后,你会发现 ASP.NET Identity 和之前的 Forms Authentication 还是有很多不同的,但都是基于 Cookie 加密的方式,下面看三段代码:

Forms Authentication 方式登录:

System.Web.Security.FormsAuthentication.SetAuthCookie("xishuai", false);

ASP.NET Identity 方式登录(截止 2015-01-11):

var userId = await UserManager.GetUserIdAsync(user, cancellationToken);
Context.Response.SignIn(StoreTwoFactorInfo(userId, loginProvider));

ASP.NET Identity 方式登录(最新,来自 SignInManager.cs):

var userId = await UserManager.GetUserIdAsync(user);
await Context.Authentication.SignInAsync(Options.Cookies.TwoFactorUserIdCookieAuthenticationScheme, StoreTwoFactorInfo(userId, loginProvider));

首先,ASP.NET Identity 和 Forms Authentication 都是通过把用户信息加密后,放入响应头的 Cookie 中,只不过两种 Cookie 加密的方式不同(ASP.NET Identity 会更加复杂),所以如果登录方式使用的 Forms Authentication,那在 ASP.NET 5 中就没有办法判断用户验证,因为加密和解密要一一对应,如果不对应,那获取到的 Cookie 就没有办法解密成功,所以也就没有办法通过身份验证(IsAuthenticated 为 false),另外,关于 ASP.NET Identity,它不像一个技术点,有点类似于框架的概念,只不过把身份验证的内容包装了一下,比如产生了 ApplicationDbContext、UserManager、SignInManager 等等,作用就是让你使用更加方便,查看源码就知道,其实核心内容就是上面那些。

关于 SignInManager.cs 中的代码,我们发现有很大的变化,比如 SignInAsync 中的代码,
Context.Authentication.SignInAsync
的实现,我们可以从 Security 项目中找到,具体在 Microsoft.AspNet.Authentication/AuthenticationHandler.cs,感觉和之前的相比变的复杂了。

回到最初的问题:在 ASP.NET 5 中,如何实现身份验证(兼容 Forms Authentication)?

上面的问题虽然看起来很简单,但是有个首要前提:ASP.NET 5 不支持 Forms Authentication,那么这个问题就变得复杂了,但我们可以拆分下:

了解现阶段 ASP.NET 5 身份验证的实现方式。

在 ASP.NET 5 中,解密 Cookie(通过 Forms Authentication 加密)。

我们先研究第一问题,首先,我们不使用 ASP.NET 5 Identity,而是直接登录进行身份验证,为什么要这么做?因为登录系统不能重写,所以我们使用 ASP.NET 5 Identity 也没有什么意义,况且多了一大堆不必要的东西(UserManager、SignInManager 等),会让问题变的复杂,在之前的博文最后,有一个简单示例,如下:

//app.UseIdentity();
app.UseCookieAuthentication((cookieOptions) =>
{
cookieOptions.AuthenticationType = IdentityOptions.ApplicationCookieAuthenticationType;
cookieOptions.AuthenticationMode = AuthenticationMode.Active;
cookieOptions.CookieHttpOnly = true;
cookieOptions.CookieName = ".CookieName";
cookieOptions.LoginPath = new PathString("/Account/Login");
cookieOptions.CookieDomain = ".mysite.com";
}, "AccountAuthorize");

[AllowAnonymous]
public IActionResult Login(string returnUrl = null)
{
var userId = "xishuai";
var identity = new ClaimsIdentity(IdentityOptions.ApplicationCookieAuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, userId));
Response.SignIn(identity);
return Redirect(returnUrl);
}

上面是一年前的代码,一年后变成了这样:

//app.UseIdentity();
app.UseCookieAuthentication((cookieOptions) =>
{
cookieOptions.AutomaticAuthenticate = true;
cookieOptions.AutomaticChallenge = true;
cookieOptions.CookieHttpOnly = true;
cookieOptions.ExpireTimeSpan = TimeSpan.FromMinutes(43200);
cookieOptions.LoginPath = new PathString("/account/login");
cookieOptions.CookieName = ".CNBlogsCookie";
cookieOptions.CookiePath = "/";
});

public async Task<IActionResult> Login(string returnUrl = null)
{
var userId = "xishuai";
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(ClaimTypes.Name, userId));
await HttpContext.Authentication.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity), new AuthenticationProperties() { IsPersistent = true });
return Redirect(returnUrl);
}

上面看似没问题的代码,但实际使用中遇到了很多的问题,比如生成 Cookie 的 Expires 为 Session,也就是我们设置的 ExpireTimeSpan 没有作用,解决方式:SignInAsync 需要传递一个
new AuthenticationProperties() { IsPersistent = true }
参数,另外还有其它问题,我现在已经记不得了,不过记录了一个 Issue:HttpContext.Authentication.SignInAsync not working,再贴一下 project.json 中程序包版本,后来测试很多次,可能是版本不一致引起的:

"dependencies": {
"Microsoft.AspNet.Authentication.Cookies": "1.0.0-rc2-16160",
"Microsoft.AspNet.DataProtection.Extensions": "1.0.0-rc2-15874",
"Microsoft.AspNet.Diagnostics": "1.0.0-rc2-16303",
"Microsoft.AspNet.IISPlatformHandler": "1.0.0-rc2-15994",
"Microsoft.AspNet.Mvc": "6.0.0-rc2-16614",
"Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-rc2-16614",
"Microsoft.AspNet.Server.Kestrel": "1.0.0-rc2-16156",
"Microsoft.AspNet.StaticFiles": "1.0.0-rc2-16036",
"Microsoft.AspNet.Tooling.Razor": "1.0.0-rc2-15994",
"Microsoft.Extensions.Configuration.FileProviderExtensions": "1.0.0-rc2-15905",
"Microsoft.Extensions.Configuration.Json": "1.0.0-rc2-15905",
"Microsoft.Extensions.Logging": "1.0.0-rc2-15907",
"Microsoft.Extensions.Logging.Console": "1.0.0-rc2-15907",
"Microsoft.Extensions.Logging.Debug": "1.0.0-rc2-15907",
"Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0-rc2-16142"
}

后来折腾了很久,测试可以使用了,但发布到服务器的时候,又出现了问题,因为站点使用的是负载均衡,需要把程序发布到两台服务器上,当两台服务器同时在跑的时候,比如登录请求到一台服务器,验证刚好请求到另一台服务器,这时候身份验证就没有效果,然后跳转到登录页面,这个问题折腾我很久,自己怎么配置都不行,后来没有办法,向微软提了一个 Issue:Multiple web servers CookieAuthentication does not work,问题提出后,很快有人回复了,问题原因是需要提供一个 key,这个有点像 Forms Authentication 方式中 Web.config 的 MachineKey,我们需要将身份验证的配置,修改如下:

var dataProtection = new Microsoft.AspNet.DataProtection.DataProtectionProvider(new DirectoryInfo(@"c:\shared-auth-ticket-keys\"));

app.UseCookieAuthentication((cookieOptions) =>
{
cookieOptions.AutomaticAuthenticate = true;
cookieOptions.AutomaticChallenge = true;
cookieOptions.CookieHttpOnly = true;
cookieOptions.ExpireTimeSpan = TimeSpan.FromMinutes(43200);
cookieOptions.LoginPath = new PathString("/account/login");
cookieOptions.CookieName = ".CNBlogsCookie";
cookieOptions.CookiePath = "/";
cookieOptions.DataProtectionProvider = dataProtection;
});

后来重新发布,测试还是出现问题,和之前的问题一样,跳转到登录页面,然后我尝试把一台服务器生成在
c:\shared-auth-ticket-keys
目录下的 key 文件,拷贝到另外一台服务器中,但还是没用,过了很多天,有人回复了:


You need to point the key directory to a shared directory which both applications can access. Putting it in c:\shared-auth-ticket-keys isn't enough in multiple server scenarios, as it's still going to create a key ring local to each machine.

You need to create an UNC share somewhere that both applications can access, and use that, for example \keystore\keystore

Or you implement a key store yourself suitable to your architecture, for example, using SQL Server.


大致意思是,虽然是同一个目录,但会在不同服务器生成不同的 key 文件,所以身份验证就不通过,解决方式是使用 key 共享文件,这样让不同服务器都能访问同一个 key 文件,另外一种方式是将 key 存储在一个地方,比如 SQL Server 中,但我不是很了解 key 的读取和存储方式,所以,我最后尝试用第一种方式解决,只需要我们将目录更改为共享目录:

var dataProtection = new Microsoft.AspNet.DataProtection.DataProtectionProvider(new DirectoryInfo(@"\\10.10.10.10\shared-auth-ticket-keys\"));

后来再重新发布,还是出现了问题,比如共享文件放在一台服务器上,这台服务器访问没用什么问题,但另一台服务器却不能访问,文件资源管理器可以访问此共享文件,这个问题也折腾我很久,但不和 ASP.NET 5 相关,主要问题是不了解 ASP.NET 如何访问共享文件,后来找资料解决了,记录了一篇博文:ASP.NET 访问共享文件夹

目前的情况:第一个问题已经实现,但是比较简陋,开始考虑并实现第二个问题。

一开始的时候,我提了一个 Issue:Share ASP.NET MVC 5 Forms authentication?

这个 Issue 我觉得很有价值,它让我了解了很多东西,比如 ASP.NET 5 不支持 Forms Authentication,ASP.NET 5 和 Forms Authentication 的 Cookie 加密方式不同,ASP.NET 5 会更加复杂,因为登录系统不能被重写,并且 ASP.NET 5 不支持 Forms Authentication,那么摆在我面前的只有一条路,在 ASP.NET 5 中,解密 Cookie(通过 Forms Authentication 加密),针对这个问题,我的一些想法:

其实看起来这个问题好像不是很复杂,通过 Key 加密生成 Cookie(Forms Authentication),然后通过下面方式获取 Cookie(ASP.NET 5):

var cookies = Request.Cookies.First(x => x.Key == ".CNBlogsCookie").Value;

然后通过某些手段解密生成 IdentityUser 对象,对,没错,就这么简单。

我们先不住 ASP.NET 5 中实现下,很简单:

var cookies = "";
FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(cookies);
string[] roles = authTicket.UserData.Split(new char[] { ';' });
var user = new GenericPrincipal(User.Identity, roles);

这段代码是执行成功的,但我们需要在 Web.config 中,配置如下代码:

这段代码必须要和登录站点中的配置一样,原因是加密和解密的方式要一一对应,接下来的工作,我们需要在 ASP.NET 5 中实现上面的代码,但你会发现找不到
FormsAuthentication.Decrypt
了,这么办呢?只能查看源码,然后把相关代码贴出来编译一下,如果成功了(我尝试了很多次,因为涉及的代码太多,实现起来非常困难),这是第一步,第二步我们将编译通过的代码,放在 ASP.NET 5 中再编译一次,这个工作我还没做,不过看起来并不是那么简单,因为运行时和基础类库都发生变化了。

如果重写这部分代码,我贴一下需要的一些资源(后面再尝试下):

System.Web/Security/FormsAuthentication.cs(referencesource)

System.Web/Security/FormsAuthentication.cs(GitHub)

System.Web/Security/FormsAuthentication.cs(mono)

https://github.com/aspnet/Identity

https://github.com/aspnet/Security

https://github.com/aspnet/dataprotection

后来,上面那个 Issue 有人回复如下:



看到这,有点想哭的赶脚,但不管怎样,还是要尝试下,希望下集是一个成功的博文记录,未完待续。。。

最后,贴一下这段时间累积的有关资料:

Sharing cookies between applications.

Understanding OWIN Forms authentication in MVC 5

asp.net - Cookie-based Forms Authentication Across MVC 5 and MVC 6 (vNext) Applications

MVC5 - ASP.NET Identity登录原理 - Claims-based认证和OWIN

FormsAuthenticationTicket基于forms的验证

asp.net Forms表单验证 使用经验及验证流程分析

Difference between Claims vs OAuth

Adding ASP.NET Identity to an Empty or Existing Web Forms Project

authentication - CookieAuthenticationOptions, ExpireTimeSpan does not work

Understanding OWIN Forms authentication in MVC 5

Reading Katana Cookie Authentication Middleware’s Cookie from FormsAuthenticationModule

MVC Forms Authentication and Storing Data in the Cookie

Use MachineKey in ASP.NET vNext

Setting the Machine Key as usual? ... or any other gotchas for web farm scenarios?

Key Encryption At Rest
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: