查看原文
其他

JWT + ASP.NET Core 集成方案

DotNet 2022-07-19

The following article is from dotNET搬砖队 Author 天上有木月

JWT


JSON Web Token 经过数字签名后,无法伪造,一个能够在各方之间安全的传输JSON对象的开放标准(RFC 7519)参考前Introduction to JSON Web Tokens 。


创建项目和解决方案

dotnet new webapi -n SampleApi  
cd SampleApi  
dotnet new sln -n SampleApp  
dotnet sln  add .\SampleApi.csproj  

引用包

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer  

该包已经依赖Microsoft.IdentityModel.TokensSystem.IdentityModel.Tokens.Jwt,该包由Azure AD 团队提供,所以不在aspnetcore6 运行时中。

  • 或直接修改jwtaspnetcore.csproj,引用包
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.1" />  
  • appsettings.json
  "Authentication": {  
    "JwtBearer": {  
      "Issuer""http://api.sampleapi.com",  
      "Audience""SampleApi",  
      "SecurityKey""SecurityKey23456"  
    }  
  }  
  • Issuer:令牌的颁发者。一般就写成域名,实际可任意
  • Audience 颁发给谁。一般写成项目名,实际可任意
  • SecurityKey:签名验证的KEY;至少 128bit ,即16个英文字符以上,实际可任意英文字符

定义一个JwtSettings

public class JwtSettings  
{  
    public JwtSettings(byte[] key, string issuer, string audience)  
    {  
        Key = key;  
        Issuer = issuer;  
        Audience = audience;  
    }  
  
    /// <summary>  
    ///令牌的颁发者  
    /// </summary>  
    public string Issuer { get; }  
  
    /// <summary>  
    /// 颁发给谁  
    /// </summary>  
    public string Audience { get; }  
  
    public byte[] Key { get; }  
  
    public TokenValidationParameters TokenValidationParameters => new TokenValidationParameters  
    {  
        //验证Issuer和Audience  
        ValidateIssuer = true,  
        ValidateAudience = true,  
        ValidateIssuerSigningKey = true,  
        //是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比  
        ValidateLifetime = true,  
        ValidIssuer = Issuer,  
        ValidAudience = Audience,  
        IssuerSigningKey = new SymmetricSecurityKey(Key)  
    };  
  
    public static JwtSettings FromConfiguration(IConfiguration configuration)  
    {  
        var issuser = configuration["Authentication:JwtBearer:Issuer"] ?? "default_issuer";  
        var auidence = configuration["Authentication:JwtBearer:Audience"] ?? "default_auidence";  
        var securityKey = configuration["Authentication:JwtBearer:SecurityKey"] ?? "default_securitykey";  
  
        byte[] key = Encoding.ASCII.GetBytes(securityKey);  
  
        return new JwtSettings(key, issuser, auidence);  
    }  
}  

中间件Middleware引用

app.UseAuthentication();//认证  
app.UseAuthorization();//授权  

定义JWT扩展方法服务注入

public static IServiceCollection AddJwt(this IServiceCollection services, IConfiguration configuration)  
{  
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();  
    services.AddScoped<IStorageUserService, StorageUserService>();  

    var jwtSettings = JwtSettings.FromConfiguration(configuration);  
    services.AddSingleton(jwtSettings);  

    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)  
            .AddJwtBearer(options => options.TokenValidationParameters = jwtSettings.TokenValidationParameters);  

    return services;  
}  

引用服务

services.AddJwt(Configuration);  

定义一个数据库的实体类,数据库访问 为模拟数据

public class SysUser  
{  
    public int Id { getset; }  
    public string UserName { getset; }  
}  
public interface IStorageUserService  
{  
    /// <summary>  
    /// 根据登录验证用户  
    /// </summary>  
    /// <param name="loginInfo"></param>  
    /// <returns></returns>  
    Task<SysUser> CheckPasswordAsync(LoginInfo loginInfo);  
}  
public class StorageUserService : IStorageUserService  
{  
    public async Task<SysUser> CheckPasswordAsync(LoginInfo loginInfo)  
    {  
        return await Task.FromResult(  
          new SysUser   
          {   
            Id = new Random().Next(10000),   
            UserName = loginInfo.UserName   
          }  
        );  
    }  
}  

