[Day30] C# MVC 實際撰寫後之我的想法 - C#&AspNetCore

在此先說明一下,大概花了一個月製作了了一套商品出貨訂單系統的Side Project。
儲存庫:https://github.com/yuhsiang237/Order-System
影片網址:https://www.youtube.com/watch?v=nSk6OZjhDMY

所以這文章內容會包含很多我與之前的製作系統經驗的分享。

首先,MVC現在已經算是很主流的網站架構了,我在之前有使用過Laravel、ASP.NET Core MVC這兩套框架來設計系統,因此在整體的觀感上因為設計模式是有共同的出發點,所以上手難度是差不多,甚至說是無痛換上的感覺。因此,依照這個推論,如果仍有下個世代的框架出來,也是標榜MVC那也夠無痛接手,頂多就是學一些基礎語法糖而已。

而專案內通常有少部分會寫到演算法,我目前處理過最複雜演算法就是BFS,主要是要產生訂單的子代、父代,並且將畫圖產生出來,就是一張圖論的圖。這算是我在專案裡面處理過最複雜的結構。
這是我的演算法筆記用的存庫,遇到一些算法需求會整理上去。
https://github.com/yuhsiang237/AlgorithmStudy

而MVC基本上跟演算法無關。他只是給你一套共同認知的地基,讓你能夠跟其他人有共同基礎去打造事物,這有什麼好處呢?能夠增加維護性,因為在沒基礎下,需要知道整體脈絡怎麼寫必須去翻舊的程式碼,算是一部分技術債,而MVC則把這技術債降低,讓你能夠比較無痛的降低學習成本,有點像是一套彼此知道的內功,學會就能打天下。

再來我說說Laravel與ASP.NET Core MVC的差異。

在除錯上,C#有著地表最強IDE Visual Studio撐腰,所以除錯非常容易,只要中斷點下去跑一次八成就知道要改哪裡了。Laravel則需要肉眼DEBUG,或是自己插Return返回點。

在撰寫速度上,C#需要編譯時間,Laravel是直譯語言則不需要所以會快一些。但如果考量到C#便利的中斷點,那最後把時間拉長我認為是C#會是優勝。

在型別上,C#屬於強型別,每屬性有特定的類型,能夠讓自己很清楚知道要塞什麼數值。而Laravel則是弱型別,宣告是用$var所以大專案下來可能會不知道哪個是哪個變數,可能會造成數值塞錯報錯。

在直譯與編譯上,C#需要編譯才能知道撰寫成功與否,Laravel則是要當場跑才知道。如果C#可以成功編譯,那可能上線只會有邏輯錯誤;Laravel則是需要當場去試錯才比較可能知道問題。

在開發速度上,C# MVC有一套框架給你但並不給你便利的內建系統,如Seed塞值、資料庫遷移、內建Redius、等等。而Laravel則是全都給你了,所以你就只要寫邏輯就好。

在版本升級上,C# MVC基礎就View Model Controller所以升級版本只要考量套件問題,相容就沒問題,而現在ASP.NET Core仍然是用基礎寫法寫就能無痛升級。Laravel則是要考量目錄變動,因為它幫你內建很多功能,每個版本的寫法也不太相同。

在資料結構上,C#內建超多的資料結構,如List、Array、Directory、Hashset…幾乎是C++的STL演算法有的他都全包還擴增,而Laravel則是沒有,需要自己找套件包或自己寫,在處理複雜資料時就會稍微辛苦一點。

在其他領域應用上,C#能夠開發網頁、桌面程式、library、windows、linux(asp.net core)。而Laravel則是只能開發網頁。

總結

這是我對於這兩套MVC的差異理解,但總體來說還是看自己需求,以目前來看未來我會選擇C# MVC,因為在維護上方便許多、在台灣工作環境相對友善(?)。而更重要的是需求分析、設計模式、演算法方面,因此目前可能會往這邊走XD。
總之,哪種方法能夠讓我花最少成本大概就是它了,畢竟寫程式某種意義就是避免浪費時間。

[Day29] C# MVC 被使用在哪些地方 - C#&AspNetCore

