<xmp id="63nn9"><video id="63nn9"></video></xmp>

<xmp id="63nn9"></xmp>

<wbr id="63nn9"><ins id="63nn9"></ins></wbr>

<wbr id="63nn9"></wbr><video id="63nn9"><ins id="63nn9"><table id="63nn9"></table></ins></video>

Loading

基于.NetCore開發博客項目 StarBlog - (27) 使用JWT保護接口

前言

這是StarBlog系列在2023年的第二篇更新??

這幾個月都在忙,更新變得很不勤快,但是拖著不更新我的心里更慌,很久沒寫,要開頭就變得很難??

說回正題,之前的文章里,我們已經把博客關鍵的接口都開發完成了,但還少了一個最關鍵的「認證授權」,少了這東西,網站就跟篩子一樣,誰都可以來添加和刪除數據,亂套了~

關于「認證授權」的知識,會比較復雜,要學習這塊的話,建議分幾步:

  • 基礎概念
  • AspNetCore 的 Identity 框架
  • 其他框架,如 IdentityServer

關于基礎概念可以看看我之前寫的這篇: Asp-Net-Core學習筆記:身份認證入門

PS:Identity 框架的還沒寫好??

為了避免當復讀機,本文就不涉及太多概念的東西了,建議先看完上面那篇再來開始使用JWT~

JWT

前面介紹文章的CRUD接口時,涉及到修改的接口,都加了 [Authorize] 特性,表示需要登錄才能訪問,本文就以最簡單的方式來實現這個登錄認證功能。

在 AspNetCore 中,使用 JWT 的工作流程大概如下:

  • JWT就是一個Base64編碼的字符串,分為 head/payload/sign 三個部分(sign簽名是使用特定秘鑰生成的,別人無法偽造,所以就算修改了payload部分的信息,后端校驗也不會通過)
  • 用戶登錄時,后端可以在里面存一些類似用戶ID、郵箱、手機號之類的數據,然后把這串東西返回給前端存儲,注意不要把不能被客戶端知道的信息放在里面(也可以對payload進行加密)
  • 之后調用需要登錄的接口時,都要帶上這個JWT(一般是放在 HTTP Header 里面)
  • 這串東西只有后端能解析,后端拿到之后就知道用戶的身份了

JWT 還有其他一些特性,比如說是沒有狀態的,這就很符合我們用的 RESTFul 接口了,不像傳統使用 session 和 cookies 那樣,原版 JWT 只要簽發之后,在有效期結束前就不能取消,用戶也沒法注銷,為了避免泄露 JWT token 導致安全問題,一般過期時間都設置得比較短。(這個不能取消的問題,也是可以通過曲線救國解決的,不過不在本文的討論范圍哈)

初步接觸 JWT

OK,說了那么多,還是開始來寫代碼吧

生成 JWT

要生成的話很簡單,不需要什么額外的配置,幾行代碼就搞定了

public LoginToken GenerateLoginToken(User user) {
  var claims = new List<Claim> {
    new(JwtRegisteredClaimNames.Sub, user.Id), // User.Identity.Name
    new(JwtRegisteredClaimNames.GivenName, user.Name),
    new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), // JWT ID
  };
  var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("jwt key"));
  var signCredential = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
  var jwtToken = new JwtSecurityToken(
    issuer: "jwt issuer 簽發者",
    audience: "jwt audience 接受者",
    claims: claims,
    expires: DateTime.Now.AddDays(7),
    signingCredentials: signCredential);

  return new LoginToken {
    Token = new JwtSecurityTokenHandler().WriteToken(jwtToken),
    Expiration = TimeZoneInfo.ConvertTimeFromUtc(jwtToken.ValidTo, TimeZoneInfo.Local)
  };
}

最開始的 claims 就是前面說的后端往JWT里面存的數據

"The set of claims associated with a given entity can be thought of as a key. The particular claims define the shape of that key; much like a physical key is used to open a lock in a door. In this way, claims are used to gain access to resources." from MSDN

Claim 的構造方法可以接收 keyvalue 參數,都是字符串

