ASP.NET Core JWT 和声明

Posted

技术标签:

【中文标题】ASP.NET Core JWT 和声明【英文标题】:ASP.NET Core JWT and Claims 【发布时间】:2018-05-08 11:29:02 【问题描述】:

我有一个关于 ASP.NET Core 和 Claims 中的 JWT 身份验证的问题,因为我不知道我是否正确获取了所有内容。

当我在 ASP.NET 中创建 JWT 令牌时,我添加了一些声明,其中一些可以是自定义的。当带有 JWT 令牌的请求从客户端发送到 API 时会发生什么。 User.Claims 如何填写?它是否使用从 JWT 读取的声明?

我想创建一个自定义身份提供程序(不想使用由 ASP.NET 提供的),并使用我自己的用户数据、角色等表。我不想存储完成所需的所有重要数据JWT 令牌中的策略(令牌中存储的信息量很重要,也很安全)。是否可以在 JWT 令牌中仅存储基本声明(如用户 ID、名称等),然后重新获取其他所需的数据 DB/缓存?除此之外,我想使用 [Authorize] 的标准机制和 Policy 机制。

如何让这一切发挥作用:自定义用户身份 + JWT + 标准 ASP.NET 基于策略的授权 + 在每次请求时从数据库/缓存中获取声明?如何做到这一点?

【问题讨论】:

【参考方案1】:

Asp 网络核心

第一步是编写配置Jwt认证的方法:

// Configure authentication with JWT (Json Web Token).
public void ConfigureJwtAuthService(IServiceCollection services)

  // Enable the use of an [Authorize(AuthenticationSchemes = 
  // JwtBearerDefaults.AuthenticationScheme)]
  // attribute on methods and classes to protect.
  services.AddAuthentication().AddJwtBearer(cfg =>
  
    cfg.RequireHttpsMetadata = false;
    cfg.SaveToken = true;
    cfg.TokenValidationParameters = new TokenValidationParameters()
    
      IssuerSigningKey = JwtController.SecurityKey,
      ValidAudience = JwtController.Audience,
      ValidIssuer = JwtController.Issuer,
      // When receiving a token, check that we've signed it.
      ValidateIssuerSigningKey = true,
      // When receiving a token, check that it is still valid.
      ValidateLifetime = true,
      // This defines the maximum allowable clock skew when validating 
      // the lifetime. As we're creating the tokens locally and validating
      // them on the same machines which should have synchronised time,
      // this can be set to zero.
      ClockSkew = TimeSpan.FromMinutes(0)
    ;
  );

现在在Startup.csConfigureServices()方法中,我们可以调用ConfigureJwtAuthService()方法来配置Jwt认证。

这是完整的Startup.cs

using System;
using Autofac;
using ExpertCodeBlogWebApp.Controllers;
using ExpertCodeBlogWebApp.Domain;
using ExpertCodeBlogWebApp.Domain.Interfaces;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.SpaServices.Webpack;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

using Microsoft.IdentityModel.Tokens;

namespace ExpertCodeBlogWebApp

  public class Startup
  
    public Startup(IConfiguration configuration)
    
      Configuration = configuration;
    

  public IConfiguration Configuration  get; 

  // This method gets called by the runtime. Use this method to add 
  // services to the container.
  public IServiceProvider ConfigureServices(IServiceCollection services)
  
    services.AddMvc();

    // Configure jwt autenticazione 
    ConfigureJwtAuthService(services);

    // Repositories
    services.AddScoped<IUserRepository, UserRepository>();

    // Create the Autofac container builder for dependency injection
    var builder = new ContainerBuilder();

    // Add any Autofac modules or registrations. 
    builder.RegisterModule(new AutofacModule());

    // Return ServiceProvider
    var serviceProvider = services.BuildServiceProvider();
    return serviceProvider;
  

  // Configure authentication with JWT (Json Web Token).
  public void ConfigureJwtAuthService(IServiceCollection services)
  
    // Enable the use of an [Authorize(AuthenticationSchemes = 
    // JwtBearerDefaults.AuthenticationScheme)]
    // attribute on methods and classes to protect.
    services.AddAuthentication().AddJwtBearer(cfg =>
    
      cfg.RequireHttpsMetadata = false;
      cfg.SaveToken = true;

      cfg.TokenValidationParameters = new TokenValidationParameters()
      
        IssuerSigningKey = JwtController.SecurityKey,
        ValidAudience = JwtController.Audience,
        ValidIssuer = JwtController.Issuer,
        // When receiving a token, check that we've signed it.
        ValidateIssuerSigningKey = true,
        // When receiving a token, check that it is still valid.
        ValidateLifetime = true,
        // This defines the maximum allowable clock skew when validating 
        // the lifetime.
        // As we're creating the tokens locally and validating them on the 
        // same machines which should have synchronised time, this can be 
        // set to zero.
        ClockSkew = TimeSpan.FromMinutes(0)
      ;
    );
  

  // This method gets called by the runtime. Use this method to configure 
  // the HTTP request pipeline.
  public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  
    if (env.IsDevelopment())
    
      app.UseDeveloperExceptionPage();
      app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
      
        HotModuleReplacement = true
      );
    
    else
    
      app.UseExceptionHandler("/Home/Error");
    

    app.UseStaticFiles();

    app.UseMvc(routes =>
    
      routes.MapRoute(
      name: "default",
      template: "controller=Home/action=Index/id?");

      routes.MapSpaFallbackRoute(
        name: "spa-fallback",
        defaults: new  controller = "Home", action = "Index" );
      );
    
  

  // For dependency injection.
  public class AutofacModule : Module
  
    // Dependency Injection with Autofact
    protected override void Load(ContainerBuilder builder)
    
      builder.RegisterType<UserRepository>().As<IUserRepository>()
        .SingleInstance();
    
  