先以C#語言實際用在那些地方開始講好了,可以製作遊戲Unity、命令列程式、跨平台(ASP.NET Core)、手機(但很少)、桌面應用、資料分析。唯一缺點大概就是嵌入式,因為通常嵌入式有記憶體限制,會壓的很低,會用純C,但是C#基本上語法跟C也很相像。

而ASP.NET的MVC則是在處理網頁為多數,如現在普遍在台灣看到的政府機構網頁、大型服務網頁都可以用這套MVC去實現,算是處理大量資料已經有認證過可以實踐的框架了。

簡言之,就是沒什麼框架能與之抗衡,光是Visual Studio的除錯能力就能減少很多開發者的時間。以及背後的支持微軟,是自己家目前產品。而Linux則是偏向雜湊派,東加西加拼湊,整合度比較低。因此,目前首選還是微軟。

結論

總而言之,除了嵌入式、手機外,一些後端API、網頁程式基本上都可以看到它的蹤影。

[Day28] C# MVC FluentValidation 好用的驗證格式套件 - C#&AspNetCore

這回介紹一個必用的驗證神器FluentValidation,在處理驗證時會比原本Model驗證好上很多。因為他能適應不同的條件變化。

首先先看一張DEMO圖,就是類似表單驗證什麼都可以使用。

安裝

1.開啟NuGet管理畫面

2.因為我是.net core專案所以安裝FluentValidation.AspNetCore,如果不是就裝第一個

撰寫一個簡單驗證

在此以帳密登入為範例

1.先建立一個Model
Models/LoginViewModel.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace FluentValidationExample.Models
{
public class LoginViewModel
{
public string Account { get; set; }
public string Password { get; set; }
}
}

2.撰寫該Model驗證
Models/Validator/LoginViewModel.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentValidation;

namespace FluentValidationExample.Models.Validator
{
public class LoginValidator : AbstractValidator<LoginViewModel>
{
public LoginValidator()
{
RuleFor(x => x.Account).NotNull().WithMessage("名稱不可為空");
RuleFor(x => x).Custom((x, c) =>
{
if (x.Password == "" || x.Password == null)
{
c.AddFailure("Password", "密碼不可為空");
}
});
}

}
}

最主要是繼承抽象的AbstractValidator,而裡面傳進去的多型是LoginViewModel,即要驗證的Model。
再來就是用RuleFor去驗證。
我寫了一個內建、一個客製的。
此外,如果有連結資料庫也能從外面把Context傳進來。

3.再來撰寫登入的Post事件
Controllers/HomeController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[HttpPost]
public IActionResult Index(LoginViewModel model)
{
LoginValidator validator = new LoginValidator();
ValidationResult result = validator.Validate(model);
if (!result.IsValid)
{
foreach (ValidationFailure failer in result.Errors)
{
ModelState.AddModelError(failer.PropertyName, failer.ErrorMessage);
}
}
return View();
}

基本上就是套用剛剛完成的validator。
當!result.IsValid時將錯誤訊息塞給ModelState,在前台就能顯示。

4.前台畫面cshtml撰寫。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@model FluentValidationExample.Models.LoginViewModel;

<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>
<div asp-validation-summary="ModelOnly"></div>

<form asp-action="Index">
Account <input asp-for="Account" /> <span asp-validation-for="Account"></span>
<br />
Password <input asp-for="Password" /> <span asp-validation-for="Password"></span>
<br />
<input type="submit" value="submtit" />
</form>

主要就塞入asp-validation-for,他會與ModelState匹配,剛剛增加的錯誤訊息就能收到並顯示在畫面上了。

6.完成,然後嘗試用Debug跑一次

總結

為什麼不用Model驗證呢?
因為不同情況如某些時候欄位必填,有時不是,這時就會有問題。
而FluentValidation則完整破除這問題,並且能以邏輯方式條列撰寫,在釐清數據正確與否非常有幫助。
因此建議還再使用Model驗證的人可以快點換成FluentValidation,複雜的條件也能迎刃而解。

