昨天我們講完了JWT 的主要架構後,今天的重點主要在ASP.NET Core 專案實現 Token-based 的身分驗證與授權,最簡單的方式就是透過 JWT 進行實作,主要有三大重點,產生有效Token、驗證Token、對特硬API作授權設定
首先的話,我們要先去安裝Microsoft.AspNetCore.Authentication.JwtBearer
套件
到appsettings.json
裡加入我們JWT 一些組態設定
"JwtSettings": {
"Issuer": "IMAC",
"SignKey": "bJs3iqzDSP1qiTzWeMJa2cMsQFji2q6DL5exm0wVKo21NczRvpfE5m7oUE1VCp4F",
"ExpireMinutes": 720
}
接著我們去創一個檔案Helpers/JwtHelper.cs
,這檔案主要是來寫JWT 所需要的設定
using System.Security.Claims;
using System.Text;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
public class JwtHelpers
{
private readonly IConfiguration Configuration;
public JwtHelpers(IConfiguration configuration)
{
this.Configuration = configuration;
}
public string GenerateToken(string userName, int expireMinutes = 30)
{
var issuer = Configuration.GetValue<string>("JwtSettings:Issuer");
var signKey = Configuration.GetValue<string>("JwtSettings:SignKey");
var claims = new List<Claim>();
claims.Add(new Claim(JwtRegisteredClaimNames.Sub, userName));
// Token 的主體內容
claims.Add(new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()));
// JWT ID
//claims.Add(new Claim(JwtRegisteredClaimNames.Iss, issuer));
//claims.Add(new Claim(JwtRegisteredClaimNames.Aud, "The Audience"));
//claims.Add(new Claim(JwtRegisteredClaimNames.Exp, DateTimeOffset.UtcNow.AddMinutes(30).ToUnixTimeSeconds().ToString()));
//claims.Add(new Claim(JwtRegisteredClaimNames.Nbf, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()));
//claims.Add(new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()));
claims.Add(new Claim("roles", "Admin"));
claims.Add(new Claim("roles", "Users"));
var userClaimsIdentity = new ClaimsIdentity(claims);
// 建立一組對稱式加密的金鑰,主要用於 JWT 簽章之用
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signKey));
// HmacSha256 MUST be larger than 128 bits, so the key can't be too short. At least 16 and more characters.
var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);
// Create SecurityTokenDescriptor
var tokenDescriptor = new SecurityTokenDescriptor
{
Issuer = issuer,
//Audience = issuer,
// 由於你的 API 受眾通常沒有區分特別對象,因此通常不太需要設定,也不太需要驗證
//NotBefore = DateTime.Now, 預設值就是 DateTime.Now
//IssuedAt = DateTime.Now, 預設值就是 DateTime.Now
Subject = userClaimsIdentity,
Expires = DateTime.Now.AddMinutes(expireMinutes),
SigningCredentials = signingCredentials
};
// 產出所需要的 JWT securityToken 物件,並取得序列化後的 Token 結果(字串格式)
var tokenHandler = new JwtSecurityTokenHandler();
var securityToken = tokenHandler.CreateToken(tokenDescriptor);
var serializeToken = tokenHandler.WriteToken(securityToken);
return serializeToken;
}
}
這部分可以說就是昨天Payload部分,而這裡是依 RFC 7519 的規格來寫,它總共定義了 7 個預設的 Claims
claims.Add(new Claim(JwtRegisteredClaimNames.Sub, userName));
claims.Add(new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()));
claims.Add(new Claim(JwtRegisteredClaimNames.Iss, issuer));
claims.Add(new Claim(JwtRegisteredClaimNames.Aud, "The Audience"));
...
再來這部分是可以自行擴充使用者身分,並使用roles 加入
claims.Add(new Claim("roles", "Admin"));
claims.Add(new Claim("roles", "Users"));
這部分是要讓你的 ASP.NET Core 能夠認得使用者傳入的 Bearer Token
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.IncludeErrorDetails = true; // 當驗證失敗時,會顯示失敗的詳細錯誤原因
options.TokenValidationParameters = new TokenValidationParameters
{
// 簽發者
ValidateIssuer = true,
ValidIssuer = builder.Configuration.GetValue<string>("JwtSettings:Issuer"),
// 接收者
ValidateAudience = false,
ValidAudience = "JwtAuthDemo",
// Token 的有效期間
ValidateLifetime = true,
// 如果 Token 中包含 key 才需要驗證,一般都只有簽章而已
ValidateIssuerSigningKey = false,
// key
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration.GetValue<string>("JwtSettings:SignKey")))
};
});
記得還要記得加上中介軟體AddAuthorization()
,一定要放在UseAuthorization
上面,因為要記得使用者要先驗證過才會授權過
接這我們去新增一個新的控制器JwtController.cs
這裡我們在最上方加上[Authorize]
,是因為我們需要使用者有驗證過才能使用到特定的API
接著在~/gentoken
這API上加入[AllowAnonymous]
是讓每位使用者不用先驗證正就可以產Token,不然前面要先驗證才能產Token有矛盾
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace DB_CRUD.Controllers
{
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class JwtController : ControllerBase
{
private readonly JwtHelpers _jwtHelpers;
public JwtController(JwtHelpers jwtHelpers)
{
_jwtHelpers = jwtHelpers;
}
// 這裡用來產生我們的Token
[AllowAnonymous]
[HttpPost("~/gentoken")]
public IActionResult GenToken(string username)
{
if (string.IsNullOrEmpty(username))
{
return BadRequest();
}
var token = _jwtHelpers.GenerateToken(username);
return Ok(token);
}
// 來看看我們在Claims裡有有哪些屬性和內容
[HttpGet("~/claims")]
public IActionResult GetClaims()
{
return Ok(User.Claims.Select(p => new { p.Type, p.Value }));
}
// 回傳我們剛剛在產Token時輸入的username
[HttpGet("~/username")]
public IActionResult GetUserName()
{
return Ok(User.Identity.Name);
}
// 傳回Jwt的id
[HttpGet("~/jwtid")]
public IActionResult GetUniqueId()
{
var jti = User.Claims.FirstOrDefault(p => p.Type == "jti");
return Ok(jti.Value);
}
}
}
重要提醒,記得要在註冊剛才寫的JwtHelper
,不然你到死也不會成功
builder.Services.AddSingleton<JwtHelpers>();
話說產Token有了,驗證Token有了,但我們要去做驗證這動作,這裡有兩種方式可以來完成,第一種方式就是使用Postman裡的Auth機制來幫我們驗證
先去產Token
在Auth 選擇驗證方式和加入剛才的Token,沒錯誤的話結果就會呈現出來
要是現在我們不是用Postman 來做驗證是使用swagger的話要現到Program.cs的AddSwaggerGen去做更動
builder.Services.AddSwaggerGen(options => {
options.AddSecurityDefinition("BearerAuth", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
Description = "JWT Authorization header using the Bearer scheme."
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "BearerAuth"}
},
new string[] { }
}
});
});
這裡就不多做說明,大家可以到連結去看看 link
接下來一樣先去產Token
載來你會看到跟之前的swagger不一樣,多了一個Authorize
的按鈕,它就是來幫我們做驗證的東西
如果有輸入Token 後應該會發現旁邊的鎖會變成鎖起來的樣子,但這裡不一定代表驗證是正確,還是要依你API 去跑的狀況來看
今天花了好一大段時間在整理JWT ,希望大家能看得懂我所寫的鐵人賽,哪今天就希望大家連假愉快~~~
參考資料: