asp.net core的cookie认证
以前都是重复造轮子,最近研究asp.net core的认证和授权,用起来还是很方便的。
配置类:
using Auth.Core.ComponentExtensions; using Auth.Core.Configuration; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.Extensions.DependencyInjection; namespace Auth.Core.ServiceExtensions { public static class CookieAuthenticationService { /// <summary> /// 注册常用的工具帮助类 /// </summary> /// <param name="services"></param> public static void AddCookieAuthenticationService(this IServiceCollection services) { GlobalConfig globalConfig = GlobalVars.GlobalConfig; //身份认证 services.AddAuthentication(options => { //只配置了DefaultScheme,这样,DefaultSignInScheme, DefaultSignOutScheme, DefaultChallengeScheme, DefaultForbidScheme 等都会使用该 Scheme 作为默认值 //注意新版本必须设置,否则会报错 options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; }) .AddCookie(options => { options.Cookie.Name = globalConfig.AuthConfig.CookieName; //设置了HttpOnly避免XSS攻击。 options.Cookie.HttpOnly = true; options.Cookie.Domain = globalConfig.AuthConfig.CookieDomain; options.ExpireTimeSpan = System.TimeSpan.FromSeconds(globalConfig.AuthConfig.ExpireTime); //authenticationPropertie中的设置时间优先 //options.ReturnUrlParameter = "abc"; //options.LoginPath = new PathString("/Account/Login"); //options.AccessDeniedPath = new PathString("/Account/Denied"); //options.LogoutPath = new PathString("/Account/Logout"); options.Cookie.Path = "/"; options.TicketDataFormat = new TicketDataFormat(new AesDataProtector(globalConfig.AuthConfig.SecurityKey)); options.SlidingExpiration = true; //是否平滑过期 options.Events = new CookieAuthenticationEvents { ///自定义验证事件 //OnValidatePrincipal = CustomCookieAuthenticationEvents.OnValidateAsync, //未登录跳转事件 OnRedirectToLogin = CustomCookieAuthenticationEvents.OnRedirectToLogin, //无权限跳转事件 OnRedirectToAccessDenied = CustomCookieAuthenticationEvents.OnRedirectToAccessDenied, ////签发前的事件 OnSigningIn = CustomCookieAuthenticationEvents.OnSigningIn, }; // 在这里可以根据需要添加一些Cookie认证相关的配置,在本次示例中使用默认值就可以了。 }); } } }
这里重定义了加密方式:options.TicketDataFormat = new TicketDataFormat(new AesDataProtector(globalConfig.AuthConfig.SecurityKey)); 默认的加密方式太长了。
加密类代码如下:
using HuaTuo.Utils; using Microsoft.AspNetCore.DataProtection; namespace Auth.Core.ServiceExtensions { internal class AesDataProtector : IDataProtector { private string _key; public AesDataProtector(string key) { this._key = key; } public IDataProtector CreateProtector(string purpose) { return this; } /// <summary> /// 只对Claim中的CustomSerializationData属性行了保护,并非整个ClaimsPrincipal对象 /// </summary> /// <param name="plaintext"></param> /// <returns></returns> public byte[] Protect(byte[] plaintext) { string text=System.Text.Encoding.UTF8.GetString(plaintext); string enText =""; try { enText = AESHelper.Encrypt(text, this._key); } catch { } return System.Text.Encoding.UTF8.GetBytes(enText); } public byte[] Unprotect(byte[] protectedData) { string text = System.Text.Encoding.UTF8.GetString(protectedData); string deText =""; try { deText = AESHelper.Decrypt(text, this._key); } catch { } return System.Text.Encoding.UTF8.GetBytes(deText); } } }
这里采用了aes对称加密,AESHelper为工具类提供的帮助类,这里就不提供了。
下面是自定义时间类,默认的没有认证通过会进行页面跳转,对于前后端分离项目是很不友好的,所以重定义对自带的几个event进行了自定义。
options.Events = new CookieAuthenticationEvents { ///自定义验证事件 //OnValidatePrincipal = CustomCookieAuthenticationEvents.OnValidateAsync, //未登录跳转事件 OnRedirectToLogin = CustomCookieAuthenticationEvents.OnRedirectToLogin, //无权限跳转事件 OnRedirectToAccessDenied = CustomCookieAuthenticationEvents.OnRedirectToAccessDenied, ////签发前的事件 OnSigningIn = CustomCookieAuthenticationEvents.OnSigningIn, };
自定义事件类代码如下:
using HuaTuo.Utils; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; using System; using System.Text; using System.Threading.Tasks; namespace Auth.Core.ComponentExtensions { //cookie认证的aop事件 public class CustomCookieAuthenticationEvents { /// <summary> /// 认证通过后才会进入此任务 /// </summary> /// <param name="context"></param> /// <returns></returns> public static Task OnValidateAsync(CookieValidatePrincipalContext context) { //TipsInfo tipsInfo = new TipsInfo() //{ // Status = 0, // Code = StatusCodes.Status401Unauthorized, // Msg = "对不起,你没有权限访问!" //}; //context.Response.ContentType = "application/json"; //return context.Response.WriteAsync(JsonHelper.JsonParse(tipsInfo), Encoding.UTF8); //var userPrincipal = context.Principal; //if (userPrincipal?.Identity?.IsAuthenticated??false) //{ // context.ShouldRenew = true; //} //else //{ // context.RejectPrincipal(); // //退出,清理cookie // context.HttpContext.SignOutAsync(); //} ////主动删除需要和数据库或redis配合,如用户修改了密码,redis或数据库中插入uid和userPrincipal中的claim进行对比,如果存在则context.RejectPrincipal; return Task.CompletedTask; } ///自定义跳转事件 public static Task OnRedirectToLogin(RedirectContext<CookieAuthenticationOptions> context) { //context.Response.StatusCode = StatusCodes.Status401Unauthorized; TipsInfo tipsInfo = new TipsInfo() { Status = 0, Code = StatusCodes.Status401Unauthorized, Msg = "对不起,未登录或登录超时!" }; context.Response.ContentType = "application/json"; return context.Response.WriteAsync(JsonHelper.JsonParse(tipsInfo), Encoding.UTF8); } /// <summary> /// 签发令牌前的aop,注意:签发到前端的cookie是对密文进行了base64加密 /// </summary> /// <param name="context"></param> /// <returns></returns> internal static Task OnSigningIn(CookieSigningInContext context) { AuthenticationProperties authenticationProperties = new AuthenticationProperties(); context.Properties.AllowRefresh =false;//自动刷新cookie context.Properties.IssuedUtc = DateTime.UtcNow;//签发时间 context.Properties.ExpiresUtc = DateTime.UtcNow.Add(context.Options.ExpireTimeSpan); context.Properties.IsPersistent = true; //必须设置,否则ExpiresUtc无效 return Task.CompletedTask; } internal static Task OnRedirectToAccessDenied(RedirectContext<CookieAuthenticationOptions> context) { var user = context.HttpContext.User.Identity; context.Response.StatusCode = StatusCodes.Status403Forbidden; TipsInfo tipsInfo = new TipsInfo() { Status = 0, Code = StatusCodes.Status403Forbidden, Msg = "对不起,您没有权限访问此接口!" }; context.Response.ContentType = "application/json"; return context.Response.WriteAsync(JsonHelper.JsonParse(tipsInfo), Encoding.UTF8); } } }
前端登录接口的控制器代码如下:
[HttpPost] public Task<TipsInfo> GetToken(LoginDto loginDto) { //通过services层检测用户名密码省略.... AuthConfig authConfig = GlobalVars.GlobalConfig.AuthConfig; TipsInfo tipsInfo = new TipsInfo(); //cookie令牌,签发到前端的cookie对密文又进行了base64加密 var claimIdentity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme); claimIdentity.AddClaim(new Claim("Uid", "1")); claimIdentity.AddClaim(new Claim(ClaimTypes.Role, "admin")); claimIdentity.AddClaim(new Claim(ClaimTypes.Name, "guizhoumen")); claimIdentity.AddClaim(new Claim(ClaimTypes.Email, "email")); claimIdentity.AddClaim(new Claim(ClaimTypes.MobilePhone, "13318250159")); var claimsPrincipal = new ClaimsPrincipal(claimIdentity); // 在OnSigningIn事件中配置后,下面的可以注释掉。 //AuthenticationProperties authenticationProperties = new AuthenticationProperties(); //authenticationProperties.AllowRefresh = true;//自动刷新cookie //authenticationProperties.IssuedUtc = DateTime.UtcNow;//签发时间 //authenticationProperties.ExpiresUtc = DateTime.UtcNow.AddHours(2); //authenticationProperties.IsPersistent = true;
//HttpContext.SignInAsync(claimsPrincipal, authenticationProperties); HttpContext.SignInAsync(claimsPrincipal); return Task.FromResult(tipsInfo); }
这里需要注意,如果需要设置过期时间,必须定义AuthenticationProperties,而且IsPersistent必须设置为true,AllowRefresh表示自动刷新过期时间。
这里注释掉,是因为我把这段提到了OnSigningIn时间中去定义了。