# [112]天方科技 ASP.net core 教育訓練 1120418([Authorize]身分驗證、JWT、DB動態轉換設定)
## HTTP request 取得來源預設順序
1. Form fields
2. The request body (For controllers that have the [ApiController] attribute.)
3. Route data
4. Query string parameters
5. Uploaded files
### 如果是使用`[HttpGet]`只有`Route data`、`Query string`兩種
參考:
[**ASP.NET Core 中的資料繫結─來源**](https://learn.microsoft.com/zh-tw/aspnet/core/mvc/models/model-binding?view=aspnetcore-7.0#sources)
### 上傳檔案使用form-data
`Content-Type:multipart/form-data`
每個分隔可以放入資料,例如純文字、二進位等資料,ASP.Net為了避免攻擊,預設限制檔案大小,如果有需求需額外設定


## 簡單型別
- Boolean
- Byte, SByte
- Char
- DateOnly
- DateTime
- DateTimeOffset
- Decimal
- Double
- Enum
- Guid
- Int16, Int32, Int64
- Single
- TimeOnly
- TimeSpan
- UInt16, UInt32, UInt64
- Uri
- Version
參考:
[**簡單型別**](https://learn.microsoft.com/zh-tw/aspnet/core/mvc/models/model-binding?view=aspnetcore-7.0#simple-types)
### Guid 全球唯一碼
長度16bytes
可以在Visual Studios產生
### 工具→建立GUID

### 常用的是第4點登錄格式,可以看到結果裡的GUID`0394B6BB-0583-4B5F-B0E4-94F479887064`

## 資料放在body
### 使用Get
```csharp=
[HttpGet("year/{year}")]
public IActionResult GetStudentByYear(int year, [FromForm] DtoStudent dtoStudent)
{
string name = dtoStudent.Name!;
string gender = dtoStudent.Gender!;
var linq = from s in _context.s30_student
where s.std_year == year
&& (name != null && name.Length > 0 ? s.std_name.StartsWith(name) : true)
&& (gender != null && gender.Length == 1 ? s.std_sex == gender : true)
group s by new { s.cls_id, s.std_year } into g
where g.Count() > 0
select new { cls_id = g.Key.cls_id, year = g.Key.std_year, count = g.Count(), Students = g.ToList() };
return Ok(linq);
}
```
### 在Swagger被拒絕

### 使用Post
查詢動作可以使用Post方式進行
```csharp=
[HttpPost("year/{year}")]
public IActionResult GetStudentByYear(int year, [FromForm] DtoStudent dtoStudent)
{
string name = dtoStudent.Name!;
string gender = dtoStudent.Gender!;
var linq = from s in _context.s30_student
where s.std_year == year
&& (name != null && name.Length > 0 ? s.std_name.StartsWith(name) : true)
&& (gender != null && gender.Length == 1 ? s.std_sex == gender : true)
group s by new { s.cls_id, s.std_year } into g
where g.Count() > 0
select new { cls_id = g.Key.cls_id, year = g.Key.std_year, count = g.Count(), Students = g.ToList() };
return Ok(linq);
}
```

### 屬性設定
```csharp=
public class DtoStudent
{
public string? Gender { get; } //如果不設set,代表只能使用get,變成唯獨屬性
}
```
## JWT Authentication(身分驗證)
### 需要先從NuGet新增套件`Microsoft.AspNetCore.Authentication.JwtBearer`

### 註冊`Authentication`服務,設定JWT驗證時的`token`資訊
初始版本
```csharp=
builder.Services
.AddAuthentication(//JwtBearerDefaults.AuthenticationScheme net7已預設
)
.AddJwtBearer(options => options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true, //簽發者
ValidIssuer="Skytek.com.tw", //驗證內容可以自訂
ValidateAudience = true, //接收者
ValidAudience="http://www.Skytek.com.tw",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("my password")), // 發行數位簽章,使用對稱式金鑰,將字串轉為byte陣列,交由key檢查數位簽章
ValidateLifetime = true, //驗證有效期限
});
```
將設定放入`appsettings.json`,`ConnectionStrings`下方
```csharp=
"ConnectionStrings": {
"edu": "Data Source=.;Initial Catalog=edu;Persist Security Info=True;TrustServerCertificate=true;User ID=sa;Password=localhost"
},
"JWT": {
"Key": "FF13FC14-DE54-49F0-8D51-DCD1C676377F", //工具→建立GUID→登錄格式
"Issuer": "Skytek.com.tw",
"Audience": "http://Skytek.com.tw"
}
```
透過`Configuration[]`引入設定檔
```csharp=
builder.Services
.AddAuthentication(//JwtBearerDefaults.AuthenticationScheme 選擇JWT驗證 net7已預設
)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true, //簽發者
ValidIssuer = builder.Configuration["JWT:Issuer"], //讀取appsettings設定檔
ValidateAudience = true, //接收者
ValidAudience = builder.Configuration["JWT:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JWT:Key"]!)), //對等式金鑰,呼叫索引器[],"!"表示不會出現null情況
//IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration.GetSection("JWT:Key").Value!)) //呼叫方法()
ValidateLifetime = true, //有效期限
};
});
```
### 原先註冊服務後需要去使用`UseAuthentication()`
```csharp=
//app.UseAuthentication(); //原先需要增加,順序要在UseAuthorization()前 net7已預設
app.UseAuthorization(); //使用授權
app.MapControllers();
app.Run();
```
## Controller驗證設定
### `Authorize`放置
`Action`後
```csharp=
[HttpGet,Authorize] //身分驗證,如果沒有指定角色,只要通過驗證就能使用API
//or
[HttpGet]
[Authorize(Roles = "Admin, Users")] //指定角色,不符合角色的話即使有驗證也不能進入,有區分大小寫
```
### Swagger `Authorize`功能註冊
需要先從NuGet新增套件`Swashbuckle.AspNetCore.Filters`

註冊前

註冊後

```csharp=
builder.Services.AddSwaggerGen();
//↓
builder.Services.AddSwaggerGen(options =>
{
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme //自訂名稱
{
Description = "bearer{token}", //輸入token時的提示描述,可自訂
In = ParameterLocation.Header, //參數位址
Name = "Authorization", //參數名稱
Type = SecuritySchemeType.ApiKey //安全碼種類為ApiKey
});
options.OperationFilter<SecurityRequirementsOperationFilter>();
}
);
```
### 設定完成

### 如果沒有驗證會被拒絕使用

## 創建`AuthController.cs`
新增空的API

### 創建`DtoLogin.cs`
```csharp=
namespace Edu.Dto
{
public class DtoLogin
{
//登入需要的資訊
public string Name { get; set; } //帳號
public string Password { get; set; } //密碼
public string DB { get; set; } //之後切換DB可以使用
public bool RememberMe { get; set; } //登入資訊記憶(有資安問題,可用可不用)
}
}
```
### 因為要用`Dto`會將資料包在`body`,所以使用`Post`
先注入`IConfiguration`,此為內建服務
```csharp=
private readonly IConfiguration _configuration; //內建服務
public AuthController(IConfiguration configuration)
{
_configuration = configuration;
}
```
實作Login以及產生JWT驗證功能
```csharp=
[HttpPost]
[AllowAnonymous] //允許匿名者,沒有加Authorize時預設
public IActionResult Login(DtoLogin login)
{
//尚未使用DB,先行自訂
//如果不加(),會先執行&&在執行||,所以不加()也行
if((login.Name != "clone" && login.Name != "eric") || login.Password != "pwd") { //檢查帳號
return BadRequest("Login Fail");
}
var roles = new List<string> //login的角色身分
{
"Admin",
"clone"
};
if(login.Name == "eric") roles = new List<string>{ "Users"};
var token = CreateToken(login, roles); //呼叫自訂方法回傳的token
return Ok(token);
}
//建立token
private string CreateToken(DtoLogin login, List<string> roles)
{
//Identity Principal
List<Claim> claims = new List<Claim>
{
new Claim(ClaimTypes.Name, login.Name), //使用ClaimTypes.Name屬性放置登入的Name
//由於是要放在token的資料,所以不要放入密碼等敏感性資料
new Claim(ClaimTypes.GroupSid, login.DB), //使用ClaimTypes沒有DB屬性,所以選擇一個不常使用的屬性來放置,GroupSid = 群組身分號碼
new Claim("Comp", "Skytek"), //也可自訂屬性
};
roles.ForEach(r => claims.Add(new Claim(ClaimTypes.Role, r))); //放置角色種類
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Key"]!)); //在program.cs註冊也有使用,但是在這裡要用_configuration
var cre = new SigningCredentials(key, SecurityAlgorithms.HmacSha512); //數位簽章要使用剛剛宣告的key,以及設定演算碼
var token = new JwtSecurityToken(
issuer: _configuration["JWT:Issuer"],
audience: _configuration["JWT:Audience"],
claims: claims,
expires: dtoLogin.RememberMe ? DateTime.Now.AddYears(1) : DateTime.Now.AddSeconds(30), //設定RememberMe和有效期限
signingCredentials:cre //數位簽章
);
var jwt = new JwtSecurityTokenHandler().WriteToken(token); //產生JWT
return jwt;
}
```
## 設定完成後測試
### 首先透過`[HttpPost]Login`取得JWT


### JWT驗證:`bearer JWT`

### 通過驗證後,允許使用功能


### 將JWT驗證碼貼到JWT.io查看驗證碼轉譯內容

參考:
[**JSON Web Tokens**](https://jwt.io/)
### 其中`"exp": 1714185066`是驗證碼過期時間
不論設定的時間是多久,系統都會預設偏移+5分鐘

```csharp=
var token = new JwtSecurityToken(
issuer: _configuration["JWT:Issuer"],
audience: _configuration["JWT:Audience"],
claims: claims,
expires: dtoLogin.RememberMe ? DateTime.Now.AddYears(1) : DateTime.Now.AddSeconds(30), //設定RememberMe和有效期限
signingCredentials:cre
);
```
### 如果要修改時間偏移,可以在註冊服務時設定
```csharp=
builder.Services
.AddAuthentication()
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = builder.Configuration["JWT:Issuer"],
ValidateAudience = true,
ValidAudience = builder.Configuration["JWT:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JWT:Key"]!)),
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero //預設驗證有效期間除了自行設定的時間,會加上系統預設的5分鐘,這裡將系統預設時間設為0
};
});
```
## [Authorize]放置
### 個別Action驗證
```csharp=
[HttpGet]
[Authorize(Roles = "Admin, Users")] //角色可以不設
```
### 整個Controller驗證,這時可以在個別Action使用`AllowAnonymous`匿名者,不用經過驗證
```csharp=
[Route("api/[controller]")]
[ApiController]
[Authorize]
//個別Action設定允許匿名者
[HttpGet]
[AllowAnonymous]
```
### 整個專案驗證,在`program.cs`註冊服務,如果有Login功能,需要加上`[AllowAnonymous]`標籤,避免Login功能也被鎖住
此設定在Swagger不會生效,可能是Swagger沒有包含此功能,可改用Postman測試
```csharp=
builder.Services.AddControllers(options => { options.Filters.Add(new AuthorizeFilter()); });
```
## Postman `Token`驗證
### 選擇`Authorization`頁面

### Type選擇`Bearer Token`

### 將JWT填入Token輸入框

### 驗證成功

## DB轉換設定
### 在`appsettings.json`裡新增`ConnectionStrings`設定
```json=
"ConnectionStrings": {
"edu": "Data Source=.;Initial Catalog=edu;Persist Security Info=True;TrustServerCertificate=true;User ID=sa;Password=localhost",
"db1": "Data Source=.;Initial Catalog=db1;Persist Security Info=True;TrustServerCertificate=true;User ID=sa;Password=localhost"
}
```
### 新增`Context`擴充類別`eduContext.partial.cs`,也可以`eduContext2.cs`
使用`eduContext.partial.cs`可以與擴充的`Context`變成巢狀結構,`eduContext2.cs`則會變成另外的`class`


### 在`program.cs`註冊`HttpContext`相依性
`HttpContext` 用於封裝個別 `HTTP` 要求和回應的所有資訊,`IHttpContextAccessor`只是取用`HttpContext`實例的接口,用 Singleton 的方式就可以供其它物件使用。
`builder.Services.AddSingleton<Interface, 實作Interface的class>()`:類似global變數,整個應用程式只會有一個物件,可被共用,生命週期持續到應用程式結束
```csharp=
builder.Services.AddDbContext<eduContext>(
options => options.UseSqlServer(builder.Configuration.GetConnectionString("edu")));
//↓
builder.Services.AddDbContext<eduContext>(); //options改在DBContext的OnConfiguring依HttpContext.User動態設定
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); //為了給DBContext取得HttpContext.User
```
參考:
[**HttpContext 類別**](https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.aspnetcore.http.httpcontext?view=aspnetcore-7.0)
### 設定擴充功能,讓DB可以動態修改
```csharp=
public partial class eduContext
{
//private readonly IConfiguration _configuration;
private readonly HttpContext? _httpContext;
public eduContext(DbContextOptions<eduContext> options, IHttpContextAccessor httpContext) //需使用IHttpContextAccessor作為接口
: base(options)
{
_httpContext = httpContext.HttpContext;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
//base.OnConfiguring(optionsBuilder);
if(!optionsBuilder.IsConfigured) //判斷optionsBuilder是否設定
{
var db = _httpContext?.User.Claims //取得是否有做過身分驗證的資料,沒有的話就是空的
.Where(c => c.Type == ClaimTypes.GroupSid) //取得當初AuthController.cs設定DB時的位置
.Select(c => c.Value)
.SingleOrDefault() ?? "edu"; //無授權者預設的db 必須回傳單筆
optionsBuilder.UseSqlServer($"Server=.;Database={db};Trusted_Connection=True;TrustServerCertificate=true"); //複製ConnectionStrings的設定,將DB的部分改為動態
}
}
}
```
### 可以再登入時決定使用的DB

## 前端Token驗證 待補