iT邦幫忙

2024 iThome 鐵人賽

DAY 23
0

接下來可以要開始撰寫 API 了,可以依照先前設計的 API 規格來實作,如果有特別需求再做調整。

建立註冊及登入API

售票系統一定會有使用者登入的機制,無論是系統自行管理或是使用 SSO 進行註冊及登入,實作的窮小子售票系統的重點並不在這些登入及身分驗證的機制,只是要練習高併發的應用,根據之前設計的 API 規格來做開發。

安裝套件

用戶主要交互的服務會是 SalesService 我們在 SalesService 來建立註冊跟登入的 API

API 認證的部分預計使用 JWT 來做驗證,密碼的部分我們使用 Bcrypt 進行加密,首先安裝需要用到的套件

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package BCrypt.Net-Next

產生 Token

新增一個 Token Service 用來產生 Token

/Service/TokenService.cs

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;

namespace iThome2024.SalesService.Service;

public class TokenService(IConfiguration configuration)
{
    private IConfiguration _configuration = configuration;

    public string GenerateJwtToken(string username)
    {
        var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]!));
        var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

        var claims = new[]
        {
            new Claim(ClaimTypes.NameIdentifier, username),
        };

        var token = new JwtSecurityToken(
            issuer: _configuration["Jwt:Issuer"],
            claims: claims,
            expires: DateTime.Now.AddMinutes(15),
            signingCredentials: credentials);

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

在 appsettings.json 新增 JWT 需要的參數

  "Jwt": {
    "Issuer": "iThome2024",
    "Key": ""
  }

Program.cs 註冊服務

builder.Services.AddSingleton<TokenService>();

驗證 Token

驗證的部分透過 .NET 框架提供的 Service 進行配置

Program.cs

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = false,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
        };
    });
builder.Services.AddAuthorization();
\\...
var app = builder.Build();
\\...
app.UseAuthentication();
app.UseAuthorization();

註冊 API

為了接收 Body 傳入的帳號密碼,先建立一個 ViewModel

ViewModel/UserSignInViewModel.cs

namespace iThome2024.SalesService.ViewModel;

public class UserSignInViewModel
{
    public required string Username { get; set; }
    public required string Password { get; set; }
}

新增一個用於註冊的 Endpoint,使用 BCrypt 套件產生 Hash 後的密碼並存入 DB

using BC = BCrypt.Net.BCrypt;

app.MapPost("/api/auth/user", async (UserModel model, [FromServices] TicketSalesContext context) =>
{
    model.Username = model.Username.ToUpper();
    var user = await context.User.FirstOrDefaultAsync(u => u.Username == model.Username);
    if (user != null)
    {
        return Results.BadRequest("Username already exists");
    }
    user = new User
    {
        Username = model.Username,
        Password = BC.HashPassword(model.Password)
    };
    await context.User.AddAsync(user);
    await context.SaveChangesAsync();

    return Results.Ok("User registered successfully");
});

登入 API

新增一個用於登入的 Endpoint,接收使用者的帳號密碼,驗證過後透過 TokenService 建立JWT Token 並回傳給使用者

app.MapPost("/api/auth", async (
    UserSignInViewModel model,
    [FromServices] TicketSalesContext context,
    [FromServices] TokenService tokenService) =>
{
    model.Username = model.Username.ToUpper();
    var user = await context.User.FirstOrDefaultAsync(u => u.Username == model.Username);
    if (user == null)
    {
        return Results.Unauthorized();
    }

    if (!BC.Verify(model.Password, user.Password))
    {
        return Results.Unauthorized();
    }

    var token = tokenService.GenerateJwtToken(model.Username);
    return Results.Ok(new { token });
});

API 加入驗證

目前我們的所有 API 都不需要驗證就可以訪問了,可以拿其中一個 API 加入驗證來做測試,我們拿用來測試 PubSub 的 API 做測試,在 Endpoint 最後加上RequireAuthorization

app.MapPost("/Test/PubSubPublishMessage", async (string message, [FromServices] PublisherService publisherService) =>
{
    return await publisherService.Publish(message);
})
.WithName("TestPubSubPublishMessage")
.WithOpenApi()
.RequireAuthorization();

開啟 Postman 測試看看,可以看到現在直接打這個API 會回復我們 401 Unauthorized

https://ithelp.ithome.com.tw/upload/images/20240924/20168312rTCgxX18RQ.png

測試

要進行測試我們要先註冊一個帳號進行登入再打一次 /Test/PubSubPublishMessage

https://ithelp.ithome.com.tw/upload/images/20240924/201683120veq4fPxya.png
進到 DB 可以看到使用者已經順利被建立,密碼也是雜湊過的

https://ithelp.ithome.com.tw/upload/images/20240924/20168312SsiUVPAyK6.png
接著測試看看登入,先測試輸入錯誤的密碼,確實無法順利登入

https://ithelp.ithome.com.tw/upload/images/20240924/20168312ONF8aOYzcI.png
輸入正確的密碼後就會順利 Response Token 給我們

https://ithelp.ithome.com.tw/upload/images/20240924/2016831266KMrxp3eE.png
我們把 Token 填入 Header 後再測試看看 PubSubTest

https://ithelp.ithome.com.tw/upload/images/20240924/20168312dI8AUZs0p8.png
可以看到驗證通過了 API 也順利返回 MessageId

https://ithelp.ithome.com.tw/upload/images/20240924/20168312Hr6MqK8FNl.png
最後再推送到 Github 上跑 CI/CD 部屬到 Cloud Run ,至此基本的註冊、登入及身分驗證就完成了。

備註: 記得部屬到 Cloud Run 時 appsettings 裡有關 JWT 的設定也要存放到 Secreat Manager


上一篇
Day22: 實作-開發-Table Schema 建立 Code First
下一篇
Day24: 實作-開發-管理活動
系列文
窮小子的售票系統30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言