AuthController登录GenerateToken

using Microsoft.AspNetCore.Authorization;  
using Microsoft.AspNetCore.Mvc;  
using&nbsnbsp;Microsoft.IdentityModel.Tokens;  
using SampleApi.Models;  
using System.IdentityModel.Tokens.Jwt;  
using System.Security.Claims;  
  
namespace SampleApi.Auth;  
  
/// <summary>  
/// 登录认证个人信息  
/// </summary>  
[ApiController]  
[Route("/api/[controller]/[action]")]  
[AllowAnonymous]  
public class AuthController : ControllerBase  
{  
    private readonly IStorageUserService _userService;  
    private readonly JwtSettings _jwtSettings;  
  
    public AuthController(JwtSettings jwtSettings, IStorageUserService userService)  
    {  
        _jwtSettings = jwtSettings;  
        _userService = userService;  
    }  
  
    /// <summary>  
    /// 登录,生成访问Toekn  
    /// </summary>  
    /// <param name="loginInfo"></param>  
    /// <returns></returns>  
    [HttpPost]  
    public async Task<IActionResult> GenerateToken(LoginInfo loginInfo)  
    {  
        SysUser user = await _userService.CheckPasswordAsync(loginInfo);  
        if (user == null)  
        {  
            return Ok(new  
            {  
                Status = false,  
                Message = "账号或密码错误"  
            });  
        }  
  
        var claims = new List<Claim>();  
  
        claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));  
        claims.Add(new Claim(ClaimTypes.Name, user.UserName));  
  
        var key = new SymmetricSecurityKey(_jwtSettings.Key);  
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);  
        var token = new JwtSecurityToken(  
            issuer: _jwtSettings.Issuer,  
            audience: _jwtSettings.Audience,  
            claims: claims,  
            expires: DateTime.Now.AddMinutes(120),  
            signingCredentials: creds  
            );  
        return Ok(new  
        {  
            Status = true,  
            Token = new JwtSecurityTokenHandler().WriteToken(token)  
        });  
    }  
}  

aspnetcore6默认集成了swagger,直接运行项目,实际上为模拟数据库请求,所以点击登录接口即可。

{  
  "status"true,  
  "token""eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6Ijc4NjciLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoic3RyaW5nIiwiZXhwIjoxNjQzMDMyNzA1LCJpc3MiOiJodHRwOi8vYXBpLnNhbXBsZWFwaS5jb20iLCJhdWQiOiJTYW1wbGVBcGkifQ.Rl8XAt2u0aZRxEJw2mVUnV6S9WzQ65qUYjqXDTneCxE"  
}  

当使用Swagger测试时,增加,可配置全局请求头。增加一个扩展方法。

services.AddSwagger(Configuration);  
public static IServiceCollection AddSwagger(this IServiceCollection services, IConfiguration configuration)
{
    services.AddSwaggerGen(options =>
    {
        try
        {
            options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, $"{typeof(Startup).Assembly.GetName().Name}.xml"), true);
        }
        catch (Exception ex)
        {
            Log.Warning(ex.Message);
        }
        options.SwaggerDoc("v1"new OpenApiInfo
        {
            Title = "SampleApp - HTTP API",
            Version = "v1",
            Description = "The SampleApp Microservice HTTP API. This is a Data-Driven/CRUD microservice sample"
        });

        options.AddSecurityRequirement(new OpenApiSecurityRequirement()
            {
                {
                    new OpenApiSecurityScheme
                    {
                        Reference = new OpenApiReference()
                        {
                            Id =  "Bearer",
                            Type = ReferenceType.SecurityScheme
                        }
                    },
                    Array.Empty<string>()
                }
            });
        options.AddSecurityDefinition(JwtBearerDefaults.AuthenticationScheme, new OpenApiSecurityScheme
        {
            Description = "JWT授权(数据将在请求头中进行传输) 参数结构: \"Authorization: Bearer {token}\"",
            Name = "Authorization"//jwt默认的参数名称
            In = ParameterLocation.Header, //jwt默认存放Authorization信息的位置(请求头中)
            Type = SecuritySchemeType.ApiKey
        });

    });
    services.AddEndpointsApiExplorer();

    return services;

}