對于 key ,.Net 提供了一些常量,在 JwtRegisteredClaimNamesClaimTypes 類里邊,這倆的區別就是后者是老的,一般在Windows體系下使用,比如說同樣是 Name 這個 key

  • ClaimTypes.Name = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
  • JwtRegisteredClaimNames.Name = "name"

我們是在 JWT 里面設置 Claim,用 JwtRegisteredClaimNames 就好了

參考:https://stackoverflow.com/questions/50012155/jwt-claim-names

從 JWT 中讀取信息

也就是讀取放在里面的各個 Claim

在正確配置 Authentication 服務和 JwtBearer 之后,已登錄的客戶端請求過來,后端可以在 Controller 里面拿到 JWT 數據

像這樣

var name = HttpContext.User.FindFirst(JwtRegisteredClaimNames.Name)?.Value;

還可以用 System.Security.Claims.PrincipalExtensions 的擴展方法 FindFirstValue 直接拿到字符串值。

吐槽:如果對應的 Claim 不存在的話,這個擴展方法返回的值是 null,但不知道為啥,他源碼用的是 string 作為返回值類型,而不是 string? ,真是令人遺憾

使用 JWT 保護接口

了解 JWT 的使用方式之后,終于可以把 JWT 應用到博客項目中了~

配置JWT參數

為了避免硬編碼,我們把 JWT 需要的 Issuer, Audience, Key 三個參數寫在配置里面

形式如下

"Auth": {
  "Jwt": {
    "Issuer": "starblog",
    "Audience": "starblog-admin-ui",
    "Key": "F2REaFzQ6xA9k77EUDLf9EnjK5H2wUot"
  }
}

接著需要定義一個類來方便映射配置。

StarBlog.Web/Models/Config 下添加 Auth.cs

public class Auth {
  public Jwt Jwt { get; set; }
}

public class Jwt {
  public string Issuer { get; set; }
  public string Audience { get; set; }
  public string Key { get; set; }
}

注冊一下

builder.Services.Configure<Auth>(configuration.GetSection(nameof(Auth)));

配置 Authentication 服務

這部分代碼比較多,寫成擴展方法,避免 Program.cs 文件代碼太多

添加 StarBlog.Web/Extensions/ConfigureAuth.cs 文件

public static class ConfigureAuth {
  public static void AddAuth(this IServiceCollection services, IConfiguration configuration) {
    services.AddScoped<AuthService>();
    services.AddAuthentication(options => {
      options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
      options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
      .AddJwtBearer(options => {
        var authSetting = configuration.GetSection(nameof(Auth)).Get<Auth>();
        options.TokenValidationParameters = new TokenValidationParameters {
          ValidateAudience = true,
          ValidateLifetime = true,
          ValidateIssuer = true,
          ValidateIssuerSigningKey = true,
          ValidIssuer = authSetting.Jwt.Issuer,
          ValidAudience = authSetting.Jwt.Audience,
          IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authSetting.Jwt.Key)),
          ClockSkew = TimeSpan.Zero
        };
      });
  }
}

然后在 Program.cs 里,需要使用這個擴展方法來注冊服務

builder.Services.AddAuth(builder.Configuration);

還得配置一下中間件,這個順序很重要,需要使用身份認證保護的接口或資源,必須放到這倆 Auth... 中間件的后面。

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
// ...
app.MapControllerRoute(...);
app.Run();

封裝登錄邏輯

還是那句話,為了方便使用balabala……

新建 StarBlog.Web/Services/AuthService.cs 文件

public class AuthService {
  private readonly Auth _auth;
  private readonly IBaseRepository<User> _userRepo;

  public AuthService(IOptions<Auth> options, IBaseRepository<User> userRepo) {
    _auth = options.Value;
    _userRepo = userRepo;
  }

  public LoginToken GenerateLoginToken(User user) {
    var claims = new List<Claim> {
      new(JwtRegisteredClaimNames.Sub, user.Id), // User.Identity.Name
      new(JwtRegisteredClaimNames.GivenName, user.Name),
      new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), // JWT ID
    };
    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_auth.Jwt.Key));
    var signCredential = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
    var jwtToken = new JwtSecurityToken(
      issuer: _auth.Jwt.Issuer,
      audience: _auth.Jwt.Audience,
      claims: claims,
      expires: DateTime.Now.AddDays(7),
      signingCredentials: signCredential);

    return new LoginToken {
      Token = new JwtSecurityTokenHandler().WriteToken(jwtToken),
      Expiration = TimeZoneInfo.ConvertTimeFromUtc(jwtToken.ValidTo, TimeZoneInfo.Local)
    };
  }
}