JwtController.cs

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
using AutoMapper;
using ExpertCodeBlogWebApp.Domain;
using ExpertCodeBlogWebApp.Domain.Interfaces;
using ExpertCodeBlogWebApp.Domain.Models;
using ExpertCodeBlogWebApp.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;

namespace ExpertCodeBlogWebApp.Controllers


[Route("api/[controller]")]
public class JwtController : Controller

  #region Private Members
  // JWT-related members
  private TimeSpan TokenExpiration;
  private SigningCredentials SigningCredentials;
  // EF and Identity members, available through DI
  private MyDbContext DbContext;
  private IUserRepository _userRepository;
  private readonly ILogger _logger;
  #endregion Private Members

  #region Static Members
  private static readonly string PrivateKey = "my_PrivateKey";
  public static readonly SymmetricSecurityKey SecurityKey = 
    new SymmetricSecurityKey(Encoding.ASCII.GetBytes(PrivateKey));
  public static readonly string Issuer = "my_Issuer";
  public static readonly string Audience = "my_Audience";
  #endregion Static Members

  #region Constructor
  // I have used Autofac in the Startup.cs for dependency injection)
  public JwtController(
    MyDbContext dbContext,
    IUserRepository userRepository,
    ILogger<JwtController> logger)
  
    _logger = logger;
    _userRepository = userRepository;
    // Instantiate JWT-related members
    TokenExpiration = TimeSpan.FromMinutes(10);
    SigningCredentials = new SigningCredentials(SecurityKey, 
      SecurityAlgorithms.HmacSha256);
    // Instantiate through Dependency Injection with Autofact
    DbContext = dbContext;
  
  #endregion Constructor

  #region Public Methods 
  // Manages the request for a new authentication or the refresh of an 
  // already established one
  [HttpPost("token")]
  public async Task<IActionResult> 
    Authentication([FromBody]JwtRequestViewModel jwt)
  
    if (ModelState.IsValid)
    
      string grantType = jwt.GrantType; 
      if (grantType == "password")
      
        string userName = jwt.UserName;
        string password = jwt.Password;

        // Password check required
        var user = await 
          _userRepository.GetUserInfoWithCheckPwd(userName, password);

        // Check if user is expired (check the ExpireDate property)
        if (UserExpired(user))
          return BadRequest($"Account of user.Name expired!");

        if (UserEnabled(user))
          return await GenerateToken(user);
        else
          return BadRequest("User name or password invalid.");
      
    
    else if (grantType == "refresh_token")
    
      string userName = jwt.UserName;

      // Refresh token (no password check required)
      var user = await _userRepository.GetUserInfoByName(userName);

      // Check if user is expired (check the ExpireDate property)
      if (UserExpired(user))
        return BadRequest($"Account of user.Name expired!");

      string token = jwt.Token;
      if (token == user.Token)
      
        // Generate token and send it via a json-formatted string
        return await GenerateToken(user);
      
      else
      
        return BadRequest("User token invalid.");
      
    
    else
      return BadRequest("Authentication type invalid.");
  
  else
    return BadRequest("Request invalid.");
  
  #endregion Public Methods

  #region Private Methods
  private bool UserExpired(Users utente)
  
    if (utente != null)
      return utente.ExpireDate.CompareTo(DateTime.Now) < 0;
    return true;
  

  private bool UserEnabled(Users utente)
  
    if (utente != null)
      return utente.Enabled == true;
    return false;
  

  private JsonSerializerSettings DefaultJsonSettings
  
    get
    
      return new JsonSerializerSettings()
      
        Formatting = Formatting.Indented
      ;
    
  

  private async Task<IActionResult> GenerateToken(Users user)
  
    try
    
      if (user != null)
      
        var handler = new JwtSecurityTokenHandler();
        DateTime newTokenExpiration = DateTime.Now.Add(TokenExpiration);

        ClaimsIdentity identity = new ClaimsIdentity(
          new GenericIdentity(user.Name, "TokenAuth"),
          new[]  new Claim("ID", user.Id.ToString())
        );

        var securityToken = handler.CreateToken(new SecurityTokenDescriptor
        
          Issuer = JwtController.Issuer,
          Audience = JwtController.Audience,
          SigningCredentials = SigningCredentials,
          Subject = identity,
          Expires = newTokenExpiration
        );
        string encodedToken = handler.WriteToken(securityToken);

        // Update token data on database
        await _userRepository.UpdateTokenData(user.Name, encodedToken, 
          newTokenExpiration);
        // Build the json response 
        // (I use Automapper to maps an object into another object)
        var jwtResponse = Mapper.Map<JwtResponseViewModel>(user);
        jwtResponse.AccessToken = encodedToken;
        jwtResponse.Expiration = (int)TokenExpiration.TotalSeconds;
        return Ok(jwtResponse);
      
      return NotFound();
      
      catch(Exception e)
      
        return BadRequest(e.Message);
      
    
    #endregion
  