获取当前用户信息

/// <summary>  
/// 编码Token  
/// </summary>  
/// <param name="token"></param>  
/// <returns></returns>  
[HttpGet]  
[AllowAnonymous]  
public CurrentUser DecodeToken(string token)  
{  
    var jwtTokenHandler = new JwtSecurityTokenHandler();  

    if (jwtTokenHandler.CanReadToken(token))  
    {  
        JwtPayload jwtPayload = new JwtSecurityTokenHandler().ReadJwtToken(token).Payload;  
        string? userIdOrNull = jwtPayload.Claims.FirstOrDefault(r => r.Type == ClaimTypes.NameIdentifier)?.Value;  
        string? UserName = jwtPayload.Claims.FirstOrDefault(r => r.Type == ClaimTypes.Name)?.Value;  
        CurrentUser currentUser = new CurrentUser  
        {  
            UserId = userIdOrNull == null ? null : Convert.ToInt32(userIdOrNull),  
            UserName = UserName  
        };  
        return currentUser;  
    }  
    return null;  
}  

根据请求头获取用户信息

IStorageUserService增加接口,StorageUserService的实现,创建一个CurrentUser类

public class StorageUserService : IStorageUserService  
{  
    private readonly IHttpContextAccessor _contextAccessor;  
  
    public StorageUserService(IHttpContextAccessor contextAccessor)  
    {  
        _contextAccessor = contextAccessor;  
    }  
  
    public async Task<CurrentUser> GetUserByRequestContext()  
    {  
        var user = _contextAccessor.HttpContext.User;  
  
        string? userIdOrNull = user.Claims?.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;  
        string? UserName = user.Claims?.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;  
  
        CurrentUser currentUser = new CurrentUser  
        {  
            IsAuthenticated = user.Identity.IsAuthenticated,  
            UserId = userIdOrNull == null ? null : Convert.ToInt32(userIdOrNull),  
            UserName = UserName  
        };  
        return await Task.FromResult(currentUser);  
    }  
}  
  
public class CurrentUser  
{  
    /// <summary>  
    /// 是否登录  
    /// </summary>  
    public bool IsAuthenticated { getset; }  
    /// <summary>  
    /// 用户Id  
    /// </summary>  
    public int? UserId { getset; }  
    /// <summary>  
    /// 用户名  
    /// </summary>  
    public string? UserName { getset; }  
}  
public interface IStorageUserService  
{  
    /// <summary>  
    /// 根据Request Header携带Authorization:Bearer+空格+AccessToken获取当前登录人信息  
    /// </summary>  
    /// <returns></returns>  
    Task<CurrentUser> GetUserByRequestContext();  
}  

AuthController调用服务

/// <summary>
/// 根据Request Header携带Authorization:Bearer+空格+AccessToken获取当前登录人信息
/// </summary>
/// <returns></returns>
[HttpGet]
[Authorize]
public async Task<CurrentUser> GetUserByRequestContext()
{
    return await _userService.GetUserByRequestContext();
}

在swagger右上角,点击Authorize,header的参数结构: "Authorization: Bearer+空格+ {token}"

开源地址


SampleApp/SampleApi:https://github.com/luoyunchong/SampleApp/tree/master/SampleApi


.NET +JWT

JSON Web Token Libraries - jwt.io 可以看到,.NET有6个类库实现了JWT。

1、微软 Azure团队的实现:AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet: IdentityModel extensions for .Net (https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet)

2、jwt-dotnet/jwt: Jwt.Net, a JWT (JSON Web Token) implementation for .NET (https://github.com/jwt-dotnet/jwt)

- EOF -

推荐阅读  点击标题可跳转

C# 免费开源Blazor在线Ico转换工具

理解 .NET 6 Minimal APIs

.NET Core 验证码 - LazyCaptcha(开源)


看完本文有收获?请转发分享给更多人

推荐关注「DotNet」,提升.Net技能 

点赞和在看就是最大的支持❤️

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存