详解ASP.NET Core Token认证
令牌认证(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中。
测试下:
其他方案
下面是比较成熟的项目,可以在实际项目中使用:
- aspnet.security.openidconnect.server – asp.net 4.x的验证中间件。
- openiddict – 在identity上添加openid验证。
- identityserver4 – .net core认证中间件(现在测试版本)。
下面的文章可以让你更加的了解认证:
- overview of token authentication features
- how token authentication works in stormpath
- use jwts the right way!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
推荐阅读
-
浅谈如何在ASP.NET Core中实现一个基础的身份认证
-
详解ASP.NET Core和ASP.NET Framework共享身份验证
-
详解ASP.NET Core Token认证
-
在ASP.NET Core中实现一个Token base的身份认证实例
-
详解如何在ASP.NET Core中应用Entity Framework
-
详解在ASP.NET Core中使用Angular2以及与Angular2的Token base身份认证
-
浅谈ASP.NET Core 中间件详解及项目实战
-
详解如何在ASP.NET Core中使用Redis
-
【翻译】使用WebApi和Asp.Net Core Identity 认证 Blazor WebAssembly(Blazor客户端应用)
-
Asp.Net Core控制器如何接收原始请求正文内容详解