在此附上專案連結:
https://github.com/yuhsiang237/FluentValidation-Project

更多範例可以參考官方:
https://docs.fluentvalidation.net/en/latest/aspnet.html

[Day27] C# MVC 回傳獨立頁面或整體頁面 - C#&AspNetCore

在此介紹如何讓某些頁面不套用原本預設的_Layout.cshtml。

如常見的登入頁面,常常是一個獨立畫面,這時只要回傳PartialView。
UserController.cs

1
2
3
4
public IActionResult SignIn()
{
return PartialView();
}

而預設會套用_Layout.cshtml的則是回傳View。
HomeController.cs

1
2
3
4
public IActionResult Hone()
{
return View();
}

結論

簡單介紹一下,此外如果要控制預設吃什麼Layout可以從以下檔案設定:

1
\Views\_ViewStart.cshtml

[Day26] C# MVC LINQ實作常見用法 - C#&AspNetCore

在這篇中主要紀錄一些常見的C# LINQ寫法,通常搭配資料庫EF Core使用。

1.簡單取得匹配某ID的”複數”筆資料列

1
2
3
4
var query = 
from a in _context.Roles
where a.Id == user.RoleId
select a;

2.取得某”單筆”物件資料

作法1:

1
2
3
4
5
6
7
8
9
10
var query = (from a in _context.Users
where a.Account == userAccount
select new
{
Id = a.Id,
Account = a.Account,
RoleId = a.RoleId,
Name = a.Name,
Email = a.Email
}).FirstOrDefault()

作法2:

1
var query =  _context.Products.FirstOrDefault(x => x.Id == shipmentOrderDetail.ProductId);

差別在一個全是用.的方式寫。

3.群組資料結果。此例將日期變成群組,統計每日期具有的資料筆數。
且使用匿名class建立搜尋結果。

1
2
3
4
5
var query = from a in _context.ShipmentOrders
where a.IsDeleted != true
where a.DeliveryDate.Value.Month == DateTime.Now.Month
group a by a.DeliveryDate.Value.Date into g
select new { DeliveryDate = g.Key, Count = g.Count() }

4.主表a之後inner join b表的資料。

1
2
3
4
5
6
7
8
9
10
11
12
var query = from a in _context.ReturnShipmentOrders
join b in _context.ShipmentOrders
on a.ShipmentOrderId equals b.Id
where a.IsDeleted != true
select new ReturnShipmentOrderViewModel
{
Id = a.Id,
Number = a.Number,
ShipmentOrderNumber = b.Number,
Total = a.Total,
ReturnDate = a.ReturnDate
};

5.LINQ子查詢,Category表資料在另一個多對多表上,使用子查詢去取得結果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var query = from a in _context.Products
where a.IsDeleted != true
select new ProductIndexViewModel
{
Id = a.Id,
Number = a.Number,
Name = a.Name,
CurrentUnit = a.CurrentUnit,
Price = a.Price,
Description = a.Description,
Category = (from b in _context.ProductProductCategoryRelationships
join c in category
on b.ProductCategoryId equals c.Id
where b.ProductId == a.Id
select new ProductCategory
{
Id = c.Id,
Name = c.Name
}).ToList()
};

6.取得總數統計,在此例以取得某年的Total加總。

1
2
3
4
var query = (from a in _context.ShipmentOrders
where a.IsDeleted != true
where a.FinishDate.Value.Year == DateTime.Now.Year
select a.Total).Sum()

7.取得查詢的結果數量

1
2
3
var query = (int)(from a in _context.Products
where a.IsDeleted != true
select a.Id).Count();

結論

主要在這篇中紀錄實作常見會寫到的作法。
此外,如果熟悉LINQ後反而會造成原生SQL語法淡忘,所以還是要加減注意一下原生作法。

Arduino LED與Button控制

前言

想以僅有的材料做個LED與按鈕控制實驗。
並且藉由撰寫程式來監控按鈕,並控制LED明暗。

