6月15日,在端午节前的最后一个工作日,想起有段日子没有写过文章了,倒有些荒疏了。今借夏日蒸蒸之气,偷得浮生半日悠闲。闲话就说到这里吧,提前祝大家端午愉快(屈原听了该不高兴了:))!
.NetCore自发布以来,颇受关注,现在.Net Core2.0已经正式发布,边迫不及待的将.Net跨平台移植的工作进行到底。想来,也费不了多少事儿。我经常和同事们说,要敢于尝试新鲜事物,不阴损守旧,方能使自己不断进步,站在队伍的前列。下面就关于Asp.Net Core在Web 及API项目上身份认证的问题做下简单的阐述。
一、Asp.Net Core Web项目的登录认证
在MVC Web项目中,做用户登录授权,是必不可少的工作,不知道大家平时是怎么做的,我想,大多朋友还是使用微软提供的一套认证机制,可以省去很多功夫。从WebForm时代的Form身份认证,无非是通过客户端Cookie中存储认证票据,在请求受保护的资源时,通过Cookie中携带的身份票据,再有Asp.net的认证模块,完整对请求者的身份认证。这一过程,是很清晰简单的了。在MVC中,大多是通过中间件(MiddleWare)来完整认证授权过程。在ASP.NETMVC中,我们了解到基于声明的授权认证(Claim),这种认证方式,好处在于,我们想在用户授权时,存储多个属性信息,只需要添加多个声明即可,我们在微软的认证中间件中,看到的都是定义好的常量,当然,我们可以定义自己的ClaimTypes。然我们看看微软在.NetCore中定义的一些声明吧:
// // Summary: // http://schemas.xmlsoap.org/ws/2009/09/identity/claims/actor. public const string Actor = "http://schemas.xmlsoap.org/ws/2009/09/identity/claims/actor"; // // Summary: // The URI for a claim that specifies the postal code of an entity, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/postalcode. public const string PostalCode = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/postalcode"; // // Summary: // The URI for a claim that specifies the primary group SID of an entity, http://schemas.microsoft.com/ws/2008/06/identity/claims/primarygroupsid. public const string PrimaryGroupSid = "http://schemas.microsoft.com/ws/2008/06/identity/claims/primarygroupsid"; // // Summary: // The URI for a claim that specifies the primary SID of an entity, http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid. public const string PrimarySid = "http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid"; // // Summary: // The URI for a claim that specifies the role of an entity, http://schemas.microsoft.com/ws/2008/06/identity/claims/role. public const string Role = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"; // // Summary: // The URI for a claim that specifies an RSA key, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/rsa. public const string Rsa = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/rsa"; // // Summary: // The URI for a claim that specifies a serial number, http://schemas.microsoft.com/ws/2008/06/identity/claims/serialnumber. public const string SerialNumber = "http://schemas.microsoft.com/ws/2008/06/identity/claims/serialnumber"; // // Summary: // The URI for a claim that specifies a security identifier (SID), http://schemas.xmlsoap.org/ws/2005/05/identity/claims/sid. public const string Sid = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/sid"; // // Summary: // The URI for a claim that specifies a service principal name (SPN) claim, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/spn. public const string Spn = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/spn"; // // Summary: // The URI for a claim that specifies the state or province in which an entity resides, // http://schemas.xmlsoap.org/ws/2005/05/identity/claims/stateorprovince. public const string StateOrProvince = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/stateorprovince"; // // Summary: // The URI for a claim that specifies the street address of an entity, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/streetaddress. public const string StreetAddress = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/streetaddress"; // // Summary: // The URI for a claim that specifies the surname of an entity, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname. public const string Surname = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"; // // Summary: // The URI for a claim that identifies the system entity, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/system. public const string System = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/system"; // // Summary: // The URI for a claim that specifies a thumbprint, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/thumbprint. // A thumbprint is a globally unique SHA-1 hash of an X.509 certificate. public const string Thumbprint = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/thumbprint"; // // Summary: // The URI for a claim that specifies a user principal name (UPN), http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn. public const string Upn = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"; // // Summary: // The URI for a claim that specifies a URI, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/uri. public const string Uri = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/uri"; // // Summary: // http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata. public const string UserData = "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata"; // // Summary: // http://schemas.microsoft.com/ws/2008/06/identity/claims/version. public const string Version = "http://schemas.microsoft.com/ws/2008/06/identity/claims/version"; // // Summary: // The URI for a claim that specifies the webpage of an entity, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/webpage. public const string Webpage = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/webpage"; // // Summary: // The URI for a claim that specifies the Windows domain account name of an entity, // http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname. public const string WindowsAccountName = "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"; // // Summary: // http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsdeviceclaim. public const string WindowsDeviceClaim = "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsdeviceclaim"; // // Summary: // http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsdevicegroup. public const string WindowsDeviceGroup = "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsdevicegroup"; // // Summary: // http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsfqbnversion. public const string WindowsFqbnVersion = "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsfqbnversion"; // // Summary: // http://schemas.microsoft.com/ws/2008/06/identity/claims/windowssubauthority. public const string WindowsSubAuthority = "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowssubauthority"; // // Summary: // The URI for a claim that specifies the alternative phone number of an entity, // http://schemas.xmlsoap.org/ws/2005/05/identity/claims/otherphone. public const string OtherPhone = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/otherphone"; // // Summary: // The URI for a claim that specifies the name of an entity, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier. public const string NameIdentifier = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"; // // Summary: // The URI for a claim that specifies the name of an entity, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name. public const string Name = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"; // // Summary: // The URI for a claim that specifies the mobile phone number of an entity, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone. public const string MobilePhone = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone"; // // Summary: // The URI for a claim that specifies the anonymous user; http://schemas.xmlsoap.org/ws/2005/05/identity/claims/anonymous. public const string Anonymous = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/anonymous"; // // Summary: // The URI for a claim that specifies details about whether an identity is authenticated, // http://schemas.xmlsoap.org/ws/2005/05/identity/claims/authenticated. public const string Authentication = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/authentication"; // // Summary: // The URI for a claim that specifies the instant at which an entity was authenticated; // http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationinstant. public const string AuthenticationInstant = "http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationinstant"; // // Summary: // The URI for a claim that specifies the method with which an entity was authenticated; // http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod. public const string AuthenticationMethod = "http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod"; // // Summary: // The URI for a claim that specifies an authorization decision on an entity; http://schemas.xmlsoap.org/ws/2005/05/identity/claims/authorizationdecision. public const string AuthorizationDecision = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/authorizationdecision"; // // Summary: // The URI for a claim that specifies the cookie path; http://schemas.microsoft.com/ws/2008/06/identity/claims/cookiepath. public const string CookiePath = "http://schemas.microsoft.com/ws/2008/06/identity/claims/cookiepath"; // // Summary: // The URI for a claim that specifies the country/region in which an entity resides, // http://schemas.xmlsoap.org/ws/2005/05/identity/claims/country. public const string Country = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/country"; // // Summary: // The URI for a claim that specifies the date of birth of an entity, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/dateofbirth. public const string DateOfBirth = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/dateofbirth"; // // Summary: // The URI for a claim that specifies the deny-only primary group SID on an entity; // http://schemas.microsoft.com/ws/2008/06/identity/claims/denyonlyprimarygroupsid. // A deny-only SID denies the specified entity to a securable object. public const string DenyOnlyPrimaryGroupSid = "http://schemas.microsoft.com/ws/2008/06/identity/claims/denyonlyprimarygroupsid"; // // Summary: // The URI for a claim that specifies the deny-only primary SID on an entity; http://schemas.microsoft.com/ws/2008/06/identity/claims/denyonlyprimarysid. // A deny-only SID denies the specified entity to a securable object. public const string DenyOnlyPrimarySid = "http://schemas.microsoft.com/ws/2008/06/identity/claims/denyonlyprimarysid"; // // Summary: // The URI for a claim that specifies a deny-only security identifier (SID) for // an entity, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/denyonlysid. // A deny-only SID denies the specified entity to a securable object. public const string DenyOnlySid = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/denyonlysid"; // // Summary: // http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsuserclaim. public const string WindowsUserClaim = "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsuserclaim"; // // Summary: // http://schemas.microsoft.com/ws/2008/06/identity/claims/denyonlywindowsdevicegroup. public const string DenyOnlyWindowsDeviceGroup = "http://schemas.microsoft.com/ws/2008/06/identity/claims/denyonlywindowsdevicegroup"; // // Summary: // http://schemas.microsoft.com/ws/2008/06/identity/claims/dsa. public const string Dsa = "http://schemas.microsoft.com/ws/2008/06/identity/claims/dsa"; // // Summary: // The URI for a claim that specifies the email address of an entity, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email. public const string Email = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress";
那么我们在Asp.Net Core项目中的认证,也是比较简单的。也是通过HttpContext的扩展方法SignInAsync,来传入声明的身份信息。要使用的微软的认证组件,我们在.Net Core Web项目中,做如下改动:
首先,在Start.cs类中,添加服务,具体代码如下:
/// <summary> /// /// </summary> /// <param name="services"></param> public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, o => { o.Cookie.Name = "_AdminTicketCookie"; o.LoginPath = new PathString("/Account/Login"); o.LogoutPath = new PathString("/Account/Login"); o.AccessDeniedPath = new PathString("/Error/Forbidden"); }); services.AddTransient<TiKu.Application.Interfaces.IAdminService, TiKu.Application.AdminService>(); services.AddMvc(); }
其次,添加认证中间件
/// <summary> /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. /// </summary> /// <param name="app"></param> /// <param name="env"></param> public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); } app.UseStaticFiles(); app.UseAuthentication();//添加认证中间件 app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }
最后,在用户登录的地方,登录成功后,调用HttpContext的SignIn方法,将授权信息写入Cookie,示例代码如下:
/// <summary> /// <![CDATA[登陆]]> /// </summary> /// <param name="model"></param> /// <returns></returns> [HttpPost] [ValidateAntiForgeryToken] public async Task<ActionResult> Login(Models.LoginViewModel model) { try { //模型验证通过后 if (ModelState.IsValid) { model.password = TiKu.Common.Security.MD5.Md5(model.password);//MD5加密 TiKu.Domain.Entity.tb_Admin admin = await _AdminService.CheckAccountAndPassword(account: model.account, password: model.password); //验证用户名密码 if (admin != null) { var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);//一定要声明AuthenticationScheme identity.AddClaim(new Claim(ClaimTypes.Name, admin.Account)); identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, admin.Id.ToString())); await HttpContext.SignInAsync(identity.AuthenticationType, new ClaimsPrincipal(identity), new AuthenticationProperties { IsPersistent = model.isPersistent, RedirectUri = "/Home/Index", ExpiresUtc = new System.DateTimeOffset(dateTime: DateTime.Now.AddHours(6)), }); //更新登陆时间 await _AdminService.UpdateLastLoginTime(id: admin.Id); } else { await HttpContext.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme); ModelState.AddModelError("", "用户名或密码错误!"); } } } catch (Exception ex) { ModelState.AddModelError("", "用户名或密码错误!"); _Logger.Error("用户登录时发生错误!", ex); } return View(model); }
这样就完成了Asp.net core web项目的登录认证工作。
二、Asp.Net Core WebApi基于JWT的认证授权
关于JWT的工作原理,大家可以自行了解(https://jwt.io/)。JWT实现了服务端无状态,在分布式服务,会话一致性,单点登录等方面,凸显优势,不占用服务端资源。使用JWT需要注意的是,令牌过期后刷新,以及更改密码后令牌未过期的处理问题。
这里,我以JWT作为.net core webapi项目的认证方式。
首先,我再Api项目中新建了一个名为OAuthController的控制器,定义一个Action名为Token的方法,用来让客户端获取令牌之用,具体代码如下:
/// <summary> /// <![CDATA[获取访问令牌]]> /// </summary> /// <param name="user"></param> /// <param name="password"></param> /// <returns></returns> [HttpPost] public async Task<TiKu.Domain.ValueObject.RestfulData<TiKu.Domain.ValueObject.AccessTokenObj>> Token(string user, string password) { var result = new TiKu.Domain.ValueObject.RestfulData<TiKu.Domain.ValueObject.AccessTokenObj>(); try { if (string.IsNullOrEmpty(user)) throw new ArgumentNullException("user", "用户名不能为空!"); if (string.IsNullOrEmpty(password)) throw new ArgumentNullException("password", "密码不能为空!"); //验证用户名和密码 var userInfo = await _UserService.CheckUserAndPassword(mobile: user, password: password); var claims = new Claim[] { new Claim(ClaimTypes.Name,user), new Claim(ClaimTypes.NameIdentifier,userInfo.Id.ToString()), }; var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(Configuration["JwtSecurityKey"])); var expires = DateTime.Now.AddDays(28);// var token = new JwtSecurityToken( issuer: Configuration["issuer"], audience: Configuration["audience"], claims: claims, notBefore: DateTime.Now, expires: expires, signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)); //生成Token string jwtToken = new JwtSecurityTokenHandler().WriteToken(token); result.code = 1; result.data = new Domain.ValueObject.AccessTokenObj() { AccessToken = jwtToken, Expires = TiKu.Common.Utility.Util.ToUnixTime(expires) }; result.message = "授权成功!"; return result; } catch (Exception ex) { result.message = ex.Message; result.code = 0; logger.Error("获取访问令牌时发生错误!", ex); return result; } }
这里,我定义了一个统一返回数据格式的模型-RestfulData,其中有不返回数据data的RestfulData和带data数据的RestfulData<T>,以及返回集合类型的RestfulArray<T>,具体代码如下:
/// <summary> /// /// </summary> public class RestfulData { /// <summary> /// <![CDATA[错误码]]> /// </summary> public int code { get; set; } /// <summary> ///<![CDATA[消息]]> /// </summary> public string message { get; set; } /// <summary> /// <![CDATA[相关的链接帮助地址]]> /// </summary> public string url { get; set; } } /// <summary> /// /// </summary> /// <typeparam name="T"></typeparam> public class RestfulData<T> : RestfulData { /// <summary> /// <![CDATA[数据]]> /// </summary> public virtual T data { get; set; } } /// <summary> /// <![CDATA[返回数组]]> /// </summary> /// <typeparam name="T"></typeparam> public class RestfulArray<T> : ResultData<IEnumerable<T>> { }
配置JWT认证服务,在Start.cs启动类中,配置如下:
/// <summary> /// /// </summary> /// <param name="services"></param> public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddSingleton<IConfiguration>(Configuration); services.AddMemoryCache();//添加基于内存的缓存支持 services.AddAutofac(); //配置授权 services.AddAuthentication(options => { options.DefaultAuthenticateScheme = "JwtBearer"; options.DefaultChallengeScheme = "JwtBearer"; }).AddJwtBearer("JwtBearer", (jwtBearerOptions) => { jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(Configuration["JwtSecurityKey"])),//秘钥 ValidateIssuer = true, ValidIssuer = Configuration["issuer"], ValidateAudience = true, ValidAudience = Configuration["audience"], ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(5) }; }); services.AddMvc(); //IOC Autofac var builder = new ContainerBuilder(); builder.Populate(services); //注册应用服务 var assemblyApplicationService = System.Reflection.Assembly.Load("TiKu.Application"); builder.RegisterAssemblyTypes(assemblyApplicationService).AsImplementedInterfaces(); var container = builder.Build(); Container = container; return new AutofacServiceProvider(container); }
上面使用了IOC容器Autofac。
其次,配置认证中间件:
/// <summary> /// /// </summary> /// <param name="app"></param> /// <param name="env"></param> public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseAuthentication();//配置授权 //处理异常 app.UseStatusCodePages(new StatusCodePagesOptions() { HandleAsync = (context) => { if (context.HttpContext.Response.StatusCode == 401) { using (System.IO.StreamWriter sw = new System.IO.StreamWriter(context.HttpContext.Response.Body)) { sw.Write(Newtonsoft.Json.JsonConvert.SerializeObject(new { status = 401, message = "access denied!", })); } } return System.Threading.Tasks.Task.Delay(0); } }); app.UseMvc(routes => { routes.MapRoute(name: "default", template: "api/{controller=Home}/{action=Index}/{id?}"); routes.MapRoute(name: "mvc", template: "{controller=Home}/{action=Index}/{id?}"); }); }
为了测试,我们给ValuesController控制器添加Authorize特性。
/// <summary> /// /// </summary> [Authorize] [Route("api/[controller]")] public class ValuesController : Controller { // GET api/values [HttpGet] public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; } }
最后,让我们测试下API的授权,这里,我以Ajax模拟API的调用:
script type="text/javascript"> //获取令牌 $.post("/oauth/token", $.param({ user: "lichaoqiang", password: "fdsfds" })).done(function (data) { if (data.code === 1) { localStorage.setItem("token", data.data.accessToken); } }); //设置HTTP头 $.ajaxSetup({ beforeSend: function (xhr) { if (localStorage.getItem("token") !== null) { xhr.setRequestHeader('Authorization', 'Bearer ' + localStorage.getItem("token")); } } }); $.getJSON("/api/values", function (data) { console.log(data); });//获取受保护的资源 </script>
看下效果,直接访问/api/values,会出现如下图:
当客户请求受保护的资源时,通过HTTP header携带上token。这里需要注意的是,请求头必须是Authorization,值是Bearer空格加上token。这样访问资源时,通过HTTP header携带令牌信息,服务端,通过认证中间件,完成授权认证过程。在上面的示例中,通过向全局Ajax注册事件,将token写入请求Header。、
至此,就完成了JWT认证授权的过程,.Net Core WebAPI配置起来也很简单。