本文版權歸博客園和做者吳雙本人共同全部 轉載和爬蟲請註明原文地址 cnblogs.com/tdwshtml
After using OWIN for months for basic OAuth authentication, it’s apparent that Microsoft is abandoning OWIN . This isn’t necessarily a bad thing. .NET Core is built on a similar structure as that which was implemented in OWIN. Essentially, we have a familiar middleware pipeline.web
這句話出自老外的博客,在使用Owin的OAuth身份認證幾個月後,發現微軟在逐漸放棄OWIN,這未必是一件壞事情,.NET Core在一個和OWIN所實現的類似結構之上。咱們有一個和OWIN極爲類似的中間件管道。redis
想必瞭解或者使用過OWIN的朋友們,在作.NET Core應用的時候都會有如上描述的這種感受。就我我的的理解,微軟在早幾年推出OWIN的時候,就但願將管道留給用戶,就以Startup.cs爲管道配置和應用入口,OWIN脫離了Asp.Net管道事件,咱們能夠將任何中間件在管道中隨意插拔。在OWIN中爲咱們提供了完備的認證流程,和一套完整的規範。好比 Microsoft.Owin.Security.OAuth等,在使用OWIN時,咱們可使用OWIN的默認實現,也能夠實現其接口,自定義咱們本身的實現方式。有關Microsoft OWIN的內容,不是本篇分享的主題,推薦騰飛的 MVC5 - ASP.NET Identity登陸原理 - Claims-based認證和OWIN 和蟋蟀哥的 ASP.NET WebApi OWIN 實現 OAuth 2.0 。數據庫
本篇分享主要關注在.NET Core的認證機制。不管咱們是使用WebApi仍是MvcWeb App,瞭解微軟的認證機制老是有好處的。認證是應用API服務器識別用戶身份的過程,token是更現代的認證方式,簡化權限管理,下降服務器負載。在認證過程當中,最重要的就是拿到token, token包含或者應該包含什麼信息呢?json
1.這我的是誰?api
2.這我的能夠用此token訪問什麼樣的內容?(scope)安全
3.token的過時時間 (expire)服務器
4.誰發行的token。cookie
5.其餘任何你但願加入的聲明(Claims)session
那咱們爲何要使用token呢?使用session或者用redis來實現stateServer很差嗎?
1.token是低(無)狀態的,Statelessness
2.token能夠與移動端應用緊密結合
3.支持多平臺服務器和分佈式微服務
答案是兩種方式,Cookies和Authorization Header。那麼何時放到Cookies中,何時又放到Authentication中呢?
第一,若是是在Web應用,則放到Cookies當中,而且應該是HttpOnly的,js不能直接對其進行操做,安全性會比將其存在Web Stroage中好一些,由於在Web Storage當中的內容,能夠很容的被潛在的XSS腳本攻擊並獲取。在HttpOnly的cookies當中會相對安全一些,不過也有潛在的CSRF跨站僞造請求的危險,不過這種hack的手段成功率是很低的,有興趣的朋友能夠自行看一下CSRF原理。
第二,若是是手機移動端應用的話,那必定是存儲在App本地,並由Authorization Header帶到後臺並獲得身份認證。
上一段前兩週寫的最原始的小Demo吧,沒有數據庫訪問等,可根據demo自行改變 ,如今的新代碼已經加入了不少業務在其中
startup.cs代碼
1 using Microsoft.AspNetCore.Authentication.Cookies; 2 using Microsoft.AspNetCore.Builder; 3 using Microsoft.AspNetCore.Hosting; 4 using Microsoft.AspNetCore.Http; 5 using Microsoft.AspNetCore.Http.Authentication; 6 using Microsoft.Extensions.Configuration; 7 using Microsoft.Extensions.DependencyInjection; 8 using Microsoft.Extensions.Logging; 9 using System.Collections.Generic; 10 using System.Security.Claims; 11 using Wings.AuthenticationApp.Middleware; 12 13 namespace Wings.AuthenticationApp 14 { 15 public class Startup 16 { 17 public Startup(IHostingEnvironment env) 18 { 19 var builder = new ConfigurationBuilder() 20 .SetBasePath(env.ContentRootPath) 21 .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) 22 .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) 23 .AddEnvironmentVariables(); 24 Configuration = builder.Build(); 25 26 } 27 28 public IConfigurationRoot Configuration { get; } 29 30 // This method gets called by the runtime. Use this method to add services to the container. 31 public void ConfigureServices(IServiceCollection services) 32 { 33 // Add framework services. 34 services.AddMvc(); 35 } 36 37 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 38 public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 39 { 40 loggerFactory.AddConsole(Configuration.GetSection("Logging")); 41 loggerFactory.AddDebug(); 42 43 app.UseCookieAuthentication(CookieAuthMiddleware.GetOptions()); 44 app.UseOwin(); 45 app.UseCors(a => { a.AllowAnyOrigin(); }); 46 app.UseMvc(); 47 // Listen for login and logout requests 48 app.Map("/login", builder => 49 { 50 builder.Run(async context => 51 { 52 var name = context.Request.Form["name"]; 53 var pwd = context.Request.Form["pwd"]; 54 if (name == "wushuang" && pwd == "wushuang") 55 { 56 57 var claims = new List<Claim>() { new Claim("name", name), new Claim("role", "admin") }; 58 var identity = new ClaimsIdentity(claims, "password"); 59 var principal = new ClaimsPrincipal(identity); 60 await context.Authentication.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal); 61 context.Response.Redirect("http://www.baidu.com"); 62 } 63 else 64 { 65 await context.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); 66 context.Response.Redirect("http://www.google.com"); 67 } 68 }); 69 }); 70 71 //app.Map("/logout", builder => 72 //{ 73 // builder.Run(async context => 74 // { 75 // // Sign the user out / clear the auth cookie 76 // await context.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); 77 78 // // Perform a simple redirect after logout 79 // context.Response.Redirect("/"); 80 // }); 81 //}); 82 83 } 84 85 } 86 }
下面是Middleware---->CookieAuthMiddleware.cs的代碼,
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Security.Principal; using System.Threading.Tasks; namespace Wings.AuthenticationApp.Middleware { public class CookieAuthMiddleware { public static CookieAuthenticationOptions GetOptions() { return new CookieAuthenticationOptions { AutomaticAuthenticate = true, AutomaticChallenge = true, LoginPath = new PathString("/login"), LogoutPath = new PathString("/logout"), AccessDeniedPath = new PathString("/test"), CookieHttpOnly = false, //默認就是True了 CookieName = "wings_access_token", SlidingExpiration = true, CookieManager = new ChunkingCookieManager() }; } } public static class IdentityExtension { public static string FullName(this IIdentity identity) { var claim = ((ClaimsIdentity)identity).FindFirst("name"); return (claim != null) ? claim.Value : string.Empty; } public static string Role(this IIdentity identity) { var claim = ((ClaimsIdentity)identity).FindFirst("role"); return (claim != null) ? claim.Value : string.Empty; } } }
對應如上demo,簡單測試一下,結果以下:
首先使用錯誤的密碼,來請求token endpoint,接下來咱們看一下即便窗口,當有請求進入的時候,我用以下代碼判斷用戶的認證狀況,拿到的結果必然是false:
接下來,我使用正確的帳號密碼,來打入token,判斷結果必定爲true,因此我使用自定義的拓展方法,來獲取下,該用戶token的信息:
如上demo沒有加入一些容錯機制,請注意。在用戶認證成功後,能夠進入帶有Authorize Attribute的Action,不然401.以下是幾個重要參數的解釋
Startup.cs
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Threading.Tasks; 5 using Microsoft.AspNetCore.Builder; 6 using Microsoft.AspNetCore.Hosting; 7 using Microsoft.Extensions.Configuration; 8 using Microsoft.Extensions.DependencyInjection; 9 using Microsoft.Extensions.Logging; 10 using Wings.TokenAuth.Middleware; 11 using System.Security.Claims; 12 using Microsoft.IdentityModel.Tokens; 13 using System.Text; 14 using Microsoft.Extensions.Options; 15 16 namespace Wings.TokenAuth 17 { 18 public class Startup 19 { 20 public Startup(IHostingEnvironment env) 21 { 22 var builder = new ConfigurationBuilder() 23 .SetBasePath(env.ContentRootPath) 24 .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) 25 .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) 26 .AddEnvironmentVariables(); 27 Configuration = builder.Build(); 28 } 29 30 public IConfigurationRoot Configuration { get; } 31 32 // This method gets called by the runtime. Use this method to add services to the container. 33 public void ConfigureServices(IServiceCollection services) 34 { 35 // Add framework services. 36 services.AddMvc(); 37 } 38 39 // The secret key every token will be signed with. 40 // In production, you should store this securely in environment variables 41 // or a key management tool. Don't hardcode this into your application! 42 private static readonly string secretKey = "mysupersecret_secretkey!123"; 43 44 public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 45 { 46 loggerFactory.AddConsole(LogLevel.Debug); 47 loggerFactory.AddDebug(); 48 49 app.UseStaticFiles(); 50 51 // Add JWT generation endpoint: 52 var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey)); 53 var options = new TokenProviderOptions 54 { 55 Audience = "ExampleAudience", 56 Issuer = "ExampleIssuer", 57 SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256), 58 }; 59 60 app.UseMiddleware<TokenProviderMiddleware>(Options.Create(options)); 61 62 app.UseMvc(); 63 } 64 } 65 }
TokenProviderOptions.cs
1 using Microsoft.AspNetCore.Http; 2 using Microsoft.Extensions.Options; 3 using Microsoft.IdentityModel.Tokens; 4 using Newtonsoft.Json; 5 using System; 6 using System.Collections.Generic; 7 using System.IdentityModel.Tokens.Jwt; 8 using System.Linq; 9 using System.Security.Claims; 10 using System.Threading.Tasks; 11 12 namespace Wings.TokenAuth.Middleware 13 { 14 public class TokenProviderOptions 15 { 16 public string Path { get; set; } = "/token"; 17 18 public string Issuer { get; set; } 19 20 public string Audience { get; set; } 21 22 public TimeSpan Expiration { get; set; } = TimeSpan.FromMinutes(5); 23 24 public SigningCredentials SigningCredentials { get; set; } 25 } 26 public class TokenProviderMiddleware 27 { 28 private readonly RequestDelegate _next; 29 private readonly TokenProviderOptions _options; 30 31 public TokenProviderMiddleware( 32 RequestDelegate next, 33 IOptions<TokenProviderOptions> options) 34 { 35 _next = next; 36 _options = options.Value; 37 } 38 39 public Task Invoke(HttpContext context) 40 { 41 // If the request path doesn't match, skip 42 if (!context.Request.Path.Equals(_options.Path, StringComparison.Ordinal)) 43 {
//use new JwtSecurityTokenHandler().ValidateToken() to valid token 44 return _next(context); 45 } 46 47 // Request must be POST with Content-Type: application/x-www-form-urlencoded 48 if (!context.Request.Method.Equals("POST") 49 || !context.Request.HasFormContentType) 50 { 51 context.Response.StatusCode = 400; 52 return context.Response.WriteAsync("Bad request."); 53 } 54 55 return GenerateToken(context); 56 } 57 private async Task GenerateToken(HttpContext context) 58 { 59 var username = context.Request.Form["username"]; 60 var password = context.Request.Form["password"]; 61 62 var identity = await GetIdentity(username, password); 63 if (identity == null) 64 { 65 context.Response.StatusCode = 400; 66 await context.Response.WriteAsync("Invalid username or password."); 67 return; 68 } 69 70 var now = DateTime.UtcNow; 71 72 // Specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims. 73 // You can add other claims here, if you want: 74 var claims = new Claim[] 75 { 76 new Claim(JwtRegisteredClaimNames.Sub, username), 77 new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), 78 new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(now).ToString(), ClaimValueTypes.Integer64) 79 }; 80 81 // Create the JWT and write it to a string 82 var jwt = new JwtSecurityToken( 83 issuer: _options.Issuer, 84 audience: _options.Audience, 85 claims: claims, 86 notBefore: now, 87 expires: now.Add(_options.Expiration), 88 signingCredentials: _options.SigningCredentials); 89 var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt); 90 91 var response = new 92 { 93 access_token = encodedJwt, 94 expires_in = (int)_options.Expiration.TotalSeconds 95 }; 96 97 // Serialize and return the response 98 context.Response.ContentType = "application/json"; 99 await context.Response.WriteAsync(JsonConvert.SerializeObject(response, new JsonSerializerSettings { Formatting = Formatting.Indented })); 100 } 101 102 private Task<ClaimsIdentity> GetIdentity(string username, string password) 103 { 104 // DON'T do this in production, obviously! 105 if (username == "wushuang" && password == "wushuang") 106 { 107 return Task.FromResult(new ClaimsIdentity(new System.Security.Principal.GenericIdentity(username, "Token"), new Claim[] { })); 108 } 109 110 // Credentials are invalid, or account doesn't exist 111 return Task.FromResult<ClaimsIdentity>(null); 112 } 113 114 public static long ToUnixEpochDate(DateTime date) 115 => (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds); 116 117 118 } 119 }
下面上測試結果:
使用錯誤的帳戶和密碼請求token
使用正確的帳戶和密碼來請求,返回結果以下:
參考文章和論文,不只限於以下幾篇,感謝國外大佬們有深度的分享:
http://stackoverflow.com/questions/29055477/oauth-authorization-service-in-asp-net-core
https://stormpath.com/blog/token-authentication-asp-net-core
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware#fundamentals-middleware
https://docs.microsoft.com/en-us/aspnet/core/security/authentication/cookie#controlling-cookie-options
https://stormpath.com/blog/token-authentication-asp-net-core