材料

  1. LED 3MM 紅色 x 1 :發光二極體 (1.8V~2.2V,5mA~20mA)
  2. Arduino版 x 1:任意一塊都可 (UNO)
  3. 220歐姆電阻(5%) x 1:紅紅棕金
  4. 2.2k歐姆電阻(5%) x 5:紅紅紅金

首先是LED的線路,要讓LED能夠穩定且不燒壞的亮燈,要先計算電阻多少。
OUTPUT會給予5V,因此用歐姆定律

1
V = IR

(5-2)=0.02R
R=150ohm

但是LED其實可以低於20mA,規格是5mA~20mA
因此嘗試串聯一顆220歐姆電阻算看看:
(5-2)=I*220
I=3/220
=0.013A
=13mA
還在範圍內!所以就用220歐姆電阻了!

再來是按鈕部分,這時要扯到一個新概念就是上拉電阻
上拉電阻:
在數位電路中,上拉電阻(英語:Pull-up resistors)是當某輸入埠未連接設備或處於高阻抗的情況下,一種用於保證輸入訊號為預期邏輯電平的電阻元件。他們通常在不同的邏輯裝置之間工作,提供一定的電壓訊號。

主要用上拉電阻協助我們按鈕提供穩定的訊號。

當開關斷開的時候,邏輯閘的輸入訊號被上拉到Vin;當開關閉合的時候,邏輯閘的輸入端與地面相連,輸入訊號接近0伏特。

通常上拉電阻都會接10K電阻,但我手邊只有2.2k所以串個5顆。

V=IR
5 = I*(2200*5)
I=0.0004A=0.4mA

Code.ino

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const int BUTTON_PIN = 7;  // 按鍵的接腳
const int LED_PIN = 8; // LED燈的接腳

int buttonState = 0; // 按鈕的狀態

void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT); //設定LED的PIN腳為輸出
pinMode(BUTTON_PIN, INPUT); //設定按鈕的接腳為輸入,因為我們要讀取它的狀態
}

void loop() {
buttonState = digitalRead(BUTTON_PIN); //讀取按鍵的狀態

if(buttonState == LOW){ //如果按鍵按了
Serial.print(buttonState);
digitalWrite(LED_PIN, HIGH); //LED就亮了
}else{ //如果按鍵是未按下
Serial.print(buttonState);
digitalWrite(LED_PIN, LOW); //LED就不亮
}
}

總結

發現電路需要理論部分的加強,否則接出來的線路會有問題。
以此來說就是上拉電阻,如果不知道他的原理,會覺得接個5V出來是要做什麼。

參考資料
https://zh.wikipedia.org/wiki/%E4%B8%8A%E6%8B%89%E7%94%B5%E9%98%BB

[Day25] C# MVC API JWT Token驗證 (下) JWT實作 - C#&AspNetCore

在這篇中主要實作JWT的登入認證,取得Token、透過Token對有權限的API進行存取。

安裝

1.先安裝JWT套件,因為我是用AspNetCore所以用
Microsoft.AspNetCore.Authentication.JwtBearer

撰寫

1.撰寫產生JWT Token的程式碼。
TokenController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[HttpPost("GenerateToken")]
[AllowAnonymous]
public ActionResult<string> GenerateToken()
{
string issuer = "JwtAuthDemo";
string signKey = "ASASA1@AAASASASASA1@AAASAS"; // 需要大於16字元
string Account = "TestAccount";

var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signKey));
var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);

// 建立 SecurityTokenDescriptor
var tokenDescriptor = new SecurityTokenDescriptor
{
Issuer = issuer,
Subject = new ClaimsIdentity(new[]
{
new Claim(JwtRegisteredClaimNames.Sub, Account), // User.Identity.Name
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), // JWT ID
new Claim("test", "123") // 可加入自訂內容在聲明裡
}),
Expires = DateTime.Now.AddMinutes(60),
SigningCredentials = signingCredentials
};
var tokenHandler = new JwtSecurityTokenHandler();
var securityToken = tokenHandler.CreateToken(tokenDescriptor);
var serializeToken = tokenHandler.WriteToken(securityToken);
return serializeToken;
}

