ASP.NET Core 8 建立 JWT 身份驗證 完整示範

本篇會示範:
  • 一個登入 API ,會產生 JWT Token 並回傳
  • 有標記 AllowAnonymousAttribute 的 API 可以不需要 Token 即可使用
  • 其他 API 都需要加上 JWT Token ,不然會回傳 401 Unauthorized
  • 有傳入 JWT Token 的請求都可以藉由 JWT Token 自動取得使用者資訊

產生 JWT Token

安裝套件

先使用 NuGet 安裝 Microsoft.AspNetCore.Authentication.JwtBearer 套件,或是使用 .NET CLI 執行以下指令安裝
	
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
    

建立 JWT 設定檔物件:
    
using System.ComponentModel.DataAnnotations;


public class JwtOptions
{
    public const string SectionName = "Jwt";

    [Required] public string SignKey { get; set; } = null!;
    [Required] public string Issuer { get; set; } = null!;
    public int ExpireMinutes { get; set; } = 60 * 24; // 過期時間(分鐘),這裡範例的是一天(24 小時)
}
    

在 appsettings.json 設定檔中加入以下設定值:
    
{
  "AllowedHosts": "*",
  "Jwt": {
    "SignKey": "12345678901234567890123456789012",
    "Issuer": "http://ruyut.com/",
    "ExpireMinutes": 60
  }
}
    

註:記得替換為自己的金鑰,和網域,避免安全問題

建立 JwtService ,用來產生 JWT Token:
    
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;


public class JwtService(IOptions<JwtOptions> options)
{
    private readonly JwtOptions _jwtOptions = options.Value;

    public string GenerateToken(string username)
    {
        var claims = new List<Claim>();
        claims.Add(new Claim(JwtRegisteredClaimNames.Sub, username));

        var userClaimsIdentity = new ClaimsIdentity(claims);

        var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SignKey));

        var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);

        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Issuer = _jwtOptions.Issuer,
            IssuedAt = DateTime.Now,
            Subject = userClaimsIdentity,
            Expires = DateTime.Now.AddMinutes(_jwtOptions.ExpireMinutes),
            SigningCredentials = signingCredentials
        };

        var tokenHandler = new JwtSecurityTokenHandler
        {
            SetDefaultTimesOnTokenCreation = false
        };
        var securityToken = tokenHandler.CreateToken(tokenDescriptor);
        var serializeToken = tokenHandler.WriteToken(securityToken);
        return serializeToken;
    }
}
    

上面的範例幾乎是最簡單的產生 JWT Token 的程式碼了,產生出來的內容只有以下四個:
  • sub: 使用者
  • exp: 過期時間
  • iat: 產生時間
  • iss: 發行者
範例內容如下:
    
{
  "sub" : "test",
  "exp" : 1704125202,
  "iat" : 1704121602,
  "iss" : "http://ruyut.com/"
}
    

內容好像很少,不過目前夠用了(可能需要的就是加入 jti ,用以識別 JWT ,透過 jti 來拒絕 JWT 存取),假設需要角色和權限的驗證,筆者也比較傾向於動態確認,不要把角色和權限寫入 Token 中,讓取得 token 的人都可以看光光,具體的做法可以看這篇: ASP.NET Core 6 身分驗證 JWT 登入時自動賦予角色和權限

記得在 Program.cs 中註冊服務
    
var builder = WebApplication.CreateBuilder(args);

// 讀取設定值
builder.Services.AddOptions<JwtOptions>()
    .BindConfiguration(JwtOptions.SectionName)
    .ValidateDataAnnotations()
    .ValidateOnStart();

builder.Services.AddSingleton<JwtService>();

var app = builder.Build();
    

建立最簡單認證用的 Controller ,這個範例沒有實現帳號密碼的檢查,需要各位使用當前裝案的方式替換,然後會將使用者帳號送去給剛剛建立的 JwtService 產生 Jwt Token
    
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;


[ApiController]
[Route("[controller]")]
public class AuthController : ControllerBase
{
    private readonly JwtService _jwtService;

    public AuthController(JwtService jwtService)
    {
        _jwtService = jwtService;
    }

    public record class LoginDto(string UserName, string Password);

    public record class TokenDto(string Token);

