目的

在swagger內使用jwt token測試API

建立新專案

選擇ASP.NET Core Web API專案範本,並執行下一步 步驟1-1

設定新的專案

命名你的專案名稱,並選擇專案要存放的位置。 步驟2-1

其他資訊

選擇.net6版本,支援OpenAPI支援一定要勾選,此選項.net5以後才會有,.net core 3.1並沒有此選項,需要從NuGet安裝,並點建立 步驟3-1

專案基本設定

右邊紅框處專案檔點兩下,會開啟專案的xml檔案,額外加入兩行xml資料,目的是要透過編譯器產生文件檔案

<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
  • 加入前 步驟4-1
  • 加入後 步驟4-2

編輯Program.cs檔案

修改program檔案內容,調整AddSwaggerGen的內容,目的是為了可以讀取我們所寫的註解

builder.Services.AddSwaggerGen(options => {
  // using System.Reflection;
  var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
  options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
});
  • 加入前 步驟5-1
  • 加入後 步驟5-2

NuGet加入套件

透過NuGet安裝

  • JWT
  • Microsoft.AspNetCore.Authentication.JwtBearer
  • Microsoft.IdentityModel.Tokens
  • System.IdentityModel.Tokens.Jwt

範例6-1

新增Helpers資料夾並在裡面新增JwtHelpers.cs類別檔案

jwt範例使用保哥範例來做修改,目的只是為了取得jwt token

  public class JwtHelper {
    private readonly JwtSettingsOptions _settings;

    public JwtHelper(IOptionsMonitor<JwtSettingsOptions> settings) {
      //注入appsetting的json
      _settings = settings.CurrentValue;
    }

    public string GenerateToken(string userName, int expireMinutes = 120) {
      //發行人
      var issuer = _settings.Issuer;
      //加密的key,拿來比對jwt-token沒有
      var signKey = _settings.SignKey;
      建立JWT-Token
      var token = JwtBuilder.Create()
                      //所採用的雜湊演算法
                      .WithAlgorithm(new HMACSHA256Algorithm()) // symmetric
                      //加密key
                      .WithSecret(signKey)
                      //角色
                      .AddClaim("roles", "admin")
                      //JWT ID
                      .AddClaim("jti", Guid.NewGuid().ToString())
                      //發行人
                      .AddClaim("iss", issuer)
                      //使用對象名稱
                      .AddClaim("sub", userName) // User.Identity.Name
                      //過期時間
                      .AddClaim("exp", DateTimeOffset.UtcNow.AddMinutes(expireMinutes).ToUnixTimeSeconds())
                      //此時間以前是不可以使用
                      .AddClaim("nbf", DateTimeOffset.UtcNow.ToUnixTimeSeconds())
                      //發行時間
                      .AddClaim("iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds())
                      //使用者全名
                      .AddClaim(ClaimTypes.Name, userName)
                      //進行編碼
                      .Encode();
      return token;
    }
  }
  //將appsetting轉為強行別所使用
  public class JwtSettingsOptions {
    public string Issuer { get; set; } = "";
    public string SignKey { get; set; } = "";
  }

因篇幅過長,只擷取JwtHelpers.cs部分內容,記得要using下列命名空間

using JWT.Algorithms;
using JWT.Builder;
using Microsoft.Extensions.Options;
using System.Security.Claims;

範例7-1

新增Filters資料夾並在裡面新增AuthorizeCheckOperationFilter.cs類別檔案

因為在使用swagger做認證測試時,會遇到一個很惱人的問題,就是當我的某些api並不需要做認證,卻還是會在畫面上顯示鎖頭

  • 第一支並沒有attribute,無須認證
  • 第二支為AllowAnonymous,無須認證
  • 第三支為Authorize,需要認證

範例8-1

      public class AuthorizeCheckOperationFilter : IOperationFilter {
    private readonly EndpointDataSource _endpointDataSource;

    public AuthorizeCheckOperationFilter(EndpointDataSource endpointDataSource) {
      _endpointDataSource = endpointDataSource;
    }
    public void Apply(OpenApiOperation operation, OperationFilterContext context) {
      取得所有controller內的action
      var Descriptor = _endpointDataSource.Endpoints.FirstOrDefault(x =>
          x.Metadata.GetMetadata<ControllerActionDescriptor>() == context.ApiDescription.ActionDescriptor);
      //取得包含Authorize的Attribute
      var Authorize = Descriptor.Metadata.GetMetadata<AuthorizeAttribute>() != null;
      //取得包含AllowAnonymous的Attribute
      var AllowAnonymous = Descriptor.Metadata.GetMetadata<AllowAnonymousAttribute>() != null;
      //如果不需要鎖頭則return回去
      if (!Authorize || AllowAnonymous)
        return;
      //需要鎖頭則在swagger-UI中定義出來
      operation.Security = new List<OpenApiSecurityRequirement>
      {
                new()
                {
                    [
                        new OpenApiSecurityScheme {Reference = new OpenApiReference
                            {
                                Type = ReferenceType.SecurityScheme,
                                Id = "Bearer"}
                        }
                    ] = new List<string>()
                }
            };
    }
  }

編輯WeatherForecastController檔案

注入JwtHelper 範例9-1

  • 第一支為預設沒有attribute的方法
  • 第二支為登入方法,attribute是AllowAnonymous,任何人都可以使用
  • 第三支為登入後才可以取得的資料,attribute是Authorize(Roles = “admin”),role需要是admin才可以使用
    [HttpGet("Login"), AllowAnonymous]
    public ActionResult<string> Login(string username , string password) {
      var token = _jwtHelpers.GenerateToken(username);
      return Ok(token);
    }
    [HttpGet("username"), Authorize(Roles = "admin")]
    public ActionResult<string> Username() {
      return Ok(User.Identity?.Name);
    }

範例9-2

再次編輯Program.cs檔案

分兩個區塊說明

  1. JWT設定
//清除預設映射
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
//註冊JwtHelper
builder.Services.AddSingleton<JwtHelper>();
//使用選項模式註冊
builder.Services.Configure<JwtSettingsOptions>(
    builder.Configuration.GetSection("JwtSettings"));
//設定認證方式
builder.Services
  //使用bearer token方式認證並且token用jwt格式
  .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  .AddJwtBearer(options => {
    options.TokenValidationParameters = new TokenValidationParameters {
      // 可以讓[Authorize]判斷角色
      RoleClaimType = "roles",
      // 預設會認證發行人
      ValidateIssuer = true,
      ValidIssuer = builder.Configuration.GetValue<string>("JwtSettings:Issuer"),
      // 不認證使用者
      ValidateAudience = false,
      // 如果 Token 中包含 key 才需要驗證,一般都只有簽章而已
      ValidateIssuerSigningKey = true,
      // 簽章所使用的key
      IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration.GetValue<string>("JwtSettings:SignKey")))
    };
  }); 