2.撰寫測試JWT請求的程式碼
在此撰寫兩個一個是取得所有聲明,一個是取得該Token得獨立識別值。
TokenController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Authorize]
[HttpGet("GetClaims")]
public IActionResult GetClaims()
{
return Ok(User.Claims.Select(p => new { p.Type, p.Value }));
}

[Authorize]
[HttpGet("GetTokenID")]
public IActionResult GetTokenID()
{
var jti = User.Claims.FirstOrDefault(p => p.Type == "jti");//每個token的識別值,如果要禁用某token就可以把識別值存到資料庫當黑名單
return Ok(jti.Value);
}

3.增加服務
Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public void ConfigureServices(IServiceCollection services){


services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
// 當驗證失敗時,回應標頭會包含 WWW-Authenticate 標頭,這裡會顯示失敗的詳細錯誤原因
options.IncludeErrorDetails = true; // 預設值為 true,有時會特別關閉

options.TokenValidationParameters = new TokenValidationParameters
{

// 一般我們都會驗證 Issuer
ValidateIssuer = true,
ValidIssuer = "JwtAuthDemo",

// 通常不太需要驗證 Audience
ValidateAudience = false,
//ValidAudience = "JwtAuthDemo", // 不驗證就不需要填寫

// 一般我們都會驗證 Token 的有效期間
ValidateLifetime = true,

// 如果 Token 中包含 key 才需要驗證,一般都只有簽章而已
ValidateIssuerSigningKey = false,

// JWT簽證密要,之後可自行設定在appsettings.json
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("ASASA1@AAASASASASA1@AAASAS"))
};
});

}

4.添加授權

1
2
3
4
5
6
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
app.UseAuthentication();
app.UseAuthorization();

}

5.完成測試結果:
取得Token

用剛剛產生的JWT Token取得聲明

用剛剛產生的JWT Token取得Token識別值

總結

C#在處理JWT上算是蠻簡潔的,不用寫太多行,主要複雜點就在Startup.cs增加服務、產生Token這兩個步驟。
專案存庫位置:https://github.com/yuhsiang237/ASP.NET-Core-RESTfulAPI-JWT

然後附上DEMO影片一個是用Postman測試,一個是程式碼跑起的時的Swagger
swagger demo:

postman demo:

Arduino限流LED實驗

前言

想以僅有的材料做個LED限流的實驗,並使用電路公式算出理論數值實作。

材料

  1. LED 3MM 紅色 x 1 :發光二極體
  2. Arduino版 x 1:任意一塊都可
  3. 220歐姆電阻 x 3:紅紅棕金

理論部分

首先我們Arduino電源接3.3V,而LED紅的電流需要0.02A(20mA)電壓則是2.0-2.2V,姑且算2.0電壓。

這時3.3V怎樣降到2.0V?就是使用電阻
那要多少歐姆的電阻值呢?
透過歐姆定律
在電路學裏,歐姆定律(英語:Ohm’s law)表明,導電體兩端的電壓與通過導電體的電流成正比,以方程式表示。

1
V=IR

(3.3-2.0) = 0.02 * R
1.3 = 0.02R
R = 1.3/0.02
R = 65
所以我們需要65Ω(Ohm)的電阻
但天殺的= =肯定不會這麼剛好有65歐姆的電阻,這時就要用現有的電阻兜出來。
並聯電阻可以降低歐姆。
依照克希荷夫電路定律推倒後得到以下公式:

1
Rt = 1/(1/R1+1/R2+1/R3+...+1/Rn)

以現有的220歐姆電阻要降到65就要稍微計算一下

3顆:
1/(1/220+1/220+1/220) = 73
4顆:
1/(1/220+1/220+1/220+1/220) = 55

發現3顆電阻約可以到73很接近65了,第4顆則是會低過65。
我們使用電阻是為了降低電壓、限流,讓IC能夠正常啟用下避免過熱燒毀、穩定運作,所以差不多就使用3顆電阻並聯。

電路圖

接線圖

現實:

總結

用理論公式推後就可以用任意的電壓、電阻來兜
避免IC燒壞、延長使用時間

