ASP.NET Core 认证系统,包括Cookie, JwtBearer, OAuth, OpenIdConnect等
下面我们要讲的就是基于Cookie的身份认证
由于HTTP协议是无状态的,但对于认证来说,必然要通过一种机制来保存用户状态,而最常用,也最简单的就是Cookie了,它由浏览器自动保存并在发送请求时自动附加到请求头中。尽管在现代Web应用中,Cookie已略显笨重,但它依然是最为重要的用户身份保存方式。
让我们来了解下什么是:身份
理解身份验证如何工作的关键是首先了解ASP.NET Core 2.0中的身份。有三个类代表用户的身份:
Claim:身份证单元,ClaimsIdentity:身份证和ClaimsPrincipal:身份证持有者
Microsoft.AspNetCore.All 是 ASP.NET Core 的全家桶,包含 Mvc, EFCore, Identity, NodeService, AzureAppServe 等等,并集成到了 .NET Core SDK 当中,个人感觉有违 .NET Core 轻量模块化的理念,虽然使用看似更加方便了,却也让我们变得更加傻瓜化。当然你可以移除掉 Microsoft.AspNetCore.All 的引用,然后手动安装需要的Nuget包,当然,你可以不这么做,并略过此小节
如果你移除掉Microsoft.AspNetCore.All 的引用,那么基于Cookie的身份验证就需要安装如下几个包
dotnet add package Microsoft.AspNetCore.Hosting --version 2.0.0 //ASP.NET Core应用程序的宿主程序,必须引用
dotnet add package Microsoft.AspNetCore.Server.Kestrel --version 2.0.0 //最常用的Web服务器,支持跨平台,在Windows下还可以使用HttpSysServer或IISIntegration
dotnet add package Microsoft.Extensions.Logging.Console --version 2.0.0 //用来记录日志,可将日志输出到控制台,调试窗口,Windows事件日志等
dotnet add package Microsoft.AspNetCore.Authentication.Cookies --version 2.0.0 //Cookies
在Microsoft.AspNetCore.Authentication 的namespace命名空间下有定义了一个 AuthenticationHttpContextExtensions类
AuthenticationHttpContextExtensions 类是对 HttpContext 认证相关的扩展,它提供了如下扩展方法:
AuthenticateAsync:验证在 SignInAsync
中颁发的证书,并返回一个 AuthenticateResult
对象,表示用户的身份
ChallengeAsync:通知用户需要登录。在默认实现类AuthenticationHandler中,返回401
ForbidAsync:通知用户权限不足。在默认实现类AuthenticationHandler中,返回403
SignInAsync:登录用户。(该方法需要与AuthenticateAsync配合验证逻辑) SignInAsync用户登录成功后颁发一个证书(加密的用户凭证,这个凭证放入Cookie中),用来标识用户的身份
SignOutAsync:退出登录,里面做的操作就是清除Cookie
项目实例:
控制器:AccountController.cs
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using WebApp.Models;
namespace WebApp.Controllers
{
[AllowAnonymous]//允许匿名访问
public class AccountController : Controller
{
private DbService db;
public AccountController(DbService _db)
{
this.db = _db;
}
/// <summary>
/// 登陆视图界面
/// </summary>
/// <param name="returnUrl"></param>
/// <returns></returns>
[HttpGet]
public IActionResult Login(string returnUrl = null) //这个returnUrl一般用于做单点登陆的认证中心回调地址
{
TempData["returnUrl"] = returnUrl;
return View();
}
/// <summary>
/// 登陆Post请求
/// </summary>
/// <param name="model"></param>
/// <param name="returnUrl"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)//这个returnUrl同上
{
const string errorMsg = "用户名或密码错误";
if (model == null)
{
//ModelState.AddModelError(string.Empty,errorMsg);
return BadRequest(errorMsg);
}
var userInfo = new DbService().FindUser(model.UserName, model.Password); //查询数据库
if (userInfo == null)
{
//ModelState.AddModelError(string.Empty, errorMsg);
return BadRequest(errorMsg);
}
var roles = db.GetRolesByUserId(userInfo.Id);
//创建身份证的证件单元:一张身份证由许多证件单元组成
List<Claim> claims = new List<Claim>() {
new Claim(ClaimTypes.Sid, userInfo.Id.ToString()),
new Claim(ClaimTypes.Name, userInfo.Name),
new Claim(ClaimTypes.Email, userInfo.Email),
new Claim(ClaimTypes.MobilePhone, userInfo.PhoneNumber),
new Claim(ClaimTypes.Country, userInfo.Source),//来源:QQ,微信,微博之类的
};
roles.ForEach(r => claims.Add(new Claim(ClaimTypes.Role, r)));
//根据证件单元创建身份证
ClaimsIdentity identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
//创建身份证这个证件的携带者:我们叫这个证件携带者为“证件当事人”
ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal();
claimsPrincipal.AddIdentity(identity); //将身份证交给到“证件当事人”手上
//这一步就是用身份证信息创建了一个加密的Cookie,然后这个Cookie会发送到浏览器中,这叫保存登陆状态(其实我觉得更应该叫Token)
//然后浏览器再将这个Cookie发送服务端,服务端解析这个Cookie。
await HttpContext.SignInAsync(claimsPrincipal);
if (returnUrl == null)
{
returnUrl = TempData["returnUrl"]?.ToString();
}
if (returnUrl != null)//&& !Url.IsLocalUrl(returnUrl)
{
return Redirect(returnUrl);
}
return RedirectToAction(nameof(HomeController.Index), "Home");
}
/// <summary>
/// 无权限
/// </summary>
/// <param name="returnUrl"></param>
/// <returns></returns>
public IActionResult AccessDenied(string returnUrl = null)
{
return View();
}
/// <summary>
/// 登出
/// </summary>
/// <returns></returns>
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return RedirectToAction("Login");
}
/ <summary>
/ 注册
/ </summary>
/ <param name="model"></param>
/ <param name="returnUrl"></param>
/ <returns></returns>
//public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
//{
// returnUrl = returnUrl ?? Url.Content("~/");
// if (ModelState.IsValid)
// {
// var user = new User { Name = model.Name, Email = model.Name };
// //var result = await _userManager.CreateAsync(user, model.Password);
// var result = new { Succeeded = true, Errors = string.Empty };//这一步是将注册用户写入数据库,我就不写了,模拟已经注册成功了
// if (result.Succeeded)
// {
// //为指定用户生成电子邮件确认令牌。
// var code = await userManager.GenerateEmailConfirmationTokenAsync(user);
// var callbackUrl = Url.Page(
// "/Account/ConfirmEmail",
// pageHandler: null,
// values: new { userId = user.Id, code = code },
// protocol: Request.Scheme);
// //await EmailHelper.SendEmailAsync(Input.Email, "Confirm your email",
// // $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
// //登陆操作;
// var Identity = new ClaimsIdentity();
// Identity.AddClaim(new Claim(ClaimTypes.Name, model.Name));
// await HttpContext.SignInAsync(new ClaimsPrincipal(Identity));
// if (returnUrl != null) //Url不等于null一般情况是单点登录注册
// {
// return Redirect(returnUrl);
// }
// return RedirectToAction(nameof(HomeController.Index), "Home");
// }
// foreach (var error in result.Errors)
// {
// ModelState.AddModelError(string.Empty, "Error中的错误信息");
// }
// }
// return RedirectToAction("Error");
//}
}
}
控制器:HomeController.cs
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Diagnostics;
using System.Security.Claims;
using System.Threading.Tasks;
using WebApp.Models;
namespace WebApp.Controllers
{
//[AllowAnonymous]
public class HomeController : Controller
{
private IAuthorizationService _authorization;
public HomeController(IAuthorizationService authorizationService)
{
_authorization = authorizationService;
}
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)] //Schemes授权
//[Authorize(Roles = "Sale,Admin")] //这个Action只能是Sale和Admin这个角色才能访问 //Role授权
[Authorize(Policy = "MyPolicy")]
public async Task<IActionResult> Index()
{
//var allowed = await _authorization.AuthorizeAsync(User, "MyPolicy");
var user = User.FindFirst(ClaimTypes.Name).Value;
var role = User.FindFirst(ClaimTypes.Role).Value;
HttpContext.Response.Cookies.Append("UserName", "wowo");
IEnumerable<ClaimsIdentity> u = User.Identities;
var d = u.ToString();
return View();
}
public IActionResult Privacy()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}
在项目下创建一个Authentication,在文件夹中创建一个MyPolicyRequirement类,这个类自定义验证方式
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using System.Security.Claims;
using System.Threading.Tasks;
namespace WebApp
{
public class MyPolicyRequirement : IAuthorizationRequirement
{
}
public class MyPolicyHandler : AuthorizationHandler<MyPolicyRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MyPolicyRequirement requirement)
{
//这里我们可以自由的发挥我们的定义的授权验证
//在这里我们可以要求Claim中必须要有Sid这个属性
//if (context.User.HasClaim(c => c.Type == ClaimTypes.Sid))
//{
// string userIdStr = context.User.FindFirst(c => c.Type == ClaimTypes.Sid).Value;
// int userId = Convert.ToInt32(userIdStr);
// if (userId < 2)
// {
// context.Succeed(requirement);
// }
//}
if (context.User != null && context.User.HasClaim(c => c.Type == ClaimTypes.Country))
{
//自定义授权策略:只有来源为QQ的用户能访问
string source = context.User.FindFirst(ClaimTypes.Country).Value;
if (source == "WeChat")
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
}
创建一个类:DbService.cs
using System.Collections.Generic;
using System.Linq;
using WebApp.Models;
namespace WebApp
{
public class DbService
{
/// <summary>
/// 模拟用户数据库表:假设数据库中有两条数据,用来实现用户登录时的用户名和密码的检查
/// </summary>
private List<User> users = new List<User>()
{
new User { Id=1, Name="lily", Password="123456", Email="lily@gmail.com", PhoneNumber="18600000001" , Source="QQ" },
new User { Id=2, Name="jeck", Password="123456", Email="jeck@gmail.com", PhoneNumber="18600000002",Source="WeChat" },
new User { Id=3, Name="tom", Password="123456", Email="tom@gmail.com", PhoneNumber="18600000002",Source="MicroBlog" }
};
public List<Role> role = new List<Role>()
{
new Role{ Id=1,RoleName="Admin", RoleDescribe="系统管理员" },
new Role{ Id=2,RoleName="Sale", RoleDescribe="销售" },
new Role{ Id=3,RoleName="Finance", RoleDescribe="财务" },
};
private List<UserRole> userRole = new List<UserRole>()
{
new UserRole{ UserId=1, RoleId=1 }, //lily是系统管理员
new UserRole{ UserId=2, RoleId=2 }, //jeck是销售
new UserRole{ UserId=2, RoleId=3 }, //jeck又是财务
new UserRole{ UserId=3, RoleId=2 } //tom是销售
};
public User FindUser(string userName, string password)
{
return users.FirstOrDefault(r => r.Name == userName && r.Password == password);
}
//根据用户ID查询用户角色集合
public List<string> GetRolesByUserId(int userid)
{
List<string> roleNames = new List<string>();
List<int> roleids = userRole.Where(r => r.UserId == userid).Select(r => r.RoleId).ToList();
role.ForEach(r =>
{
if (roleids.Contains(r.Id))
{
roleNames.Add(r.RoleName);
}
});
return roleNames;
}
public string GetRoles(int userid)
{
List<string> roleNames = GetRolesByUserId(userid);
return string.Join(",", roleNames);
}
}
}
Startup.cs类
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using WebApp.Models;
namespace WebApp
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;//CheckConsentNeeded设置为true,非必需 cookie 不会发送到浏览器
});
启用我们的自定义的MyAuthenticationHandler身份验证中心
//services.AddAuthenticationCore(o =>
//{
// o.AddScheme<MyAuthenticationHandler>("myScheme", "描述信息而已");
//});
//Authentication:是认证。认证是用来证明一个人的身份(如:姓名,邮箱等等),一般情况是判断你这个人是否登陆过,登陆后保存了Cookie自然就知道你的身份了
services.AddAuthentication(o =>
{
o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;//使用默认的Scheme认证授权Scheme
}).AddCookie(o =>
{
//关于Cookie的配置信息请参考:https://www.cnblogs.com/sheldon-lou/p/9545726.html
//o.Cookie.Domain = ".contoso.com";//设置Cookie的作用域:他的作用域就包括contoso.com,www.contoso.com
o.LoginPath = "/Account/Login"; //在身份验证的时候判断为“未登录”则跳转到这个页面
o.LogoutPath = "/Account/Logout";//如果要退出登录则跳转到这个页面
o.AccessDeniedPath = "/Account/AccessDenied"; //如果已经通过身份验证,但是没有权限访问则跳转到这个页面
o.Cookie.HttpOnly = true;//设置 cookie 是否是只能被服务器访问,默认 true,为true时通过js脚本将无法读取到cookie信息,这样能有效的防止XSS攻击,窃取cookie内容,这样就增加了cookie的安全性
o.SlidingExpiration = true;//设置Cookie过期时间为相对时间;也就是说在Cookie设定过期的这个时间内用户没有访问服务器,那么cookie就会过期,若有访问服务器,那么cookie期限将从新设为这个时间
o.ExpireTimeSpan = TimeSpan.FromDays(1); //设置Cookie过期时间为1天
o.ClaimsIssuer = "Cookie";//获取或设置应用于创建的任何声明的颁发者
//o.Cookie.Path = "/app1"; //用来隔离同一个服务器下面的不同站点。比如站点是运行在/app1下面,设置这个属性为/app1,那么这个 cookie 就只在 app1下有效。
});
//Authorization:是授权。这里是置授权策略;授权是用来表示这个用户能做什么事情,比如admin可以修改删除数据,normal user只能查看数据。
services.AddAuthorization(options =>
{
options.AddPolicy("MyPolicy",
policy =>
{
policy.Requirements.Add(new MyPolicyRequirement());
//policy.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
//policy.RequireAuthenticatedUser();
//policy.RequireRole("Admin", "Finance");//使用角色配置策略,多个系统角色
//policy.RequireClaim("claimname"); //将ClaimsAuthorizationRequirement添加到当前实例
//policy.RequireUserName("username");
});
});
services.AddSingleton<IAuthorizationHandler, MyPolicyHandler>();//单例:依赖注入
services.AddSingleton<DbService>(); //将DbService注册到DI系统中
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();//支持授权
//默认情况下,ASP.NET Core 应用不会为 HTTP 状态代码(如“404 - 未找到”)提供状态代码页。 应用返回状态代码和空响应正文。 若要提供状态代码页,请使用状态代码页中间件。
//app.UseStatusCodePages();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
当然我们也可以自定义验证中心
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using System;
using System.Security.Claims;
using System.Threading.Tasks;
namespace WebApp.Authentication
{
/// <summary>
/// 自定义认证中心Handler
/// 通常我们会提供一个统一的认证中心,负责证书的颁发及销毁(登入登出),而其他服务只用来验证证书,并用不到SingIn/SingOut
/// </summary>
public class MyAuthenticationHandler : IAuthenticationHandler, IAuthenticationSignInHandler, IAuthenticationSignOutHandler
{
public AuthenticationScheme Scheme { get; private set; }
protected HttpContext Context { get; private set; }
//初始化上下文,Scheme
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
Scheme = scheme;
Context = context;
return Task.CompletedTask;
}
/// <summary>
/// 登陆状态保存:一般用在登陆的时候,在判断用户名和密码正确了的情况下,调用这个方法
/// 根据用户的Claims证件信息生成一个ticket票据,然后将这个票据保存到Cookie中去,这样浏览器就可以得到这个Cookie
/// </summary>
/// <param name="user"></param>
/// <param name="properties"></param>
/// <returns></returns>
public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
{
//根据身份证信息,生成票据
var ticket = new AuthenticationTicket(user, properties,Scheme.Name);
Context.Response.Cookies.Append("myCookie", Serialize(ticket)); //将票据加入到Cookie中
return Task.CompletedTask;
}
/// <summary>
/// 登出:一般是删除这个登录Cookie
/// </summary>
/// <param name="properties"></param>
/// <returns></returns>
public Task SignOutAsync(AuthenticationProperties properties)
{
Context.Response.Cookies.Delete("myCookie");
return Task.CompletedTask;
}
/// <summary>
/// 在 AuthenticateAsync 中从Cookie中读取,如果能得到这个Cookie则表示用户已经登陆过了。
/// </summary>
/// <returns></returns>
public Task<AuthenticateResult> AuthenticateAsync()
{
var cookie = Context.Request.Cookies["myCookie"];
if (string.IsNullOrEmpty(cookie))
{
return Task.FromResult(AuthenticateResult.NoResult());//认证失败返回
}
return Task.FromResult(AuthenticateResult.Success(this.Deserialize(cookie))); //认证成功返回
}
//未登录:跳转到登陆页
public Task ChallengeAsync(AuthenticationProperties properties)
{
Context.Response.Redirect("/Account/Login");
return Task.CompletedTask;
}
//禁止访问:权限不够
public Task ForbidAsync(AuthenticationProperties properties)
{
Context.Response.StatusCode = 403;
return Task.CompletedTask;
}
//反序列化
public AuthenticationTicket Deserialize(string content)
{
byte[] byteTicket = System.Text.Encoding.Default.GetBytes(content);
return TicketSerializer.Default.Deserialize(byteTicket);
}
//序列化
public string Serialize(AuthenticationTicket ticket)
{
byte[] byteTicket = TicketSerializer.Default.Serialize(ticket);
return System.Text.Encoding.Default.GetString(byteTicket);
}
}
}
看看:Login.cshtml
@{
ViewData["Title"] = "Login";
}
<h1>Login</h1>
@{
<form asp-antiforgery="true" asp-controller="Account" asp-action="Register">
User name: <input name="username" type="text" />
Password: <input name="password" type="password" value="123456"/>
<input name="submit" value="Login" type="submit" />
<input type="hidden" name="returnUrl" value="@TempData["returnUrl"]" />
</form>
}
AccessDenied.cshtml
@{
ViewData["Title"] = "AccessDenied";
}
<h1>大哥您没权限访问啊</h1>