ASP.NET Core API JWT Authentication 身分驗證與授權

.NET Core建置 JWT 驗證步驟有點繁瑣,怕忘記因此紀錄一下
Startup
public void ConfigureServices(IServiceCollection services)
{
    // 時間格式轉換
    services.AddControllers().AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
        options.JsonSerializerOptions.Converters.Add(new DatetimeJsonConverter());
    });

    // 驗證 HTTP Header 合法有效的 JWT Token
    services.AddAuthentication(x =>
    {
        x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    // 設定 JWT Bearer Token 的檢查選項
    .AddJwtBearer(options =>
    {
        // 當驗證失敗時, 回應標頭會包含 WWW-Authenticate 標頭, 這裡會顯示失敗的詳細錯誤原因
        options.IncludeErrorDetails = true;

        // 是否需要Https
        options.RequireHttpsMetadata = false;

        // 保存Token
        options.SaveToken = false;

        options.TokenValidationParameters = new TokenValidationParameters
        {
            // 驗證 Issuer
            ValidateIssuer = true,
            ValidIssuer = Configuration["Jwt:Issuer"],

            // 驗證 Audience
            ValidateAudience = false,

            // 驗證 Token 有效期間
            ValidateLifetime = true,

            // 時間偏移 (有效時間會偏移)
            ClockSkew = TimeSpan.Zero,

            // 驗證 Token 中包含 key, 如果 JWT 包含 key 才需要驗證, 一般都只有簽章
            ValidateIssuerSigningKey = false,

            // 驗證 SignKey
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
        };

        // 驗證事件捕捉
        options.Events = new JwtBearerEvents()
        {
            OnTokenValidated = context =>
            {
                // 驗證成功, 後續再看要做什麼事情...
                return Task.CompletedTask;
            },
            OnAuthenticationFailed = context =>
            {
                // 驗證失敗, 後續再看要做什麼事情...
                return Task.CompletedTask;
            }
        };
    });
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // 註冊 Middleware 允許全部跨站請求
    app.UseCors(x => x
        .AllowAnyOrigin()
        .AllowAnyMethod()
        .AllowAnyHeader());

    // 驗證 with Jwt
    app.UseAuthentication();
    app.UseMvc();
}

 

時間格式轉換

/// <summary>
/// API日期格式轉換
/// </summary>
public class DatetimeJsonConverter : JsonConverter<DateTime>
{
    public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.String)
        {
            if (DateTime.TryParse(reader.GetString(), out DateTime date))
            {
                return date;
            }
        }
        return reader.GetDateTime();
    }

    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString("yyyy/MM/dd HH:mm:ss.fff"));
    }
}

 

JWT模型

/// <summary>
/// JWT接口模型
/// </summary>
public class JWTViewModel
{
    /// <summary>
    /// 商戶號代碼
    /// </summary>
    [Required(ErrorMessage = "MerchantNo is required")]
    public string MerchantNo { get; set; }

    /// <summary>
    /// 商戶號金鑰
    /// </summary>
    [Required(ErrorMessage = "MerchantKey is required")]
    public string MerchantKey { get; set; }

    /// <summary>
    /// 用戶代碼
    /// </summary>
    [Required(ErrorMessage = "UserID is required")]
    public int UserID { get; set; }

    /// <summary>
    /// 用戶名稱
    /// </summary>
    [Required(ErrorMessage = "UserName is required")]
    [StringLength(20, ErrorMessage = "UserName maximumLength is 20")]
    public string UserName { get; set; }

    /// <summary>
    /// Token
    /// </summary>
    public string Token { get; set; }
}

 

Controller

/// <summary>
/// 身分驗證
/// </summary>
[ApiController]
[Route("[controller]")]
public class AuthController : Controller
{