((ps.懂了後會覺得一些範例書上只給一個電阻然後不告訴你為什麼、怎麼算實在太黑了。

參考資料
電阻色碼計算
https://zh.wikipedia.org/wiki/%E9%9B%BB%E9%98%BB%E8%89%B2%E7%A2%BC
電阻放置
https://zhidao.baidu.com/question/1610875004126323107.html
限流電阻計算器
https://gc.digitw.com/Program/Resistor4LED/Resistor4LED.htm
為什麼LED要加上電阻
http://yehnan.blogspot.com/2012/03/arduinoled220-ohm.html
控制LED
https://blog.jmaker.com.tw/arduino-tutorials-3/
歐姆定律
https://zh.wikipedia.org/wiki/%E6%AC%A7%E5%A7%86%E5%AE%9A%E5%BE%8B
並聯電路
https://zh.wikipedia.org/wiki/%E4%B8%A6%E8%81%AF%E9%9B%BB%E8%B7%AF#%E9%9B%BB%E9%98%BB%E5%99%A8
克希荷夫電路定律
https://zh.wikipedia.org/wiki/%E5%9F%BA%E7%88%BE%E9%9C%8D%E5%A4%AB%E9%9B%BB%E8%B7%AF%E5%AE%9A%E5%BE%8B#%E5%9F%BA%E7%88%BE%E9%9C%8D%E5%A4%AB%E9%9B%BB%E6%B5%81%E5%AE%9A%E5%BE%8B%EF%BC%88KCL%EF%BC%89
歐姆定律
https://physcourse.thu.edu.tw/mengwen/%E6%99%AE%E7%89%A9%E5%AF%A6%E9%A9%97/%E5%AF%A6%E9%A9%97%E9%A0%85%E7%9B%AE/%E6%AD%90%E5%A7%86%E5%AE%9A%E5%BE%8B/

[Day24] C# MVC API JWT Token驗證 (上) JWT基礎知識 - C#&AspNetCore

在先前有介紹過Cookie-Based驗證於MVC專案裡。

[Day12] C# MVC 驗證與授權,登入與登出 - C#&AspNetCore

而這回主要探討的是Token-Based驗證,也就是API常用的JWT (Json Web Token)。

什麼是JWT

JSON Web Token (JWT) is a proposed Internet standard for creating data with optional signature and/or optional encryption whose payload holds JSON that asserts some number of claims.

為JSON Web Token,是一個建議的網路標準,用來建立簽名,且在其中可以視需求加入自訂的JSON內容。

簡言之,就是令牌(Token),你可以拿這令牌去要資料。

JWT優點

跨域: 不受網域限制,可用來串接第三方應用,如 OAuth。
安全性: 不使用 Cookie 因此不會受到 CSRF 攻擊,不過 Token 並不能防護 XSS 攻擊,還是需要特別注意。
行動端: 可用於不支援 Cookie 的裝置上,且現在網站和 APP 串接普遍使用 Token 授權。

JWT格式

主要由三個部分組成:

1
header.payload.signature

Header

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

alg:定義加密的方式產生signature,如上是HS256表示使用是HMAC-SHA256.
typ:Token類型

常被使用的header如:typ、cty、alg、kid、x5c、x5u、crit
可自行添加參考:https://en.wikipedia.org/wiki/JSON_Web_Token

Payload

1
2
3
4
{
"loggedInAs": "admin",
"iat": 1422779638
}

loggedInAs:這是客製化claim,以此來說是管理員。
iat:這是標準的claim,表示此JWT發布的時間。

標準的claim如:iss、sub、aud、exp、nbf、iat、jti
可自行添加參考:https://en.wikipedia.org/wiki/JSON_Web_Token

通常為了讓前端或後端好存取多加上一些識別值。

Signature

1
2
3
4
5
HMAC_SHA256(
secret,
base64urlEncoding(header) + '.' +
base64urlEncoding(payload)
)

簽名,確保資料完整性的雜湊簽章。

將以上三者用Base64url Encoding結果即為JWT

生成:

1
const token = base64urlEncoding(header) + '.' + base64urlEncoding(payload) + '.' + base64urlEncoding(signature)

結果:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI

使用情境

將此Token放置於API請求的Header中:
例如:

1
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI

傳送給後端

JWT衍伸問題

Token的竊取問題,因為JWT發出去就不能銷毀,只能自己等待過期。
所以有了Refresh Token的出現。

使用Refresh Token後:
縮短每個Token時間,過期時再重新索取。

1
2
3
4
5
{
"access_token":"l0XG52TQx", //Token
"refresh_token":"KWI3JOkFA", //Refresh Token
"expires_in":3600 //幾秒過期
}

當Token過期後,使用refresh_token呼叫一個API要取一個新Token。
refresh_token是存在自己資料庫內的,所以被盜用可以馬上刪除掉!

在寫程式碼時可以在某API發現過期後立即呼叫更換。
或是用Token裡面帶入的payload去確認更換時機也可以,但可能會有時區問題。

此外,如果用Refresh Token仍會有一些安全疑慮,就是發出的token還是要等到時間自己逾期。
(不如自己建表記Token還比較安全,所以在下面我寫了額外補充)

額外補充

除了JWT外也有一些自訂的做法能用來驗證APP手機端。
如DB自建Token表,可能像是這樣:

每次操作API都帶入Token,DB檢驗一次。
被盜用時想刪隨時能刪、能強制登出。

Tokens資料表

UserID Token Expires_In
1 AJIASOIASssarJ 2021-10-16
2 AASKOPASKOAPaS 2021-10-17
3 AASK45as4daaS 2021-10-14

流程大概像這樣:
API>撈DB檢查>大於時間,刪除Token>回傳授權失敗

雖然比較耗資料庫資源但我覺得是最安全的方式XD。
而且沒有發出要等到時間到才會逾期的問題

總結

在這回中主要探討JWT是什麼、前置的知識理論。
發現了JWT的一些好處與壞處。

因此如果有疑慮好像自己建立Token表還是最安全的XD

而在下篇中將會實作JWT的機制。

參考資料
https://ithelp.ithome.com.tw/articles/10199102
https://en.wikipedia.org/wiki/JSON_Web_Token

[Day23] C# MVC Web API版本增加Swagger - C#&AspNetCore

在上回中我們介紹了 [Day22] C# MVC API版本控制 - C#&AspNetCore ,實作了RESTful API的版控。

而這回將要進行API的部屬,使用Swagger UI。

找了很多篇算是找到一篇
https://codingfreaks.de/dotnet-core-api-versioning/
可以同時使用API Versioning跟Swagger套件的範例

在這篇中主要配置API並且切版本

1.先用Nuget安裝以下套件:
Swashbuckle.AspNetCore
Swashbuckle.AspNetCore.Annotations
Swashbuckle.AspNetCore.Swagger
Swashbuckle.AspNetCore.SwaggerGen
Swashbuckle.AspNetCore.SwaggerUI

2.再來是我們要修改的檔案

首先是Swagger的定義類別

~/ConfigureSwaggerOptions.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
namespace ApiVersioningSample
{
using System;
using System.Linq;

using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;

using Swashbuckle.AspNetCore.SwaggerGen;

/// <summary>
/// Configures the Swagger generation options.
/// </summary>
/// <remarks>
/// <para>
/// This allows API versioning to define a Swagger document per API version after the
/// <see cref="IApiVersionDescriptionProvider" /> service has been resolved from the service container.
/// </para>
/// <para>Taken from https://github.com/microsoft/aspnet-api-versioning.</para>
/// </remarks>
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
#region member vars

private readonly IApiVersionDescriptionProvider _provider;

#endregion

#region constructors and destructors

/// <summary>
/// Initializes a new instance of the <see cref="ConfigureSwaggerOptions" /> class.
/// </summary>
/// <param name="provider">
/// The <see cref="IApiVersionDescriptionProvider">provider</see> used to generate Swagger
/// documents.
/// </param>
public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider)
{
_provider = provider;
}

#endregion

#region explicit interfaces

/// <inheritdoc />
public void Configure(SwaggerGenOptions options)
{
// add a swagger document for each discovered API version
// note: you might choose to skip or document deprecated API versions differently
foreach (var description in _provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
}
}

#endregion

#region methods

/// <summary>
/// Internal implementation for building the Swagger basic config.
/// </summary>
/// <param name="description">The description object containing the.</param>
/// <returns>The generated Open API info.</returns>
private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
{
var info = new OpenApiInfo
{
Title = "Sample API",
Version = description.ApiVersion.ToString(),
Description = @"<p>Sample API with versioning including Swagger.</p><p>Partly taken from <a href=""https://github.com/microsoft/aspnet-api-versioning"">this repository</a>.</p>",
Contact = new OpenApiContact
{
Name = "Yu Hsiang",
}
};
if (description.IsDeprecated)
{
info.Description += @"<p><strong><span style=""color:white;background-color:red"">VERSION IS DEPRECATED</span></strong></p>";
}
return info;
}

#endregion
}
}

