令牌認證(Token Authentication)已經成為單頁應用(SPA)和移動應用事實上的標准。即使是傳統的B/S應用也能利用其優點。優點很明白:極少的服務端數據管理、可擴展性、可以使用單獨的認證服務器和應用服務器分離。
如果你對令牌(token)不是太了解,可以看這篇文章( overview of token authentication and JWTs)
令牌認證在asp.net core中集成。其中包括保護Bearer Jwt的路由功能,但是移除了生成token和驗證token的部分,這些可以自定義或者使用第三方庫來實現,得益於此,MVC和Web api項目可以使用令牌認證,而且很簡單。下面將一步一步實現,代碼可以在( 源碼)下載。
ASP.NET Core令牌驗證
首先,背景知識:認證令牌,例如JWTs,是通過http 認證頭傳遞的,例如:
GET /foo Authorization: Bearer [token]
令牌可以通過浏覽器cookies。傳遞方式是header或者cookies取決於應用和實際情況,對於移動app,使用headers,對於web,推薦在html5 storage中使用cookies,來防止xss攻擊。
asp.net core對jwts令牌的驗證很簡單,特別是你通過header傳遞。
1、生成 SecurityKey,這個例子,我生成對稱密鑰驗證jwts通過HMAC-SHA256加密方式,在startup.cs中:
// secretKey contains a secret passphrase only your server knows var secretKey = "mysupersecret_secretkey!123"; var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey));
驗證 header中傳遞的JWTs
在 Startup.cs中,使用Microsoft.AspNetCore.Authentication.JwtBearer中的UseJwtBearerAuthentication 方法獲取受保護的api或者mvc路由有效的jwt。
var tokenValidationParameters = new TokenValidationParameters { // The signing key must match! ValidateIssuerSigningKey = true, IssuerSigningKey = signingKey, // Validate the JWT Issuer (iss) claim ValidateIssuer = true, ValidIssuer = "ExampleIssuer", // Validate the JWT Audience (aud) claim ValidateAudience = true, ValidAudience = "ExampleAudience", // Validate the token expiry ValidateLifetime = true, // If you want to allow a certain amount of clock drift, set that here: ClockSkew = TimeSpan.Zero }; app.UseJwtBearerAuthentication(new JwtBearerOptions { AutomaticAuthenticate = true, AutomaticChallenge = true, TokenValidationParameters = tokenValidationParameters });
通過這個中間件,任何[Authorize]的請求都需要有效的jwt:
簽名有效;
過期時間;
有效時間;
Issuer 聲明等於“ExampleIssuer”
訂閱者聲明等於 “ExampleAudience”
如果不是合法的JWT,請求終止,issuer聲明和訂閱者聲明不是必須的,它們用來標識應用和客戶端。
在cookies中驗證JWTs
ASP.NET Core中的cookies 認證不支持傳遞jwt。需要自定義實現 ISecureDataFormat接口的類。現在,你只是驗證token,不是生成它們,只需要實現Unprotect方法,其他的交給System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler這個類處理。
using System; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http.Authentication; using Microsoft.IdentityModel.Tokens; namespace SimpleTokenProvider { public class CustomJwtDataFormat : ISecureDataFormat<AuthenticationTicket> { private readonly string algorithm; private readonly TokenValidationParameters validationParameters; public CustomJwtDataFormat(string algorithm, TokenValidationParameters validationParameters) { this.algorithm = algorithm; this.validationParameters = validationParameters; } public AuthenticationTicket Unprotect(string protectedText) => Unprotect(protectedText, null); public AuthenticationTicket Unprotect(string protectedText, string purpose) { var handler = new JwtSecurityTokenHandler(); ClaimsPrincipal principal = null; SecurityToken validToken = null; try { principal = handler.ValidateToken(protectedText, this.validationParameters, out validToken); var validJwt = validToken as JwtSecurityToken; if (validJwt == null) { throw new ArgumentException("Invalid JWT"); } if (!validJwt.Header.Alg.Equals(algorithm, StringComparison.Ordinal)) { throw new ArgumentException($"Algorithm must be '{algorithm}'"); } // Additional custom validation of JWT claims here (if any) } catch (SecurityTokenValidationException) { return null; } catch (ArgumentException) { return null; } // Validation passed. Return a valid AuthenticationTicket: return new AuthenticationTicket(principal, new AuthenticationProperties(), "Cookie"); } // This ISecureDataFormat implementation is decode-only public string Protect(AuthenticationTicket data) { throw new NotImplementedException(); } public string Protect(AuthenticationTicket data, string purpose) { throw new NotImplementedException(); } } }
在startup.cs中調用
var tokenValidationParameters = new TokenValidationParameters { // The signing key must match! ValidateIssuerSigningKey = true, IssuerSigningKey = signingKey, // Validate the JWT Issuer (iss) claim ValidateIssuer = true, ValidIssuer = "ExampleIssuer", // Validate the JWT Audience (aud) claim ValidateAudience = true, ValidAudience = "ExampleAudience", // Validate the token expiry ValidateLifetime = true, // If you want to allow a certain amount of clock drift, set that here: ClockSkew = TimeSpan.Zero }; app.UseCookieAuthentication(new CookieAuthenticationOptions { AutomaticAuthenticate = true, AutomaticChallenge = true, AuthenticationScheme = "Cookie", CookieName = "access_token", TicketDataFormat = new CustomJwtDataFormat( SecurityAlgorithms.HmacSha256, tokenValidationParameters) });
如果請求中包含名為access_token的cookie驗證為合法的JWT,這個請求就能返回正確的結果,如果需要,你可以加上額外的jwt chaims,或者復制jwt chaims到ClaimsPrincipal在CustomJwtDataFormat.Unprotect方法中,上面是驗證token,下面將在asp.net core中生成token。
ASP.NET Core生成Tokens
在asp.net 4.5中,這個UseOAuthAuthorizationServer中間件可以輕松的生成tokens,但是在asp.net core取消了,下面寫一個簡單的token生成中間件,最後,有幾個現成解決方案的鏈接,供你選擇。
簡單的token生成節點
首先,生成 POCO保存中間件的選項. 生成類:TokenProviderOptions.cs
using System; using Microsoft.IdentityModel.Tokens; namespace SimpleTokenProvider { public class TokenProviderOptions { public string Path { get; set; } = "/token"; public string Issuer { get; set; } public string Audience { get; set; } public TimeSpan Expiration { get; set; } = TimeSpan.FromMinutes(5); public SigningCredentials SigningCredentials { get; set; } } }
現在自己添加一個中間件,asp.net core 的中間件類一般是這樣的:
using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using Newtonsoft.Json; namespace SimpleTokenProvider { public class TokenProviderMiddleware { private readonly RequestDelegate _next; private readonly TokenProviderOptions _options; public TokenProviderMiddleware( RequestDelegate next, IOptions<TokenProviderOptions> options) { _next = next; _options = options.Value; } public Task Invoke(HttpContext context) { // If the request path doesn't match, skip if (!context.Request.Path.Equals(_options.Path, StringComparison.Ordinal)) { return _next(context); } // Request must be POST with Content-Type: application/x-www-form-urlencoded if (!context.Request.Method.Equals("POST") || !context.Request.HasFormContentType) { context.Response.StatusCode = 400; return context.Response.WriteAsync("Bad request."); } return GenerateToken(context); } } }
這個中間件類接受TokenProviderOptions作為參數,當有請求且請求路徑是設置的路徑(token或者api/token),Invoke方法執行,token節點只對 POST請求而且包括form-urlencoded內容類型(Content-Type: application/x-www-form-urlencoded),因此調用之前需要檢查下內容類型。
最重要的是GenerateToken,這個方法需要驗證用戶的身份,生成jwt,傳回jwt:
private async Task GenerateToken(HttpContext context) { var username = context.Request.Form["username"]; var password = context.Request.Form["password"]; var identity = await GetIdentity(username, password); if (identity == null) { context.Response.StatusCode = 400; await context.Response.WriteAsync("Invalid username or password."); return; } var now = DateTime.UtcNow; // Specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims. // You can add other claims here, if you want: var claims = new Claim[] { new Claim(JwtRegisteredClaimNames.Sub, username), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(now).ToString(), ClaimValueTypes.Integer64) }; // Create the JWT and write it to a string var jwt = new JwtSecurityToken( issuer: _options.Issuer, audience: _options.Audience, claims: claims, notBefore: now, expires: now.Add(_options.Expiration), signingCredentials: _options.SigningCredentials); var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt); var response = new { access_token = encodedJwt, expires_in = (int)_options.Expiration.TotalSeconds }; // Serialize and return the response context.Response.ContentType = "application/json"; await context.Response.WriteAsync(JsonConvert.SerializeObject(response, new JsonSerializerSettings { Formatting = Formatting.Indented })); }
大部分代碼都很官方,JwtSecurityToken 類生成jwt,JwtSecurityTokenHandler將jwt編碼,你可以在claims中添加任何chaims。驗證用戶身份只是簡單的驗證,實際情況肯定不是這樣的,你可以集成 identity framework或者其他的,對於這個實例只是簡單的硬編碼:
private Task<ClaimsIdentity> GetIdentity(string username, string password) { // DON'T do this in production, obviously! if (username == "TEST" && password == "TEST123") { return Task.FromResult(new ClaimsIdentity(new System.Security.Principal.GenericIdentity(username, "Token"), new Claim[] { })); } // Credentials are invalid, or account doesn't exist return Task.FromResult<ClaimsIdentity>(null); }
添加一個將DateTime生成timestamp的方法:
public static long ToUnixEpochDate(DateTime date) => (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);
現在,你可以將這個中間件添加到startup.cs中了:
using System.Text; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; namespace SimpleTokenProvider { public partial class Startup { public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .AddJsonFile("appsettings.json", optional: true); Configuration = builder.Build(); } public IConfigurationRoot Configuration { get; set; } public void ConfigureServices(IServiceCollection services) { services.AddMvc(); } // The secret key every token will be signed with. // In production, you should store this securely in environment variables // or a key management tool. Don't hardcode this into your application! private static readonly string secretKey = "mysupersecret_secretkey!123"; public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(LogLevel.Debug); loggerFactory.AddDebug(); app.UseStaticFiles(); // Add JWT generation endpoint: var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey)); var options = new TokenProviderOptions { Audience = "ExampleAudience", Issuer = "ExampleIssuer", SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256), }; app.UseMiddleware<TokenProviderMiddleware>(Options.Create(options)); app.UseMvc(); } } }
測試一下,推薦使用chrome 的postman:
POST /token Content-Type: application/x-www-form-urlencoded username=TEST&password=TEST123
結果:
OK
Content-Type: application/json
{
"access_token": "eyJhb...",
"expires_in": 300
}
你可以使用jwt工具查看生成的jwt內容。如果開發的是移動應用或者單頁應用,你可以在後續請求的header中存儲jwt,如果你需要在cookies中存儲的話,你需要對代碼修改一下,需要將返回的jwt字符串添加到cookie中。
測試下:
其他方案
下面是比較成熟的項目,可以在實際項目中使用:
下面的文章可以讓你更加的了解認證:
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。