 /// <summary>
/// 登入會員資訊 with JWT
/// </summary>
public JWTViewModel LoginUserInfo
{
get {
JWTViewModel jwtModel = new JWTViewModel()
{
MerchantNo = User.Claims.FirstOrDefault(p => p.Type == "MerchantNo").Value,
UserID = Convert.ToInt32(User.Claims.FirstOrDefault(p => p.Type == "UserID").Value),
UserName = User.Claims.FirstOrDefault(p => p.Type == "UserName").Value
};
return jwtModel;
}
}
    /// <summary>
    /// 驗證會員, 取得 JWT Token
    /// </summary>
    /// <param name="inputData"></param>
    /// <returns></returns>
    [HttpPost("Authenticate")] 
    public IActionResult Authenticate(JWTViewModel inputData)
    {
        JWTViewModel jwtModel = jwtService.Authenticate(inputData);

        if (null == jwtModel)
        {
            return BadRequest(new ApiResponseModel
            {
                Code = HttpCode.Fail,
                Message = HttpMessages.JwtDataError
            });
        }

        return Ok(new ApiResponseModel
        {
            Code = HttpCode.Success,
            Message = HttpMessages.LoginSucess,
            Data = jwtModel
        });
    }
}
JwtService
public class JwtService : IJwtService
{
    private readonly IUserRepository userRepository;
    private readonly IMerchantInfoRepository merchantInfoRepository;

    public JwtService(IUserRepository userRepository, IMerchantInfoRepository merchantInfoRepository)
    {
        this.userRepository = userRepository;
        this.merchantInfoRepository = merchantInfoRepository;
    }

    /// <summary>
    /// 驗證使用者
    /// </summary>
    /// <param name="JWTViewModel"></param>
    /// <returns></returns>
    public JWTViewModel Authenticate(JWTViewModel inputData)
    {
        List<UserInfo> userList = userRepository.Get(inputData.MerchantNo, new int[] { inputData.UserID });
        MerchantInfo merchantInfo = merchantInfoRepository.Get(inputData.MerchantNo, inputData.MerchantKey);

        // 找不到使用者, 或金鑰驗證
        if (!userList.Any()) { return null; }
        if (null == merchantInfo) { return null; }

        JWTViewModel jwtModel = (from x in userList
                                    select new JWTViewModel
                                {
                                    UserID = x.UserID,
                                    UserName = x.UserName,
                                    MerchantNo = x.MerchantNo,
                                    MerchantKey = inputData.MerchantKey
                                    }).First();

        // 設定 Jwt Token 聲明資訊 (自訂屬性)
        List<Claim> claims = new List<Claim>{
            new Claim("UserID", jwtModel.UserID.ToString()),
            new Claim("UserName", jwtModel.UserName),
            new Claim("MerchantNo", jwtModel.MerchantNo),
            new Claim("MerchantKey", jwtModel.MerchantKey)
        };

        // 發行者
        claims.Add(new Claim(JwtRegisteredClaimNames.Iss, ConfigJwt.Issuer));
        // 主體內容
        claims.Add(new Claim(JwtRegisteredClaimNames.Sub, jwtModel.UserID.ToString()));
        // 唯一識別碼
        claims.Add(new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()));

        // 建立一組對稱式加密的金鑰, 主要用於 Jwt 簽章之用
        var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(ConfigJwt.SignKey));

        // HmacSha256 有要求必須要大於 128 bits, 所以 key 不能太短, 至少要 16 字元以上
        var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);

        // Token Descriptor
        SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor
        {
            Issuer = ConfigJwt.Issuer,
            Subject = new ClaimsIdentity(claims),
            Expires = DateTime.Now.AddSeconds(ConfigJwt.Expires),
            IssuedAt = DateTime.Now,
            SigningCredentials = signingCredentials
        };

        // 產出所需要的 Jwt securityToken 物件, 並取得序列化後的 Token 結果(字串格式)
        JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
        SecurityToken securityToken = tokenHandler.CreateToken(tokenDescriptor);
        string token = tokenHandler.WriteToken(securityToken);
        jwtModel.Token = tokenHandler.WriteToken(securityToken);

        return jwtModel;
    }
}

 

appsettings.json

{
"Jwt": {
    "Issuer": "",
    "SignKey": "",
    "ValidateLifetime": true,
    "Expires": 10
  }
}

留言

Top