因為篇幅關系,只把關鍵的生成 JWT 代碼貼出來,還有一些獲取用戶信息啥的代碼,還不是最終版本,接下來隨時會修改,而且也比較簡單,就沒有放出來~

再來寫個登錄接口

添加 StarBlog.Web/Apis/AuthController.cs 文件

[ApiController]
[Route("Api/[controller]")]
[ApiExplorerSettings(GroupName = ApiGroups.Auth)]
public class AuthController : ControllerBase {
  private readonly AuthService _authService;

  public AuthController(AuthService authService) {
    _authService = authService;
  }

  /// <summary>
  /// 登錄
  /// </summary>
  [HttpPost]
  [ProducesResponseType(typeof(ApiResponse<LoginToken>), StatusCodes.Status200OK)]
  public async Task<ApiResponse> Login(LoginUser loginUser) {
    var user = await _authService.GetUserByName(loginUser.Username);
    if (user == null) return ApiResponse.Unauthorized("用戶名不存在");
    if (loginUser.Password != user.Password) return ApiResponse.Unauthorized("用戶名或密碼錯誤");
    return ApiResponse.Ok(_authService.GenerateLoginToken(user));
  }
}

之后我們請求這個接口,如果用戶名和密碼正確的話,就可以拿到 JWT token 和過期時間

{
  "statusCode": 200,
  "successful": true,
  "message": "Ok",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR123I6IkpXVCJ9.eyJ1c2VybmFtZSI6ImRlYWxpIiwibmFC1kYJ9.DaJEmBAVdXks8MOedVee4xxrB-RvUSg2wIJGc30HGkk",
    "expiration": "2023-05-04T22:29:04+08:00"
  },
  "errorData": null
}

接下來,請求添加了 [Authorize] 的接口時,需要在 HTTP header 里面加上:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR123I6IkpXVCJ9.eyJ1c2VybmFtZSI6ImRlYWxpIiwibmFC1kYJ9.DaJEmBAVdXks8MOedVee4xxrB-RvUSg2wIJGc30HGkk

配置swagger支持

加了 [Authorize] 之后,在swagger里就沒法調試接口了,得用 postman 之類的工具,添加 HTTP header

不過swagger這么好用的工具肯定不會那么蠢,它是可以配置支持 JWT 的

添加 nuget 包 Swashbuckle.AspNetCore.Filters

然后編輯 StarBlog.Web/Extensions/ConfigureSwagger.cs 來配置一下(上一篇關于swagger的還沒忘記吧?)

AddSwaggerGen 里面,添加配置代碼

var security = new OpenApiSecurityScheme {
  Description = "JWT模式授權,請輸入 \"Bearer {Token}\" 進行身份驗證",
  Name = "Authorization",
  In = ParameterLocation.Header,
  Type = SecuritySchemeType.ApiKey
};
options.AddSecurityDefinition("oauth2", security);
options.AddSecurityRequirement(new OpenApiSecurityRequirement {{security, new List<string>()}});
options.OperationFilter<AddResponseHeadersFilter>();
options.OperationFilter<AppendAuthorizeToSummaryOperationFilter>();
options.OperationFilter<SecurityRequirementsOperationFilter>();

搞定。這樣swagger頁面右上角就多了個鎖頭圖標,點擊就可以輸入 JWT token

不過有一點不方便的是,每個接口分組都要輸入一次,切換了就得重新輸入了…

但至少不用postman了~

參考資料

系列文章

posted @ 2023-05-03 22:47  程序設計實驗室  閱讀(1038)  評論(4編輯  收藏  舉報
人碰人摸人爱免费视频播放

<xmp id="63nn9"><video id="63nn9"></video></xmp>

<xmp id="63nn9"></xmp>

<wbr id="63nn9"><ins id="63nn9"></ins></wbr>

<wbr id="63nn9"></wbr><video id="63nn9"><ins id="63nn9"><table id="63nn9"></table></ins></video>