在我的项目中,我使用 Angular。 Angular 调用 JwtController 方法:

login(userName: string, password: string)

  return this.getLoginEndpoint(userName, password)
    .map((response: Response) => this.processLoginResponse(response));


getLoginEndpoint(userName: string, password: string): Observable<Response> 

  // Body
  // JwtRequest is a model class that I use to send info to the controller
  let jwt = new JwtRequest(); 
  jwt.GrantType = "password";
  jwt.UserName = userName;
  jwt.Password = password;
  jwt.ClientId = "my_Issuer";
  // Post requiest (I use getAuthHeader that attach to the header the
  // authentication token, but it can also be omitted because it is ignored
  // by the JwtController
  return this.http.post(this.loginUrl, JSON.stringify(jwt), 
    this.getAuthHeader(true))


protected getAuthHeader(includeJsonContentType?: boolean): RequestOptions

  // Hera I use this.authService.accessToken  that is a my service where
  // I have store the token received from the server
  let headers = new Headers(
    'Authorization': 'Bearer ' + this.authService.accessToken );

  if (includeJsonContentType)
    headers.append("Content-Type", "application/json");

  headers.append("Accept", `application/vnd.iman.v01+json, 
    application/json, text/plain, */*`);
  headers.append("App-Version", "01");

  return new RequestOptions( headers: headers );


private processLoginResponse(response: Response)

  // process the response..

在您希望只有经过身份验证的用户才能访问的控制器类(或方法)上(而不是在您的 JwtController 上,因为它的方法必须可供所有用户访问)您可以设置:

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

要从 Angular 调用需要身份验证的控制器方法,您需要使用 getAuthHeader() 方法将令牌附加到标头中。

希望这篇文章对你有帮助。

【讨论】:

【参考方案2】:

是的,它使用存储在 jwt 令牌中的声明 在创建令牌时查看存储在令牌中的声明的 httpcontext 对象

这个链接也有帮助https://joonasw.net/view/adding-custom-claims-aspnet-core-2

【讨论】:

请在此处写下解决方案,而不是包含将来可能会损坏的链接。谢谢!

以上是关于ASP.NET Core JWT 和声明的主要内容,如果未能解决你的问题,请参考以下文章

Jwt 和 ASP.NET CORE 授权 AspNetRoleClaims

ASP.NET Core 2.1 Jwt 设置自定义声明

如何使用 ASP.NET Core 中的声明检查 JWT 中的权限?

ASP.NET Core Jwt 实现 signinmanager 声明

如何使用剃刀视图 ASP .Net Core 从 JWT 声明角色

ASP.NET Core JWT 身份验证更改声明(子)