[JWT] Bearer Token in Asp.Net Core
[JWT] Bearer Token in Asp.Net Core
JWT (Json Web Token),主要是為了保護網路安全傳輸的資訊,透過 JWT 可以產生一把權杖,透過這把權杖,來驗證使用者的資訊,細節可以參考附錄4。
JWT Token (權杖)
權杖主要由三個部分所組成(Header, Payload,以及Signature)。
{{"alg":"HS256","typ":"JWT"}.{"nameid":"ROBERT.CHEN@RFF.COM","http://schemas.microsoft.com/ws/2008/06/identity/claims/role":"Admin","exp":1532966362,"iss":"Robert Chen","aud":"Robert For Fun"}}
"token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiJST0JFUlQuQ0hFTkBSRkYuQ09NIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW4iLCJleHAiOjE1MzI5NjYzNjIsImlzcyI6IlJvYmVydCBDaGVuIiwiYXVkIjoiUm9iZXJ0IEZvciBGdW4ifQ.5wl6YqmVI9IHSDSgFw_i1bW5OpZuiKGJ9s38V0HiGog"
Header 表頭
表頭主要定義演算法,與權杖的類別。
{"alg":"HS256","typ":"JWT"}
上面的內容是指HMACSHA256
演算法,權證的類別為"JWT"
Payload
定義權杖的內容
{"nameid":"ROBERT.CHEN@RFF.COM","http://schemas.microsoft.com/ws/2008/06/identity/claims/role":"Admin","exp":1532966362,"iss":"Robert Chen","aud":"Robert For Fun"}
在此的 Payload 總共由五個部分所組成
- nameid : 主要是用來加密的金鑰。
- role : 表示此權杖的角色是什麼。
- exp : 此權杖預期超時的時間。
- iss : 表示 issuer,代表此憑證發行者。
- aud : 表示 audience,代表此憑證合法的接收者。
Signature
透過 Header 與 Payload 的資訊,產生一個簽名,且此簽名不可逆。
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiJST0JFUlQuQ0hFTkBSRkYuQ09NIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW4iLCJleHAiOjE1MzI5NjYzNjIsImlzcyI6IlJvYmVydCBDaGVuIiwiYXVkIjoiUm9iZXJ0IEZvciBGdW4ifQ.5wl6YqmVI9IHSDSgFw_i1bW5OpZuiKGJ9s38V0HiGog"
擴充服務
由於系統可能會加載註冊很多服務到容器中,透過擴充方法,將JWT服務的內容移至擴充方法。
Note : 擴充方法是靜態類別,且第一個參數必須是 this
開始。
public static class ServicesExtension
{
public static void ConfigureJWT(this IServiceCollection services, IConfiguration config)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(
options => {
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = config["Token:ValidIssuer"],
ValidAudience = config["Token:ValidAudience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Token:ValidSignKey"])),
RequireExpirationTime = true,
};
}
);
}
}
註冊服務
將JWT註冊至服務容器中,並且啟用認證機制。
public void ConfigureServices(IServiceCollection services)
{
...
services.ConfigureJWT(Configuration);
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
...
app.UseAuthentication();
...
app.UseMvc();
}
application.json
預設把權證的主要資訊,寫在 application.json 中,透過它來控制預設的設定檔。
{
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"
}
},
"Console": {
"LogLevel": {
"Default": "Warning"
}
}
},
"ConnectionString": {
"Default": "..."
},
"Token": {
"ValidIssuer": "Robert Chen",
"ValidAudience": "Robert For Fun",
"ValidSignKey" : "Robert.Chen@RFF.Com"
}
}
認證
當要存取某個 API 時,希望該 API 需要經過授權認證,只需要對於該項控制器(Controller)的方法(Action),設定屬性即可。
// HTTPGet 屬性,無須授權。
[HttpGet]
// 只要有授權即可
[HttpGet, Authorize]
// 授權並驗證授權的角色
[HttpGet, Authorize(Roles = "Admin")]
當然,也可以對整個控制項做授權控制。
[Authorize]
public class XXXController : Controller
{
...
}
Note : 屬性可以針對控制項或是方法去做設定。
取得權證
經過屬性的設定,對於某個控制項或是方法限制後,需要取得授權,才可以呼叫該控制項或是該方法。 建立新的控制來存取權證。
[Produces("application/json")]
[Route("api/Auth")]
public class AuthController : Controller
{
private bool IsValidUsername(string username)
{
return String.IsNullOrEmpty(username);
}
private bool IsValidPassword(string password)
{
return String.IsNullOrEmpty(password);
}
private IConfiguration _config;
public AuthController(IConfiguration config)
{
_config = config;
}
[AllowAnonymous]
[HttpPost]
public IActionResult Login([FromBody]LoginModel user)
{
Claim[] claims = null;
if (user == null)
return BadRequest();
if (IsValidUsername(user.Username) || IsValidPassword(user.Password))
return BadRequest();
if (IsValidAdmin(user) && IsValidPassword(user))
{
claims = new[] {
new Claim(JwtRegisteredClaimNames.NameId, "ROBERT.CHEN@RFF.COM"),
new Claim(ClaimTypes.Role, "Admin")
};
}
else if (user.Username == "John" && user.Password == "test1234")
{
claims = new[] {
new Claim(JwtRegisteredClaimNames.NameId, "John@RFF.COM"),
new Claim(ClaimTypes.Role, "others")
};
}
else {
return BadRequest();
}
var signKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Token:ValidSignKey"]));
var signCredentials = new SigningCredentials(signKey, SecurityAlgorithms.HmacSha256);
var tokenOptions = new JwtSecurityToken(
issuer: "Robert Chen",
audience: "Robert For Fun",
claims: claims,
expires: DateTime.Now.AddMinutes(5),
signingCredentials: signCredentials
);
var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions);
return Ok(new { Token = tokenString });
}
private static bool IsValidPassword(LoginModel user)
{
return string.Compare(user.Password, "12345", true) == 0;
}
private static bool IsValidAdmin(LoginModel user)
{
return String.Compare(user.Username, "Robert", true) == 0;
}
}
Note : 如果沒有對控制項的方法設定路由條件,即直接呼叫該控制項即可。(http://xxx:ooo/api/Auth)
若是有設定路由屬性([Route("login")],則 URL 為 http://xxx:ooo/api/Auth/Login
憑證參數選項
The issuer is the actual server that created the token (ValidateIssuer=true)|實際的驗證伺服器
The receiver of the token is a valid recipient (ValidateAudience=true)|權證的接收者
The token has not expired (ValidateLifetime=true)|權證的有效期限
The signing key is valid and is trusted by the server (ValidateIssuerSigningKey=true)|權證是否有效以及此權證是否經過伺服器信任
Issuer: The first parameter is a simple string representing the name of the web server that issues the token|伺服器名稱
Audience: The second parameter is a string value representing valid recipients|合法的接受者
Claims: The third argument is a list of user roles, for example, the user can be an admin, manager or author (we are going to add roles in the next post)|使用者的角色資訊
Expires: The fifth argument is DateTime object that represents the date and time after which the token expires|憑證多久後會預期
取得憑證
將相關資訊填好後,送交給驗證API,API驗證完後,會發行驗證後的 Token。
--- Post ---
POST /api/auth HTTP/1.1
Host: localhost:55747
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: 6e2feb5e-4e1b-4e7b-88f7-a1592efc2509
{
"UserName" : "Robert",
"Password" : "12345"
}
--- End of Post
--- Response ---
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiJST0JFUlQuQ0hFTkBSRkYuQ09NIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW4iLCJleHAiOjE1MzI5NjkyMTEsImlzcyI6IlJvYmVydCBDaGVuIiwiYXVkIjoiUm9iZXJ0IEZvciBGdW4ifQ.cugbDM-jf5R54FLUU6NDB9zNHfCOOgVfpzBv9lPzhXs"
}
填入權杖到表頭中
由於我們使用的是 JWT,表頭中需填入 Authorization : Bearer $token
GET /api/values HTTP/1.1
Host: localhost:55747
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiJST0JFUlQuQ0hFTkBSRkYuQ09NIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW4iLCJleHAiOjE1MzI5NjU4NTgsImlzcyI6IlJvYmVydCBDaGVuIiwiYXVkIjoiUm9iZXJ0IEZvciBGdW4ifQ.R50uTvyjQWMIhge8nb2KgYK_Sh1W5qCHNIPKMZ4G6Fg
Cache-Control: no-cache
Postman-Token: 72baa000-f91c-457c-b635-9ec8d8db8df6
留言
張貼留言