這類別主要用來放Swagger客製化的數值

再來是啟用Swagger

~/Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using TodoAPI.Models;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Microsoft.Extensions.Options;
using ApiVersioningSample;
using Microsoft.AspNetCore.Mvc.ApiExplorer;

namespace TodoAPI
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// 資料庫配置
var connection = @"Server=.\SQLExpress;Database=TodoDB;Trusted_Connection=True;ConnectRetryCount=0";
services.AddDbContext<TodoDBContext>(options => options.UseSqlServer(connection));

// 啟用API版控功能
services.AddApiVersioning(
options =>
{
// reporting api versions will return the headers "api-supported-versions" and "api-deprecated-versions"
options.ReportApiVersions = true;
});

// swagger 配置
services.AddVersionedApiExplorer(
options =>
{
// add the versioned api explorer, which also adds IApiVersionDescriptionProvider service
// note: the specified format code will format the version as "'v'major[.minor][-status]"
options.GroupNameFormat = "'v'VVV";
// note: this option is only necessary when versioning by url segment. the SubstitutionFormat
// can also be used to control the format of the API version in route templates
options.SubstituteApiVersionInUrl = true;
});
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
services.AddSwaggerGen();


}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

// swagger 配置
app.UseSwagger();
app.UseSwaggerUI(
options =>
{
// build a swagger endpoint for each discovered API version
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
}
});


