詳解ASP.NET Core Token認(rèn)證
令牌認(rèn)證(Token Authentication)已經(jīng)成為單頁(yè)應(yīng)用(SPA)和移動(dòng)應(yīng)用事實(shí)上的標(biāo)準(zhǔn)。即使是傳統(tǒng)的B/S應(yīng)用也能利用其優(yōu)點(diǎn)。優(yōu)點(diǎn)很明白:極少的服務(wù)端數(shù)據(jù)管理、可擴(kuò)展性、可以使用單獨(dú)的認(rèn)證服務(wù)器和應(yīng)用服務(wù)器分離。
如果你對(duì)令牌(token)不是太了解,可以看這篇文章( overview of token authentication and JWTs)
令牌認(rèn)證在asp.net core中集成。其中包括保護(hù)Bearer Jwt的路由功能,但是移除了生成token和驗(yàn)證token的部分,這些可以自定義或者使用第三方庫(kù)來(lái)實(shí)現(xiàn),得益于此,MVC和Web api項(xiàng)目可以使用令牌認(rèn)證,而且很簡(jiǎn)單。下面將一步一步實(shí)現(xiàn),代碼可以在( 源碼)下載。
ASP.NET Core令牌驗(yàn)證
首先,背景知識(shí):認(rèn)證令牌,例如JWTs,是通過(guò)http 認(rèn)證頭傳遞的,例如:
GET /foo Authorization: Bearer [token]
令牌可以通過(guò)瀏覽器cookies。傳遞方式是header或者cookies取決于應(yīng)用和實(shí)際情況,對(duì)于移動(dòng)app,使用headers,對(duì)于web,推薦在html5 storage中使用cookies,來(lái)防止xss攻擊。
asp.net core對(duì)jwts令牌的驗(yàn)證很簡(jiǎn)單,特別是你通過(guò)header傳遞。
1、生成 SecurityKey,這個(gè)例子,我生成對(duì)稱(chēng)密鑰驗(yàn)證jwts通過(guò)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));
驗(yàn)證 header中傳遞的JWTs
在 Startup.cs中,使用Microsoft.AspNetCore.Authentication.JwtBearer中的UseJwtBearerAuthentication 方法獲取受保護(hù)的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
});
通過(guò)這個(gè)中間件,任何[Authorize]的請(qǐng)求都需要有效的jwt:
簽名有效;
過(guò)期時(shí)間;
有效時(shí)間;
Issuer 聲明等于“ExampleIssuer”
訂閱者聲明等于 “ExampleAudience”
如果不是合法的JWT,請(qǐng)求終止,issuer聲明和訂閱者聲明不是必須的,它們用來(lái)標(biāo)識(shí)應(yīng)用和客戶(hù)端。
在cookies中驗(yàn)證JWTs
ASP.NET Core中的cookies 認(rèn)證不支持傳遞jwt。需要自定義實(shí)現(xiàn) ISecureDataFormat接口的類(lèi)。現(xiàn)在,你只是驗(yàn)證token,不是生成它們,只需要實(shí)現(xiàn)Unprotect方法,其他的交給System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler這個(gè)類(lèi)處理。
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中調(diào)用
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)
});
如果請(qǐng)求中包含名為access_token的cookie驗(yàn)證為合法的JWT,這個(gè)請(qǐng)求就能返回正確的結(jié)果,如果需要,你可以加上額外的jwt chaims,或者復(fù)制jwt chaims到ClaimsPrincipal在CustomJwtDataFormat.Unprotect方法中,上面是驗(yàn)證token,下面將在asp.net core中生成token。
ASP.NET Core生成Tokens
在asp.net 4.5中,這個(gè)UseOAuthAuthorizationServer中間件可以輕松的生成tokens,但是在asp.net core取消了,下面寫(xiě)一個(gè)簡(jiǎn)單的token生成中間件,最后,有幾個(gè)現(xiàn)成解決方案的鏈接,供你選擇。
簡(jiǎn)單的token生成節(jié)點(diǎn)
首先,生成 POCO保存中間件的選項(xiàng). 生成類(lèi):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; }
}
}
現(xiàn)在自己添加一個(gè)中間件,asp.net core 的中間件類(lèi)一般是這樣的:
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);
}
}
}
這個(gè)中間件類(lèi)接受TokenProviderOptions作為參數(shù),當(dāng)有請(qǐng)求且請(qǐng)求路徑是設(shè)置的路徑(token或者api/token),Invoke方法執(zhí)行,token節(jié)點(diǎn)只對(duì) POST請(qǐng)求而且包括form-urlencoded內(nèi)容類(lèi)型(Content-Type: application/x-www-form-urlencoded),因此調(diào)用之前需要檢查下內(nèi)容類(lèi)型。
最重要的是GenerateToken,這個(gè)方法需要驗(yàn)證用戶(hù)的身份,生成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 類(lèi)生成jwt,JwtSecurityTokenHandler將jwt編碼,你可以在claims中添加任何chaims。驗(yàn)證用戶(hù)身份只是簡(jiǎn)單的驗(yàn)證,實(shí)際情況肯定不是這樣的,你可以集成 identity framework或者其他的,對(duì)于這個(gè)實(shí)例只是簡(jiǎn)單的硬編碼:
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);
}
添加一個(gè)將DateTime生成timestamp的方法:
public static long ToUnixEpochDate(DateTime date) => (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);
現(xiàn)在,你可以將這個(gè)中間件添加到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();
}
}
}
測(cè)試一下,推薦使用chrome 的postman:
POST /token Content-Type: application/x-www-form-urlencoded username=TEST&password=TEST123
結(jié)果:
OK
Content-Type: application/json
{
"access_token": "eyJhb...",
"expires_in": 300
}
你可以使用jwt工具查看生成的jwt內(nèi)容。如果開(kāi)發(fā)的是移動(dòng)應(yīng)用或者單頁(yè)應(yīng)用,你可以在后續(xù)請(qǐng)求的header中存儲(chǔ)jwt,如果你需要在cookies中存儲(chǔ)的話,你需要對(duì)代碼修改一下,需要將返回的jwt字符串添加到cookie中。
測(cè)試下:


