详解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!

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

(0)

相关推荐

  • VS2015 搭建Asp.net core开发环境的方法

    前言 随着ASP.NET Core 1.0 rtm的发布,网上有许多相关.net core 相关文章,最近正好有时间就尝试VS2015 搭建Asp.net core开发环境,以下是简单的搭建过程,下面来一起看看吧. 步骤如下 一.首先你得装个vs2015 并且保证已经升级至 update3及以上(此处附上一个vs2015带up3的下载链接: ed2k://|file|cn_visual_studio_enterprise_2015_with_update_3_x86_x64_dvd_892329

  • ASP.NET Core应用中与第三方IoC/DI框架的整合

    一.ConfigureServices方法返回的ServiceProvider没有用! 我们可以通过一个简单的实例来说明这个问题.我们先定义了如下这个一个MyServiceProvider,它实际上是对另一个ServiceProvider的封装.简单起见,我们利用一个字典来保存服务接口与实现类型的映射关系,这个关系可以通过调用Registe方法来注册.在提供服务实例的GetService方法中,如果提供的服务类型已经被注册,我们会创建并返回对应的实例对象,否则我们将利用封装的这个ServiceP

  • 详解ASP.NET Core Docker部署

    前言 在前面文章中,介绍了 ASP.NET Core在 macOS,Linux 上基于Nginx和Jexus的发布和部署,本篇文章主要是如何在Docker容器中运行ASP.NET Core应用程序. ASP.NET Nginx 发布和部署 :http://www.cnblogs.com/savorboard/p/dotnet-core-publish-nginx.html. Asp.Net Jexus 发布和部署:http://www.cnblogs.com/savorboard/p/dot-n

  • Amazing ASP.NET Core 2.0

    前言 ASP.NET Core 的变化和发展速度是飞快的,当你发现你还没有掌握 ASP.NET Core 1.0 的时候, 2.0 已经快要发布了,目前 2.0 处于 Preview 1 版本,意味着功能已经基本确定,还没有学习过 ASP.NET Core 的同学可以直接从 2.0 开始学起,但是如果你已经掌握了 1.0 的话,那么你只需要了解在 2.0 中增加和修改的一些功能即可. 每一次大版本的发布和升级,总会带给开发人员一些惊喜和令人兴奋的特性,有关 ASP.NET Core 本次的 2.

  • Visual Studio 2017 ASP.NET Core开发

    Visual Studio 2017 ASP.NET Core开发,Visual Studio 2017 已经内置ASP.NET Core 开发工具. 在选择.NET Core 功能安装以后就可以进行ASP.NET Core开发. 新的ASP.NET Core项目为csproj ,打开之前的xproj项目,会提示单向升级,确认以后,会自动帮你升级至csproj. 新建项目 VS 2017新建ASP.NET Core 项目: 确定以后 可选择ASP.NET Core 1.0 和ASP.NET Co

  • 详解在ASP.NET Core中使用Angular2以及与Angular2的Token base身份认证

    Angular2是对Angular1的一次彻底的,破坏性的更新. 相对于Angular1.x,借用某果的广告语,唯一的不同,就是处处都不同. •首先,推荐的语言已经不再是Javascript,取而代之的TypeScript,(TypeScript = ES6 + 类型系统 + 类型注解), TypeScriipt的类型系统对于开发复杂的单页Web app大有帮助,同时编译成javascript后的执行效率也比大多数手写javascript要快.有兴趣的同学可以查阅官方文档:英文传送门 |中文传送

  • Windows Server 2012 R2 Standard搭建ASP.NET Core环境图文教程

    前言: 随着ASP.NET Core 1.0的发布,论坛里相关的文章也越来越多,正好有时间在测试环境上搭建 ASP.NET Core的发布环境,把过程中遇到的问题写给大家,以便有用到的朋友需要. 环境: Windows Server 2012 R2 Standard with Update MSDN 链接:ed2k://|file|cn_windows_server_2012_r2_with_update_x64_dvd_6052725.iso|5545705472|121EC13B53882E

  • ASP.NET Core全面扫盲贴

    1. 前言 .NET发行至今已经过了十四个年头.随着版本的不断迭代更新,.NET在Windows平台上的表现也是越来越好,可以说Windows平台上所有的应用类型.NET几乎都能完成. 只是成也Windows,败也Windows,这十四年来,除了部分"民间"版本,.NET一直没能在官方支持下摆脱Windows平台的局限,"开源"和"跨平台"这两个词语也是所有.NET开发者心中的痛楚.最终,.NET Core出现了,它让开发者们在官方和社区的支持走

  • Asp.net Core 初探(发布和部署Linux)

    前言 俗话说三天不学习,赶不上刘少奇.Asp.net Core更新这么长时间一直观望,周末帝都小雨,宅在家看了下Core Web App,顺便搭建了个HelloWorld环境来尝尝鲜,第一次看到.Net Web运行在Linux上还是有点小激动(只可惜微软走这一步路走的太晚,要不然屌丝们也不会每每遇见Java VS .Net就想辩论个你死我活). 开发环境和部署环境 Windows 10.VS2015 Update3.安装.Net Core SDK.DotNetCore.1.0.1-VS2015T

  • Visual Studio 2017下ASP.NET CORE的TagHelper智能提示解决办法

    之前在VS2017RC中就发现该问题,安装了依赖,但是前段一直点不出来asp-for,后来查了发行说明, 才知道在VS2017rc中暂时无法解决,所以一直等到VS2017正式版的发布,急冲冲的装好, 建了一个demo项目,还是无法出现TagHelper的智能提示. 不死心,我又去扒拉了一下VS2017的发行说明,找了一下已知问题: 发现有这一行: 然后我根据提示,进入到Github页面,果然找到了TagHelper为何不能使用的描述: 同样,页面给出来了解决办法,安装一个Razor服务的扩展:

随机推荐