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