NetCore 基于identity的登录验证授权机制
十年河东,十年河西,莫欺少年穷
学无止境,精益求精
本篇探讨下基于NetCore_2.1版本的登录验证授权机制,
学过MVC的童鞋们都知道,Form验证中,MVC中采用的是FormTicket的验证授权机制,那么在NetCore中怎么实现登录验证授权呢?
其实我们在Asp.Net Core项目中的认证,也是比较简单的。现在我们就通过构建一个小项目来一步步来探讨,并最终实现NetCore的登录验证及授权。
1、新建一个解决方案(NetCoreAuth),该解决方案中包含五个项目,如下:
1.1、项目NetCoreAuth是一个MVC项目,通过VS直接创建即可,我们称之为:UI层
1.2、项目NetCoreInterface是接口层,我们都知道NetCore通过依赖注入来进行代码解耦,因此,接口层是必不可少的
1.3、项目NetCoreService是实现接口的服务层,用于依赖注入时,构建和接口的映射关系,也是必不可少的一层
1.4、项目NetCoreModels是一个Model层,一般项目中,用来构建数据DTO,因此,可以说也是必不可少的一层
1.5、项目NetCoreCommon是一个公共方法层,用于存放通用类,通用枚举,静态变量等
2、项目构建完毕后,我们先来完善除NetCoreAuth层(UI层)之外的代码
2.1、项目NetCoreCommon层很简单,只有一个类,用于作返回值用的,如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
namespace NetCoreCommon { public class BaseResponse { public BaseResponse() { this.isSuccess = false; this.resultCode = -1; this.resultMessage = "请求失败..."; } /// <summary> /// 返回信息 /// </summary> public string resultMessage { get; set; } /// <summary> /// 返回编码 -1 代表失败 0代表成功 /// </summary> public int resultCode { get; set; } /// <summary> /// 处理是否成功 /// </summary> public bool isSuccess { get; set; } } public class BaseResponse<T> : BaseResponse { public T Data { get; set; } //public List<T> DataList { get; set; } public BaseResponse() { this.isSuccess = false; this.resultCode = -1; this.resultMessage = "请求失败..."; } public BaseResponse(T data) { this.Data = data; } } public class CommonBaseResponse { #region 重置Response public static BaseResponse<T> SetResponse<T>(T Data, bool bol, string Msg = "", int cord = 0) { BaseResponse<T> response = new BaseResponse<T>(); response.Data = Data; response.isSuccess = bol; response.resultCode = bol == true ? 0 : -1; if (cord != 0) { response.resultCode = cord; } response.resultMessage = bol == true ? "请求成功..." : "请求失败..."; if (!string.IsNullOrEmpty(Msg)) { response.resultMessage = Msg; } return response; } public static BaseResponse SetResponse(bool bol, string Msg = "", int cord = 0) { BaseResponse response = new BaseResponse(); response.isSuccess = bol; response.resultCode = bol == true ? 0 : -1; if (cord != 0) { response.resultCode = cord; } response.resultMessage = bol == true ? "请求成功..." : "请求失败..."; if (!string.IsNullOrEmpty(Msg)) { response.resultMessage = Msg; } return response; } #endregion } }
2.2、项目NetCoreInterface层只有一个接口,用于登录接口,如下:
public interface IAdminService { UserInfoModel CheckAccountAndPassword(string Account,string Password); }
2.3.项目NetCoreService层实现接口层,因此也只有一个登录方法,如下:
public class AdminService: IAdminService { /// <summary> /// 检测当前登录账户 验证登录 /// </summary> /// <param name="Account"></param> /// <param name="Password"></param> /// <returns></returns> public UserInfoModel CheckAccountAndPassword(string Account,string Password) { if (Account.ToLower() == "chenwolong" && Password == "123456") return new UserInfoModel(); else return null; } }
2.4、项目NetCoreModels层只有一个实体类,用于当用户登录成功后,返回当前登录用户的基本信息,并赋值给这个实体类。其作用是:
在项目编码过程中,我们可随时通过帮助类,读取当前登录人的信息,当然,在此案例中,我并没有实现通过帮助类读取当前登录人信息这个功能。~_~
public class UserInfoModel { public string userId { get; set; } = Guid.NewGuid().ToString(); public string userSex { get; set; } = "男"; public string userPhone { get; set; } = "18137070152"; public string userAccount { get; set; } = "chenwolong"; public string userName { get; set; } = "陈卧龙"; public string userCompany { get; set; } = "盟拓软件(苏州)有限公司";
public string userRole { get; set; } = "SuperAdmin"; /* 等等其他属性 */ }
以上便是上述四个项目的代码实现,是不是很简单呢?
然而,我们今天探讨的重点不是上述代码,而是基于identity的登录验证授权机制
我们仔细剖析登录验证授权这六个字,其重点是最后的两个字:授权。
登录验证无非是用户在登录页登录,服务器结合数据库进行账户密码校验,一旦验证通过,我们怎么对当前用户进行授权呢?
针对授权我的理解为:用户通过登录验证后,系统分配一个票据给登录用户,用户在一定的时间内,带着这个票据进行系统访问,系统根据当前登录人所带的票据,判断该用户是否有权限访问相关资源(结合数据库权限表中分配的资源及相关数据权限功能权限)
那么,授权怎么实现呢?如下:
3、完善项目中的 Startup 类,如下:
3.1、在Startup类ConfigureServices方法中添加服务
#region 在Start.cs类中,添加服务 services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, o => { o.Cookie.Name = "_AdminTicketCookie"; o.LoginPath = new PathString("/Account/Login"); o.LogoutPath = new PathString("/Account/LoginOut"); o.AccessDeniedPath = new PathString("/Error/Forbidden"); o.ExpireTimeSpan = TimeSpan.FromHours(4);//4小时后 Ticket过期 }); services.AddTransient<IAdminService, AdminService>(); #endregion
3.2、在Startup类Configure方法中注册授权认证中间件,如下:
#region 添加授权中间件 app.UseAuthentication();//添加授权认证中间件 #endregion
3.3、整个Startup类如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpsPolicy; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NetCoreInterface; using NetCoreService; namespace NetCoreAuth { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.Configure<CookiePolicyOptions>(options => { // This lambda determines whether user consent for non-essential cookies is needed for a given request. options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); #region 在Start.cs类中,添加服务 services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, o => { o.Cookie.Name = "_AdminTicketCookie"; o.LoginPath = new PathString("/Account/Login"); o.LogoutPath = new PathString("/Account/LoginOut"); o.AccessDeniedPath = new PathString("/Error/Forbidden"); o.ExpireTimeSpan = TimeSpan.FromHours(4);//4小时后 Ticket过期 }); services.AddTransient<IAdminService, AdminService>(); #endregion services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); #region 添加授权中间件 app.UseAuthentication();//添加授权认证中间件 #endregion app.UseCookiePolicy(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Account}/{action=Login}/{id?}"); }); } } }
4、相关控制器及视图代码
4.1、核心代码,用于登录用户授权,如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public class AccountController : Controller { public IAdminService _AdminService; public ILoggerFactory _Logger; public AccountController(IAdminService Service, ILoggerFactory LoggerService) { _AdminService = Service; _Logger = LoggerService; } public IActionResult Login() { return View(); } /// <summary> /// <![CDATA[登陆]]> /// </summary> /// <param name="model"></param> /// <returns></returns> [HttpPost] public async Task<ActionResult> UserLogin(Models.LoginViewModel model) { try { //模型验证通过后 if (ModelState.IsValid) { var admin = _AdminService.CheckAccountAndPassword("chenwolong","123456"); //验证用户名密码 if (admin != null) { /*绝对过期时间可以设置为ExpiresUtc。 若要创建持久性 cookie, IsPersistent还必须设置。 否则,cookie 是使用基于会话的生存期创建的,并且可能会在它所包含的身份验证票证之前或之后过期。 设置ExpiresUtc后,它将覆盖的ExpireTimeSpan选项的CookieAuthenticationOptions值(如果已设置)。 IsPersistent =false,//关闭浏览器 Cookies自动清除 IsPersistent =true,//关闭浏览器 Cookies不会自动清除 到设置的时间后,才会自动清除 */ var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);//一定要声明AuthenticationScheme identity.AddClaim(new Claim(ClaimTypes.Name, admin.userAccount)); identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, admin.userId)); identity.AddClaim(new Claim(ClaimTypes.Role, admin.userRole)); identity.AddClaim(new Claim(ClaimTypes.Actor, admin.userName)); await HttpContext.SignInAsync(identity.AuthenticationType, new ClaimsPrincipal(identity), new AuthenticationProperties { IsPersistent =false,//关闭浏览器 Cookies自动清除 RedirectUri = "/Home/Index", ExpiresUtc = new System.DateTimeOffset(dateTime: DateTime.Now.AddHours(6)), }); } else { await HttpContext.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme); ModelState.AddModelError("", "用户名或密码错误!"); } } else { var ErrData = ModelState.Where(x => x.Value.Errors.Count > 0) .Select(x => new { Name = x.Key, Message = x.Value.Errors?.FirstOrDefault().ErrorMessage }).ToList(); string ErrorMessage = string.Empty; foreach (var item in ErrData) { if (ErrData.IndexOf(item) == ErrData.Count - 1) { ErrorMessage += item.Message + "。"; } else { ErrorMessage += item.Message + ","; } } return Json(CommonBaseResponse.SetResponse(false, ErrorMessage)); } } catch (Exception ex) { throw ex; } return Json(CommonBaseResponse.SetResponse(true, "登录成功")); } public IActionResult LoginOut() { return View(); } }
4.2、登录方法中引用一个ViewModel,如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
namespace NetCoreAuth.Models { public class LoginViewModel { [Required(AllowEmptyStrings =false,ErrorMessage ="用户名为必填项")] [RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$",ErrorMessage ="用户名必须包含字母大小写")] public string Account { get; set; } [Required(AllowEmptyStrings = false, ErrorMessage = "用户登录密码为必填项")] [MinLength(6,ErrorMessage ="密码最小长度为6位")] public string Password { get; set; } } }
4.3、登录页面如下:
页面只是简单的做了实现,HTML编码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Login</title> <script src="~/lib/jquery/dist/jquery.js"></script> <script type="text/javascript"> function Login() { var Account = $("#Account").val(); var Password = $("#Password").val(); var UserData = { Account: Account, Password: Password }; $.post("/Account/UserLogin", UserData, function (result) { if (result.isSuccess) { window.location.href = "/home/index"; } else { alert(result.resultMessage) } }); } </script> </head> <body> <div>用户名:<input id="Account" name="Account" type="text" /></div> <div>密码:<input id="Password" name="Password" type="text" /></div> <div> <input id="Button1" type="button" onclick="Login()" value="登录" /> </div> </body> </html>
通过HTML编码中的JS方法,我们知道,用户登录授权后,会跳转至/Home/Index
那么,在Home/Index页面中,我们如何判断当前用户是否通过登录验证授权?
如下:
继承自BaseController,代码如下:
public class BaseController : Controller { public override void OnActionExecuting(ActionExecutingContext context) { var data = context.HttpContext.User; //没有通过登录验证授权 if (data.Identity.IsAuthenticated == false) { Response.Redirect("/Account/Login"); } } }
方法 OnActionExecuting 在基类视图方法运行之前执行,因此,在基类视图呈现之前,我们有必须判断登录人携带的凭据。
当然,高手也可以通过过滤器或者自定义中间件来判断携带的凭据是否合法。
@天才卧龙的博客