其他方案
下面是比較成熟的項(xiàng)目,可以在實(shí)際項(xiàng)目中使用:
- AspNet.Security.OpenIdConnect.Server – ASP.NET 4.x的驗(yàn)證中間件。
- OpenIddict – 在identity上添加OpenId驗(yàn)證。
- IdentityServer4 – .NET Core認(rèn)證中間件(現(xiàn)在測(cè)試版本)。
下面的文章可以讓你更加的了解認(rèn)證:
- Overview of Token Authentication Features
- How Token Authentication Works in Stormpath
- Use JWTs the Right Way!
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
asp.net實(shí)現(xiàn)的群發(fā)郵件功能詳解
這篇文章主要介紹了asp.net實(shí)現(xiàn)的群發(fā)郵件功能,結(jié)合具體實(shí)例形式分析了asp.net基于SMTP服務(wù)群發(fā)QQ郵件的相關(guān)操作技巧與注意事項(xiàng),需要的朋友可以參考下2017-05-05
asp.net core razor自定義taghelper的方法
這篇文章主要介紹了asp.net core razor自定義taghelper的方法,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-09-09
Asp.net MVC 對(duì)所有用戶(hù)輸入的字符串字段做Trim處理的方法
這篇文章主要介紹了Asp.net MVC 如何對(duì)所有用戶(hù)輸入的字符串字段做Trim處理,需要的朋友可以參考下2017-06-06
Asp.net的GridView控件實(shí)現(xiàn)單元格可編輯方便用戶(hù)使用
考慮到用戶(hù)使用方便,減少?gòu)棾鲰?yè)面,采用點(diǎn)“編輯”按鈕無(wú)需彈出頁(yè)面直接當(dāng)前行的單元格內(nèi)容就能編輯,思路及代碼如下,有此需求的朋友可以參考下,希望對(duì)大家有所幫助2013-08-08
GridView控件實(shí)現(xiàn)數(shù)據(jù)的修改(第9節(jié))
這篇文章主要介紹了GridView控件實(shí)現(xiàn)數(shù)據(jù)的修改,需要的朋友可以參考下2015-08-08
仿vs實(shí)現(xiàn)WPF好看的進(jìn)度條
由于WPF自帶的進(jìn)度條其實(shí)不怎么好看,而且沒(méi)啥視覺(jué)效果。下面給大家分享的是仿VS的進(jìn)度條效果的代碼,有需要的小伙伴可以參考下。2015-06-06

