欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

详解ASP.NET Core Token认证

程序员文章站 2022-06-14 15:22:01
令牌认证(token authentication)已经成为单页应用(spa)和移动应用事实上的标准。即使是传统的b/s应用也能利用其优点。优点很明白:极少的服务端数据管理...

令牌认证(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中。
测试下:

详解ASP.NET Core Token认证

详解ASP.NET Core Token认证

其他方案

下面是比较成熟的项目,可以在实际项目中使用:

  • 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!

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。