当前位置: 移动技术网 > IT编程>开发语言>.net > 详解ASP.NET Core Token认证

详解ASP.NET Core Token认证

2017年12月12日  | 移动技术网IT编程  | 我要评论

秦惠文王王后,模拟修真门派,2080雪儿

令牌认证(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!

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

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网