.NET 10 创建轻量级WebApi程序-添加JWT令牌身份验证

2025-04-16  乐帮网

webapi

本文章基于.NET 10.0.100-preview.3 编写的WebApi程序,基本诉求是前后端分离的后端WebAPI,由于关键点较多,所以会分为几个部分进行阐述,如有错误欢迎指正。
首先 有开发工具Microsoft Visual Studio 2022,再下载.NET 10 SDK 地址: https://dotnet.microsoft.com/zh-cn/download/dotnet/10.0 ,最新版本为 .NET 10.0.100-preview.3 发布说明参见:https://github.com/dotnet/core/discussions/9846。 默认以上条件都已准备,新建一个以 ASP.NET Core Web API为模板的项目,新建过程记得勾选 启用OpenApi,如果不勾选,在Program中手动添加代码也可以。

一、添加引用Nuget包
本节需要引用两个包:Microsoft.AspNetCore.Authentication.JwtBearer和Microsoft.AspNetCore.OpenApi。如果在创建项目时启用了OpenApi,则默认第二个包已经添加进来。

二、修改Program类
这里涉及的内容较多,主要是参考官方方法修改:
https://learn.microsoft.com/zh-cn/aspnet/core/security/authentication/configure-jwt-bearer-authentication?view=aspnetcore-10.0
这里我们是单服务器验证,签发和验证都是同一台服务器。以下行是实现签发类的注入后面会讲到实现:

builder.Services.AddScoped<IJwtProvider, JwtProvider>();

主要修改添加JWT配置和启用了全局验证,在验证时如何使用高级细分授权我感觉比之前容易控制,这里只做一个简单的Demo,并未做自定义授权和控制。
在API的控制器和方法上默认启用验证,不需要每个方法都添加标签  [Authorize]

app.MapControllers().RequireAuthorization();

完整个类如下:

var builder = WebApplication.CreateBuilder(args);

var defaultPolicy = new AuthorizationPolicyBuilder()
           .RequireAuthenticatedUser()
           .Build();

// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();

builder.Services.AddAuthentication()
.AddJwtBearer(options =>
{
    //options.Authority = "https://{--your-authority--}";
    //options.Audience = "api://my-web-api";
    //options.SaveToken = true;
    options.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidateIssuer = true, //是否验证Issuer
        ValidIssuer = builder.Configuration["Jwt:Issuer"], //签发人Issuer
        ValidateAudience = true, //是否验证Audience
        ValidAudience = builder.Configuration["Jwt:Audience"], //订阅人Audience
        ValidateIssuerSigningKey = true, //是否验证SecurityKey
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"] ?? string.Empty)), //SecurityKey
        ValidateLifetime = true, //是否验证失效时间
        ClockSkew = TimeSpan.FromSeconds(30), //过期时间容错值,解决服务器端时间不同步问题(秒)
        RequireExpirationTime = true,
    };
    options.RequireHttpsMetadata = false; 
});

builder.Services.AddScoped<IJwtProvider, JwtProvider>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseAuthentication();

app.UseAuthorization();

app.MapControllers().RequireAuthorization();

app.Run();

三、核心类JwtProvider实现签发
我们定义一个目标IJwtProvider里面两个方法,分别来实现JWT Token的生成和刷新, 另一个方法是Token的作废,我们在后面会讲到实现过程。

 public interface IJwtProvider
 {
     TokenModel GenerateToken(UserModel user);

     UserModel? ValidateToken(string token);
 }

在编写前需要准备几项数据appsettings.json 中JWT配置如下:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Jwt": {
    "Issuer": "demo",
    "Audience": "demo-api",
    "Key": "7a*4g@k9!xq3^v1s&00987l0p6n%rKhtqup`_n3",
    "ExpireSeconds": 10800
  }
}

用于定义Token的载体:

public class TokenModel
 {
     public TokenModel() : this(string.Empty, 0)
     {

     }
     public TokenModel(string token, int expireIn)
     {
         AccessToken = token;
         ExpireIn = expireIn;
     }
     public string AccessToken {  get; set; }
     /// <summary>
     /// 秒
     /// </summary>
     public int ExpireIn {  get; set; }

 }

 用于Token身份信息:

public class UserModel
 {
     private string[] _roles;

     private string _roleString;

     public required string UserId {  get; set; }

     public required string UserName { get; set; }

     public required string RolesString
     {
         get => _roleString;
         set
         {
             _roleString = value;
             _roles = _roleString?.Split(';') ?? new string[] { };
         }
     }

     public string[] Roles => _roles;
 }

用于请求的类:

public class AuthUserRequest
 {
     public string UserId { get; set; } = string.Empty;

     public string PassWord { get; set; } = string.Empty;
 }

 用于返回的共用类:

 public class ApiResponse
  {
      public ApiResponse(object data) :this(0, "success", data)
      {
      }
      public ApiResponse(int code,string message,object data)
      {
          Code = code;
          Message = message;
          Data = data;
      }

      public int Code { get; set; }

      public string Message { get; set; }

      public object Data { get; set; }

      public static ApiResponse New(object data)
      {
          return new ApiResponse(data);
      }
      public static ApiResponse New(int code,string message)
      {
          return new ApiResponse(code, message, new { });
      }
  }