    [AllowAnonymous] // 允許匿名登入
    [HttpPost("login")]
    public ActionResult<string> Login([FromBody] LoginDto dto)
    {
        // todo: 驗證帳號密碼
        
        var token = _jwtService.GenerateToken(dto.UserName); // 依照帳號產生 jwt

        return Ok(new TokenDto(token));
    }
}
    

登入 API 的呼叫方式如下:
    
curl -X 'POST' \
  'http://localhost:5221/Auth/login' \
  -H 'accept: text/plain' \
  -H 'Content-Type: application/json' \
  -d '{
  "userName": "ruyut",
  "password": "ruyut"
}'
    

範例回應:
    
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJydXl1dCIsImV4cCI6MTcwNDEyNTYwNywiaWF0IjoxNzA0MTIyMDA3LCJpc3MiOiJodHRwOi8vcnV5dXQuY29tLyJ9.uiHHFUF12ALpJqZQWZOIWmnvf11srEi6DnkF7fDQ9u4"
}
    

增加請求需要 Jwt Token 的限制

為了避免 Program.cs 過度臃腫,會將 JWT 相關的拆分,不直接寫在 Program.cs 中,這裡有許多可以探討,不過本篇先建立一個最簡單的 JwtStartup.cs 而不進行過多的拆分:
    
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.IdentityModel.Tokens;


public static class JwtStartup
{
    public static IServiceCollection AddJwtAuthentication(this IServiceCollection services, IConfiguration config)
    {
        var jwtOptions = new JwtOptions();
        config.GetSection(JwtOptions.SectionName).Bind(jwtOptions); // 將設定檔中的設定值綁定到 JwtOptions 物件上

        services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = ClaimTypes.NameIdentifier,
                    ClockSkew = TimeSpan.Zero, // 有效時間接受的誤差
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SignKey)), // 發行者金鑰
                    ValidateAudience = false, // 驗證受眾
                    ValidateIssuer = true, // 驗證發行者
                    ValidateIssuerSigningKey = true, // 驗證發行者金鑰
                    ValidateLifetime = true, // 驗證有效時間
                    ValidIssuer = jwtOptions.Issuer, // 發行者
                };
            });

        // .AddJwtBearer();
        services.AddAuthorization();

        // 未通過身份驗證的請求自動回傳 401 Unauthorized (除非有使用 AllowAnonymousAttribute)
        services.AddControllers(options => options.Filters.Add(new AuthorizeFilter()));

        return services;
    }
}
    

註冊服務:
    
var builder = WebApplication.CreateBuilder(args);

// 讀取設定值
builder.Services.AddOptions<JwtOptions>()
    .BindConfiguration(JwtOptions.SectionName)
    .ValidateDataAnnotations()
    .ValidateOnStart();

builder.Services.AddSingleton<JwtService>();
builder.Services.AddJwtAuthentication(builder.Configuration);

var app = builder.Build();

app.UseAuthentication(); // 身份驗證
app.UseAuthorization(); // 驗證授權 (原本應該就有這行,注意不要重複加入)
    

建立從 JWT 中取得身份識別的範例 API:
    
using Microsoft.AspNetCore.Mvc;


[ApiController]
[Route("[controller]")]
public class UserController : ControllerBase
{
    
    [HttpGet("profile")]
    public ActionResult<string> Profile()
    {
        // 從 jwt 取得使用者名稱
        var userName = User.Identity?.Name;
        return Ok(userName);
    }
}
    

登入 API 的呼叫方式如下:
    
curl --location 'http://localhost:5221/User/profile' \
--header 'accept: text/plain' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJydXl1dCIsImV4cCI6MTcwNDEyNzcxNywiaWF0IjoxNzA0MTI0MTE3LCJpc3MiOiJodHRwOi8vcnV5dXQuY29tLyJ9.--0JDyIOgrk4u5lSsqUm-cqHPeR-z1bKJj_GaRwSx_c'
    

回應範例:
    
ruyut
    

這樣就完成了登入取得 JWT 、呼叫 API 需要 JWT ,並且可以藉由 JWT 確認呼叫者的身份!

留言

張貼留言

如果有任何問題、建議、想說的話或文章題目推薦,都歡迎留言或來信: a@ruyut.com