目录
1、Cookie-based认证的实现
2、Jwt Token 的认证与授权
3、Identity Authentication + EF 的认证
Cookie-based认证的实现
cookie认证方式如下图所示,当我们访问一个网页(Admin/Index)时候,这时候系统会检查你是否有权限,假如没有权限,便会我当前Url重定向到登陆页面(/Account/Login),在登陆成功后,系统会返回一个cookie保存在浏览器,此时再带着这个cookie去重新访问你最开始要访问的页面(Admin/Index)。如下图所示。
我们在.net core 中,也有一套基于cookie-basic的认证方式。
首先,创建一个Core 2.0的MVC项目,添加两个控制器AdminController代表我们要认证后才能访问的资源,AccountController,模拟登陆。在AccountController中,
1 [Authorize] 2 public class AdminController : Controller 3 { 4 public IActionResult Index() 5 { 6 return View(); 7 } 8 }
1 public class AccountController : Controller 2 { 3 public async Task<IActionResult> MakeLogin() 4 { 5 var claims = new List<Claim> 6 { 7 new Claim(ClaimTypes.Name,"lmc"), 8 new Claim(ClaimTypes.Role, "admin") 9 }; 10 var claimsIdentity = new ClaimsIdentity( 11 claims, 12 CookieAuthenticationDefaults.AuthenticationScheme 13 ); 14 await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, 15 new ClaimsPrincipal(claimsIdentity), 16 new AuthenticationProperties { 17 IsPersistent=true, //cookie过期时间设置为持久 18 ExpiresUtc= DateTime.UtcNow.AddSeconds(20) //设置过期20秒 19 }); 20 return Ok(); 21 } 22 public async Task<IActionResult> Logout() 23 { 24 await HttpContext.SignOutAsync( 25 CookieAuthenticationDefaults.AuthenticationScheme); 26 return Ok(); 27 } 28 }
然后我们配置Startup,将认证服务加入到DI容器&&引用认证中间件。
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) 4 .AddCookie(config=> 5 { 6 config.LoginPath = "/Account/MakeLogin"; //未认证导向登陆的页面,默认为/Account/Login 7 config.Cookie.Name = "lmccookie"; //设置一个cookieName 8 9 }); 10 services.AddMvc(); 11 } 12 13 public void Configure(IApplicationBuilder app, IHostingEnvironment env) 14 { 15 if (env.IsDevelopment()) 16 { 17 app.UseDeveloperExceptionPage(); 18 app.UseBrowserLink(); 19 } 20 else 21 { 22 app.UseExceptionHandler("/Home/Error"); 23 } 24 25 app.UseStaticFiles(); 26 app.UseAuthentication(); //加入认证中间件 27 ...//// other code 28 }
测试下。 直接访问 Admin/Index被重定向到Login并返回我们定义的cookie,我们再带着cookie再次访问Admin/Index
Jwt Token 的认证
JwtToken 一般用于一些前后端分离的项目或者是移动端的项目。大体流程是用户首先访问目标资源(例如这里的api/values),然后服务器返回一个401或者是403的响应码标识未授权登陆。这时用户应该重新登陆获取token (例如这里的api/token),拿到token以后,请求头里面带着token再去访问目标资源。
JwtToken 由三部分构成 首先是HEADER,这里面包含了Base64加密过的 加密算法和token的类型;PAYLOAD ,这里包含了一个Base64加密过的 Claims数组;SIGNATURE,包含了使用你的加密算法把加密过后的 HEADER ‘. ’ 和 PAYLOAD 加一个自定义的密钥。
我们在.net core 中实现下Jwttoken的验证。
我们在ValuesController 打上[Authorize] 标签。配置我们的startup,在startup ConfigureServices方法中注入认证服务,Configure方法中添加中间件。 此时我们访问api/values=》返回401 的http状态码。
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.Configure<JwtSettings>(Configuration.GetSection("JwtSettings")); //appsettings中读取到jwtsettings节点 4 var jwtSetting = new JwtSettings(); 5 Configuration.Bind("JwtSettings", jwtSetting); 6 services.AddAuthentication(options => 7 { // 添加认证头 8 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 9 options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 10 }) 11 .AddJwtBearer(jo => jo.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters() 12 { 13 ValidIssuer = jwtSetting.Issuer, //使用者 14 ValidAudience = jwtSetting.Audience, //颁发者 15 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSetting.SecreKey)) //加密方式 16 }); 17 services.AddMvc(); 18 } 19 public void Configure(IApplicationBuilder app, IHostingEnvironment env) 20 { 21 if (env.IsDevelopment()) 22 { 23 app.UseDeveloperExceptionPage(); 24 } 25 app.UseAuthentication(); //注意加入中间件
public class JwtSettings { public string Issuer { get; set; } //办法token的人 public string Audience { get; set; } //token使用者 public string SecreKey { get; set; } //token加密钥 }
1 { 2 "Logging": { 3 "IncludeScopes": false, 4 "Debug": { 5 "LogLevel": { 6 "Default": "Warning" 7 } 8 }, 9 "Console": { 10 "LogLevel": { 11 "Default": "Warning" 12 } 13 } 14 }, 15 "JwtSettings": { 16 "Audience": "http://localhost:5000", 17 "Issuer": "http://localhost:5000", 18 "SecreKey": "HelloKeylmclmclmc" 19 } 20 }
接下来需要生成token,添加一个AuthorizeController,通过构造函数把jwtsettings 注入进来,添加一个Index的Action 用来生成我们的token。我们需要一个验证下登录用户的用户名密码,所以还需要添加一个ViewModel
public class LoginViewModel { [Required] public string Name { get; set; } [Required] public string PassWord { get; set; } }
1 private JwtSettings _jwtSettings; 2 public AuthorizeController(IOptions<JwtSettings> options) //构造函数注入,拿到appsettings 里面的jwtsettings 3 { 4 _jwtSettings = options.Value; 5 } 6 [Route("api/token")] 7 [HttpPost] 8 public IActionResult Index(LoginViewModel loginViewModel) 9 { 10 if (!ModelState.IsValid) 11 return BadRequest(); 12 if (!(loginViewModel.Name == "lmc" && loginViewModel.PassWord == "123456")) 13 return BadRequest(); 14 var claims = new Claim[] //实例化一个Claim 15 { 16 new Claim(ClaimTypes.Name,"lmc"), 17 new Claim(ClaimTypes.Role, "admin") 18 }; 19 var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecreKey)); //将appsettings里面的SecreKey拿到 20 var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); //使用HmacSha256 算法加密 21 //生成token,设置过期时间为30分钟, 需要引用System.IdentityModel.Tokens.Jwt 包 22 var token = new JwtSecurityToken(_jwtSettings.Issuer, _jwtSettings.Audience, claims, DateTime.Now, DateTime.Now.AddMinutes(30), creds); 23 //将token返回 24 return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) }); 25 }
一切准备就绪,拿到Token。带着token来访问 /api/values,注意token需要带这常量 bearer 。
带着我们的加密钥来jwt官网验证一下。
基于角色(Role),Claim/Policy 的授权:
在我们返回token的时候,实例化过一个Claim数组,其中,Role是admin。我们修改ValuesController的Authorize特性标签( [Authorize(Roles = "user")])。
当我们带着token再去验证时候。然后我们把Claim数组的Role那一项的Value 改为user 便会正常认证。
基于Claim的验证我们需要做的事在StartUp的ConfigureServices方法中,将授权模块加入到DI容器中。这里我们添加了一个SuperAdminOnlyd的Policy
此时,我们需要在AuthorizeController控制器返回Token的时候,需要在Claim数组中添加一个新的Claim,表示我们的Policy。
此时,再修改我们的ValuesController控制器的Authorize特性标签。
PostMan 走一波===》
Identity +EF Authentication 的认证
首先,使用net core 的脚手架命令创建一个自带Identity 的mvc项目。然后根据appseetings的数据库配初始化数据库(默认数据库实例是(localdb)\mssqllocaldb,因为我装vs时候没装这个,所以我换成了.) 。
还原完了数据库,就可以把项目跑起来了,可以根据右上角注册,登录下==》 (其中代码可以自行观看)
然后让我们来自己从头开始实现下这个过程:
可以在上文中Cookie认证的项目中完成,也可以新建一个空的MVC core项目。添加一个Account控制器,其中存在三个方法(action),注册、登录,登出。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Threading.Tasks; 5 using Microsoft.AspNetCore.Mvc; 6 using System.Security.Claims; 7 using Microsoft.AspNetCore.Authentication.Cookies; 8 using Microsoft.AspNetCore.Authentication; 9 using MVCOnCookieBaseStudy.ViewModel; 10 using Microsoft.AspNetCore.Identity; 11 using MVCOnCookieBaseStudy.Models; 12 13 namespace MVCOnCookieBaseStudy.Controllers 14 { 15 public class AccountController : Controller 16 { 17 private UserManager<ApplicationUser> _userManager; //加入Identity自带的注册使用的Manager 18 private SignInManager<ApplicationUser> _signInManager; //加入Identity自带的登录使用的Manager 19 20 public AccountController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager) 21 { 22 _userManager = userManager; 23 _signInManager = signInManager; 24 } 25 /// <summary> 26 /// 注册页面 27 /// </summary> 28 /// <returns></returns> 29 public IActionResult Register(string returnUrl = "/Home/Index") 30 { 31 ViewData["ReturnUrl"] = returnUrl; 32 return View(); 33 } 34 [HttpPost] 35 public async Task<IActionResult> Register(RegisterViewModel registerViewModel, string returnUrl = "/Home/Index") 36 { 37 ViewData["ReturnUrl"] = returnUrl; 38 if (ModelState.IsValid) //model 验证 39 { 40 ApplicationUser identityUser = new ApplicationUser 41 { 42 Email = registerViewModel.Email, 43 UserName = registerViewModel.Email, 44 NormalizedUserName = registerViewModel.Email 45 }; 46 var result = await _userManager.CreateAsync(identityUser, registerViewModel.Password); 47 if (result.Succeeded) 48 { 49 await _signInManager.SignInAsync(identityUser, new AuthenticationProperties { IsPersistent = true }); 50 return Redirect(returnUrl); 51 } 52 else 53 { 54 foreach(var err in result.Errors) 55 { 56 ModelState.AddModelError("", err.Description); 57 } 58 } 59 } 60 61 return View(); 62 } 63 /// <summary> 64 /// 登录页面 65 /// </summary> 66 /// <returns></returns> 67 public IActionResult Login(string returnUrl = "/Home/Index") 68 { 69 ViewData["ReturnUrl"] = returnUrl; 70 return View(); 71 } 72 [HttpPost] 73 public async Task<IActionResult> Login(RegisterViewModel LoginViewModel, string returnUrl = "/Home/Index") 74 { 75 ViewData["ReturnUrl"] = returnUrl; 76 var loginUser = await _userManager.FindByEmailAsync(LoginViewModel.Email); 77 if (loginUser == null) 78 { 79 return View(); 80 } 81 82 await _signInManager.SignInAsync(loginUser, new AuthenticationProperties { IsPersistent = true }); 83 return Redirect(returnUrl); 84 } 85 /// <summary> 86 /// 原来的Cookie登录 87 /// </summary> 88 /// <returns></returns> 89 public async Task<IActionResult> MakeLogin() 90 { 91 var claims = new List<Claim> 92 { 93 new Claim(ClaimTypes.Name,"lmc"), 94 new Claim(ClaimTypes.Role, "admin") 95 }; 96 var claimsIdentity = new ClaimsIdentity( 97 claims, 98 CookieAuthenticationDefaults.AuthenticationScheme 99 ); 100 await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, 101 new ClaimsPrincipal(claimsIdentity), 102 new AuthenticationProperties { 103 IsPersistent=true, //cookie过期时间设置为持久 104 ExpiresUtc= DateTime.UtcNow.AddSeconds(20) //设置过期20秒 105 }); 106 return Ok(); 107 } 108 /// <summary> 109 /// 登出 110 /// </summary> 111 /// <returns></returns> 112 public async Task<IActionResult> Logout() 113 { 114 await _signInManager.SignOutAsync(); 115 return Redirect("/Home/Index"); 116 } 117 } 118 //覆盖默认验证 119 public class MyCookieTestAuthorize: CookieAuthenticationEvents 120 { 121 public override Task ValidatePrincipal(CookieValidatePrincipalContext context) 122 { 123 return base.ValidatePrincipal(context); 124 } 125 } 126 }
@using MVCOnCookieBaseStudy.ViewModel @model RegisterViewModel @{ ViewData["Title"] = "Register"; } <h2>@ViewData["Title"]</h2> <div class="row"> <div class="col-md-4"> <form asp-route-returnUrl="@ViewData["ReturnUrl"]" method="post"> <h4>Create a new account.</h4> <hr /> <div asp-validation-summary="All" class="text-danger"></div> <div class="form-group"> <label asp-for="Email"></label> <input asp-for="Email" class="form-control" /> <span asp-validation-for="Email" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Password"></label> <input asp-for="Password" class="form-control" /> <span asp-validation-for="Password" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="ConfirmPassword"></label> <input asp-for="ConfirmPassword" class="form-control" /> <span asp-validation-for="ConfirmPassword" class="text-danger"></span> </div> <button type="submit" class="btn btn-default">Register</button> </form> </div> </div> @section Scripts { @await Html.PartialAsync("_ValidationScriptsPartial") }
1 @using MVCOnCookieBaseStudy.ViewModel 2 @model RegisterViewModel 3 @{ 4 ViewData["Title"] = "Login"; 5 } 6 7 <h2>Login</h2> 8 9 10 <div class="row"> 11 <div class="col-md-4"> 12 <section> 13 <form asp-route-returnurl="@ViewData["ReturnUrl"]" method="post"> 14 <h4>Use a local account to log in.</h4> 15 <hr /> 16 <div asp-validation-summary="All" class="text-danger"></div> 17 <div class="form-group"> 18 <label asp-for="Email"></label> 19 <input asp-for="Email" class="form-control" /> 20 <span asp-validation-for="Email" class="text-danger"></span> 21 </div> 22 <div class="form-group"> 23 <label asp-for="Password"></label> 24 <input asp-for="Password" class="form-control" /> 25 <span asp-validation-for="Password" class="text-danger"></span> 26 </div> 27 <div class="form-group"> 28 <button type="submit" class="btn btn-default">Log in</button> 29 </div> 30 </form> 31 </section> 32 </div> 33 </div> 34 @section Scripts{ 35 @await Html.PartialAsync("_ValidationScriptsPartial"); @*前端验证,需要引用jquery.validate.min.js*@ 36 }
这个过程中需要使用到一个ViewModel,用来传递登录数据
1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel.DataAnnotations; 4 using System.Linq; 5 using System.Threading.Tasks; 6 7 namespace MVCOnCookieBaseStudy.ViewModel 8 { 9 public class RegisterViewModel 10 { 11 [Required] 12 [EmailAddress] 13 [Display(Name = "Email")] 14 public string Email { get; set; } 15 16 [Required] 17 [DataType(DataType.Password)] 18 [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 19 [Display(Name = "Password")] 20 public string Password { get; set; } 21 22 [DataType(DataType.Password)] 23 [Display(Name = "Confirm password")] 24 [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 25 public string ConfirmPassword { get; set; } 26 } 27 }
添加集成自Identity的User 和 Role还有数据库链接上下文
public class ApplicationUserRole: IdentityRole { } public class ApplicationUser:IdentityUser { }
1 public class ApplicationDbContext:IdentityDbContext<ApplicationUser,ApplicationUserRole,string> 2 { 3 public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) 4 { 5 6 } 7 }
接下来配置我们的StartUp,在ConfigureServices方法中 将数据库连接上下文加入DI容器,Identity服务加入DI容器,重新配置下我们的密码规则。(注意在Configure加入认证中间件)
1 public void ConfigureServices(IServiceCollection services) 2 { 3 //连接数据库服务加入DI容器 4 services.AddDbContext<ApplicationDbContext>(option => 5 { 6 option.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")); 7 }); 8 //将Identity服务加入DI容器 9 services.AddIdentity<ApplicationUser, ApplicationUserRole>() 10 .AddEntityFrameworkStores<ApplicationDbContext>() 11 .AddDefaultTokenProviders(); 12 //设置注册密码的规则 13 services.Configure<IdentityOptions>(options => 14 { 15 options.Password.RequireLowercase = false; 16 options.Password.RequireNonAlphanumeric = false; 17 options.Password.RequireUppercase = false; 18 }); 19 20 services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) 21 .AddCookie(config=> 22 { 23 config.LoginPath = "/Account/Login"; //未认证导向登陆的页面,默认为/Account/Login 24 config.Cookie.Name = "lmccookie"; //设置一个cookieName 25 26 }); 27 services.AddMvc(); 28 }