为了方便转实体我们定义扩展类:

 public static class ClaimsPrincipalExtensions
  {
      public static UserModel? GetUser(this ClaimsPrincipal? principal)
      {
          if (principal == null)
              return null;

          if (principal != null && principal.Claims.Any())
          {
              return new UserModel()
              {
                  UserId = principal.Claims.FirstOrDefault(c => c.Type.Equals("UserId", StringComparison.Ordinal))?.Value ?? string.Empty,
                  UserName = principal.Claims.FirstOrDefault(c => c.Type.Equals(ClaimTypes.Name, StringComparison.Ordinal))?.Value ?? string.Empty,
                  RolesString = principal.Claims.FirstOrDefault(c => c.Type.Equals(ClaimTypes.Role, StringComparison.Ordinal))?.Value ?? string.Empty,
              };
          }
          ;

          return null;
      }
  }

最后是核心实现类JwtProvider:

 public class JwtProvider : IJwtProvider
 {
     private readonly IConfiguration _configuration;

     public JwtProvider(IConfiguration configuration)
     {
         _configuration = configuration;
     }

     public TokenModel GenerateToken(UserModel user)
     {
         string key = _configuration["Jwt:Key"] ?? string.Empty;
         int expireSeconds = 10800;
         int.TryParse(_configuration["Jwt:ExpireSeconds"], out expireSeconds);
         
         if (string.IsNullOrEmpty(key))
             return new TokenModel();
         var claims = new[]
        {
             new Claim(ClaimTypes.Name, user.UserName), //HttpContext.User.Identity.Name
             new Claim(ClaimTypes.Role, string.Join(";", user.Roles)), //HttpContext.User.IsInRole("r_admin")
             new Claim("UserId", user.UserId),
         };
       
         var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
         var creds = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);

         var jwtSecurityToken = new JwtSecurityToken(
             _configuration["Jwt:Issuer"],       //Issuer
             _configuration["Jwt:Audience"],     //Audience
             claims,                             //Claims,
             DateTime.Now,                       //notBefore
             DateTime.Now.AddSeconds(expireSeconds),        //expires
             creds                  //Credentials
         );

         var token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);

         return new TokenModel(token, expireSeconds);
     }

     public UserModel? ValidateToken(string token)
     {
         string key = _configuration["Jwt:Key"] ?? string.Empty;

         if (string.IsNullOrEmpty(token)|| string.IsNullOrEmpty(key))
             return default;
         var tokenHandler = new JwtSecurityTokenHandler();
         var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
         var validateParameter = new TokenValidationParameters()
         {
             ValidateLifetime = true,
             ValidateAudience = false,
             ValidateIssuer = false,
             ValidateIssuerSigningKey = true,
             ValidIssuer = _configuration["Jwt:Issuer"],
             ValidAudience = _configuration["Jwt:Audience"],
             IssuerSigningKey = secretKey
         };

         var principal = tokenHandler.ValidateToken(token, validateParameter, out SecurityToken validatedToken);

         return principal.GetUser();
     }
 }

 四、编写调用的AuthController

[ApiController]
  [Route("api/[controller]/[action]")]
  public class AuthController : ControllerBase
  {
      private readonly ILogger<AuthController> _logger;

      private readonly IJwtProvider _jwtProvider;

      public AuthController(ILogger<AuthController> logger, IJwtProvider jwtProvider)
      {
          _jwtProvider = jwtProvider;
          _logger = logger;
      }

      [HttpPost]
      [AllowAnonymous]
      public ApiResponse Token(AuthUserRequest request)
      {
          if (string.IsNullOrEmpty(request.UserId) || string.IsNullOrEmpty(request.PassWord))
              return ApiResponse.New(-1, "Please enter your username and password.");
          UserModel user = new UserModel() { RolesString="admim",UserId="000296",UserName="lebang2020.cn" };
          var token = _jwtProvider.GenerateToken(user);
          return ApiResponse.New(token);
      }
 
      [HttpPost]
      public ApiResponse Refresh()
      {
          //I can get user from token string in the header value.
          var user = HttpContext.User.GetUser();
          if(user==null)
              return ApiResponse.New(-1, "Unknown error.");
          //then get new token
          var token = _jwtProvider.GenerateToken(user);
          return ApiResponse.New(token);

      }

      //token 做废下次单独讲


  }

五、测试调用API
以上API的调用地址 Post:http://localhost:5080/api/auth/token 拿到token,然后携带token再请求Refresh方法,试一下这个方法,其中调用Refresh方法必须携带token,否则返回401 无权限。

如下图:
调用http://localhost:5080/api/auth/token

code1

调用http://localhost:5080/api/auth/Refresh

code2

源码下载链接: https://pan.baidu.com/s/1ykhEtLeLzZ0nO5b02aCqIg

公众号二维码

关注我的微信公众号
在公众号里留言交流
投稿邮箱:1052839972@qq.com

庭院深深深几许?杨柳堆烟,帘幕无重数。
玉勒雕鞍游冶处,楼高不见章台路。
雨横风狂三月暮。门掩黄昏,无计留春住。
泪眼问花花不语,乱红飞过秋千去。

欧阳修

付款二维码

如果感觉对您有帮助
欢迎向作者提供捐赠
这将是创作的最大动力