.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 } }