---
# System prepended metadata

title: '[112]天方科技 ASP.net core 教育訓練 1120418([Authorize]身分驗證、JWT、DB動態轉換設定)'

---

# [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為了避免攻擊，預設限制檔案大小，如果有需求需額外設定

![](https://i.imgur.com/kBaIAxo.png)

![](https://i.imgur.com/PbNjEhM.png)


## 簡單型別
- 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
![](https://i.imgur.com/LBn5gJm.png)

### 常用的是第4點登錄格式，可以看到結果裡的GUID`0394B6BB-0583-4B5F-B0E4-94F479887064`
![](https://i.imgur.com/sS7cxkv.png)


## 資料放在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被拒絕
![](https://i.imgur.com/xE6AXvq.png)

### 使用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);
        }
```

![](https://i.imgur.com/Jmvnk6m.png)

### 屬性設定
```csharp=
public class DtoStudent
    { 
        public string? Gender { get; }    //如果不設set，代表只能使用get，變成唯獨屬性
    }
```

## JWT Authentication(身分驗證)

### 需要先從NuGet新增套件`Microsoft.AspNetCore.Authentication.JwtBearer`
![](https://i.imgur.com/loy9JVH.png)
### 註冊`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`
![](https://i.imgur.com/MYaMdoJ.png)

註冊前
![](https://i.imgur.com/cYsjQ8x.png)

註冊後
![](https://i.imgur.com/TtC5N1j.png)

```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>();
}
);
```
### 設定完成
![](https://i.imgur.com/56ao4wi.png)


### 如果沒有驗證會被拒絕使用
![](https://i.imgur.com/ApJDO2l.png)

## 創建`AuthController.cs`
新增空的API
![](https://i.imgur.com/7kV5VVI.png)

### 創建`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
![](https://i.imgur.com/b2bKkA2.png)
![](https://i.imgur.com/3nd3hg6.png)

### JWT驗證：`bearer JWT`
![](https://i.imgur.com/XioR9Jq.png)

### 通過驗證後，允許使用功能
![](https://i.imgur.com/cDd57qN.png)
![](https://i.imgur.com/ThiFE6Q.png)

### 將JWT驗證碼貼到JWT.io查看驗證碼轉譯內容
![](https://i.imgur.com/KnhB2RW.png)

參考：
[**JSON Web Tokens**](https://jwt.io/)

### 其中`"exp": 1714185066`是驗證碼過期時間
不論設定的時間是多久，系統都會預設偏移+5分鐘

![](https://i.imgur.com/7QsNOxw.png)
```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`頁面
![](https://i.imgur.com/8QBgIwh.png)


### Type選擇`Bearer Token`
![](https://i.imgur.com/RnRILLO.png)

### 將JWT填入Token輸入框
![](https://i.imgur.com/bFG4hzz.png)

### 驗證成功
![](https://i.imgur.com/TjnnI5M.png)


## 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`
![](https://i.imgur.com/HKhPuef.png)

![](https://i.imgur.com/lqQPQGh.png)

### 在`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
![](https://i.imgur.com/7CUKpVG.png)


## 前端Token驗證 待補