app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();

});


}
}
}

第48~60行:swagger 配置
第79~89行:swagger 配置

4.版本的類別撰寫:

~/Controllers/V1_1/VersionController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;

namespace TodoAPI.Controllers.V1_1
{
[ApiVersion("1.1")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class VersionController : ControllerBase
{
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "api v1.1" };
}
}
}

~/Controllers/V1_1/VersionController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;

namespace TodoAPI.Controllers.V1_1
{
[ApiVersion("1.1")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class VersionController : ControllerBase
{
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "api v1.1" };
}
}
}

~/Controllers/V2_0/VersionController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;

namespace TodoAPI.Controllers.V2_0
{
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class VersionController : ControllerBase
{
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "api v2.0" };
}
}
}

基本上就是改
第4行資料夾:V1_0 至 V1_1 至 V2_0
第6行ApiVersion:1.0 至 1.1 至 2.0
其他的Controller也是一樣,這邊就舉例VersionController.cs

5.啟用後範例:

專案連結

https://github.com/yuhsiang237/ASP.NET-Core-RESTfulAPI-Swagger

總覽

Swagger UI搭配版控,而在.net Core官方是沒有提供版本+Swagger配置的,所以踩很多雷。
這回參考了 https://codingfreaks.de/dotnet-core-api-versioning/ 的文章。
花了不少時間在除錯,最後終於把Swagger搭建起來。

參考資料
https://codingfreaks.de/dotnet-core-api-versioning/
https://docs.microsoft.com/zh-tw/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-3.1&tabs=visual-studio
https://medium.com/wenchin-rolls-around/%E8%BB%9F%E9%AB%94%E7%89%88%E6%9C%AC%E8%99%9F-ee446e1ad543