範例10-1 2. Swagger-UI調整

  //說明api如何受到保護
  options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme {
    Name = "Authorization",
    //選擇類型,type選擇http時,透過swagger畫面做認證時可以省略Bearer前綴詞(如下圖)
    Type = SecuritySchemeType.Http,
    //採用Bearer token
    Scheme = "Bearer",
    //bearer格式使用jwt
    BearerFormat = "JWT",
    //認證放在http request的header上
    In = ParameterLocation.Header,
    //描述
    Description = "JWT驗證描述"
  });
  //製作額外的過濾器,過濾Authorize、AllowAnonymous,甚至是沒有打attribute
  options.OperationFilter<AuthorizeCheckOperationFilter>();
  • Type使用SecuritySchemeType.Http,不用打Bearer 範例10-2
  • Type使用SecuritySchemeType.ApiKey,需要打Bearer與空白以及文字描述會包含Name、In、Description 範例10-3 範例10-4 在下方別忘了使用認證的中介層 範例10-5

執行結果

在登入的api輸入帳號密碼 範例11-1 會回傳一組jwt token 範例11-2 點擊認證按鈕,將token輸入 範例11-3 範例11-4 使用有鎖頭的API 範例11-5 最後可以正確取得回傳值就是成功了 範例11-6 如果不輸入token直接使用有鎖頭的API,就會跳出401錯誤 範例11-7

參考

保哥 伊果的沒人看筆記本 實作Filter JWT規範

範例檔

GitHub