[實作筆記] 滲透測試報告、原因與調整方針

前情提要,測試報告

測試廠商使用 Acunetix 掃描與 Burp Suite 攔截封包進行測試:
報告大綱如下

  1. X-Frame-Options:Acunetix 發現相關弱點,且經 Burp 檢視 HTTP Response,發現於特定路徑尚未設定使用。
  2. X-XSS-Protection:經 Burp 檢視 HTTP Response,發現 於特定路徑尚未設定使用。
  3. X-Content-Type-Options:經 Burp 檢視 HTTP Response ,發現於特定路徑尚未設定使用。
  4. Strict-Transport-Security:經 Burp 檢視 HTTP Response ,發現於特定路徑尚未設定使用。。
  5. Content-Security-Policy:經 Burp 檢視 HTTP Response ,發現於特定路徑尚未設定使用。

Header 的用意

X-Frame-Options

  • 新版的瀏覽器使用 frame-ancestors
  • 用意:防範 ClickJack 攻擊

    Click Jacking 直譯為點擊劫持,常見的攻擊手段為,
    攻擊者透過 frame 嵌入受害網站;當受害者誤入攻擊者網站時(常用社交攻擊),
    會誤以為是正常網站。
    接下來就可以誘騙受害者,ex:輸入帳密,或是點擊特定連結或下載惡意軟體。

選項

  • DENY
  • SAMEORIGIN
  • ALLOW-FROM uri

X-XSS-Protection

  • 新版的瀏覽器透過 CSP (Content-Security-Policy) 來防止 XSS 攻擊
  • 用意:為提供舊版的瀏覽器防範 XSS 網站攻擊,這不屬於任何正式的規格書或草案之中

    XSS(Cross-Site Scripting),直接跨站腳本攻擊,手段非常的多樣,
    大多是在受害網站注入腳本語法(javascript),使用者瀏覽時就會遭受攻擊。

    ex:在留言區留下無限 loop 的 alert ,如果對方的網站沒有防範,
    使用者瀏覽留言區的時候,就會彈出關不完的 alter 視窗。
    (真正惡意的攻擊會更神不知鬼不覺)
    參考

選項

  • 0
    禁止 XSS 過濾。
  • 1
    啟用 XSS 過濾(通常瀏覽器是默認的)。如果檢測到跨站腳本攻擊,瀏覽器將清除頁面(刪除不安全的部分)。
  • 1;mode=block
    啟用 XSS 過濾。如果檢測到攻擊,瀏覽器將不會清除頁面,而是阻止頁面加載。
  • 1; report=<reporting-URI> (Chromium only)
    啟用 XSS 過濾。如果檢測到跨站腳本攻擊,瀏覽器將清除頁面並使用 CSP report-uri (en-US)指令的功能發送違規報告。

X-Content-Type-Options

  • 通知瀏覽器,不要自行猜測(MIME type sniffing)Content Type

原先 MIME type sniffing 是上古時代被設計來避免開發人員誤設 Content Type,
比如說明明是 javascript 檔, content Type 卻被設定 text/plain,
這時瀏覽器 sniff 判斷為 javascript 後,可以執行該 js 檔
但是被用來惡意攻擊,比如說連結 src/fake.jpg 看起來像一張圖片
實際上 Response 卻回傳可執行的 javascript (內含惡意的程式),
瀏覽器的 sniffing 行為會自行解析為腳本(javascript)並執行攻擊

選項

  • nosniff

Strict-Transport-Security

HTTP Strict-Transport-Security 回應標頭(簡稱為 HSTS (en-US))告知瀏覽器應強制使用 HTTPS 以取代 HTTP。
現代的瀏覽器優先使用 HTTPS 已是共識,不加這個 Header 也會出現不安全的警告

選項

  • max-age 指定秒數
  • includeSubDomains 包含子網域

Content-Security-Policy

這個坑很深,我另外再撰文說明,
可以透過另一個 Header Content-Security-Policy-Report-Only
產生報告逐一修正 Policy。

補充: 如何在 GCP Bucket 加上 header

GCP > Network Services > Load Balancing

  • Edit
  • Backend Configuration
  • 找到特定的 Bucket 點擊鉛筆符號
  • Edit backend bucket
  • 展開 ADVANCED CONFIGURATIONS (RESPONSE HEADERS)
  • Custom response headers > ADD HEADER
  • 輸入 Header Key 與 Value
    • X-Frame-Options: DENY
    • X-XSS-Protection: 1;mode=block
    • X-Content-Type-Options: nosniff

補充: 如何使用 Burp Suite 觀察

概念說明:
Burp 可以在你的本機建立一個 Proxy Server,
並透過它提供的瀏覽器(Chromium)連線到你指定的 uri 並進行欄截。

You ←→ Burp(Proxy) ←→ Internet。

注意: 網路上大多數的文章都會有 Proxy 的設定,其實這是可以省略的,
新版本的 Burp 用提供的瀏覽器就會連結 Burp Proxy。

  1. 下載並安裝 Burp Suite,Burp Suite Community Edition 版本即可
  2. 選擇 Temporary project > Next
  3. 選擇 Use Burp defaults > Start Burp
  4. 開啟攔截器: Proxy > Intercept is on
  5. 開啟網站: Proxy > Open Browser(Chromium) > Key In <target site URL>
  6. 決定欄截或丟棄 Proxy > Forward / Drop
  7. 查看歷史記錄 Proxy > HTTP / WebSockets history

參考

(fin)

[生活筆記] 2022 新冠(武漢)肺炎疫情

進況

2021 年 5 月 ~ 7 月進入 3 級後,理論上都是在 2 級的狀態,
入境人數每日約有 50~100 名確診者入境,
到了 2022 年 4 月以來,疫情指數成長
爆
近日單日確診已破 90000 例。

重複發生的事

  • 2020 搶/吵口罩
  • 2021 搶/吵疫苗
  • 2022 搶/吵快篩

該作的事

  • 載口罩
  • 勤洗手
  • 少去人多的地方
  • 注意自身生心理的變化

現況的因應方針

準備適量的快篩,以 2 人的小家庭其實 1 盒(5 劑)已經夠用,
買不到就作好該作的事,台灣的狀況是可以在幾周到幾個月內調整到位的。
(參考口罩、疫苗與雞蛋的狀況)
無症狀就正常生活,保持樂觀,不要過度被搧動性的偽新聞網軍談話節目影響,
如果真的要看,請深入研究各方的說詞,不要因喜好而偏重某些節目。

自覺有症狀先快篩,
陰性就保持正常生活,擔心的話請就醫。
陽性參考 CDC 指示,自我隔離或就醫,並聽從醫囑。

有症狀而無法篩檢的就可以先自我隔離,
所幸我的工作模式可以遠端處理大部份事宜,
家中也沒有老小,並且都接種過三劑疫苗。
正面面對目前的疫情,並不想介入過多的口水,
看數據作事,或許有點無情但是大局來說才是正確的方向。

分不清的名詞

角色有確診者、密切接觸者、其它
方針居家檢疫、居家隔離`、居家照護、
隔離、自主健康管理、自主防疫、
自我健康監測 ??、自主應變 ??

總覺得每天都在產生新名詞呢

資訊的取得

  • 參考國外數據
  • 聽取 CDC/(所在)縣市記者會
  • 多方參考海外新聞、數據報表
  • 盡可能獲取一些科普、醫療相關的知識,但是要注意背後的科學支持是否可信

心態上不要盲信,也不需要被風向帶到彼此敵視

未來的方向

長期來說,只能試著與病毒共存,
讓整體的生活恢復正常,問題是這個過程是要付出多少的代價 ?

第一個全面接種疫苗,
我們的第一劑覆蓋率只有 85% ,第二劑 80% ,第三劑  69%(資料來源 202205021,GOOGLE
對比世界其它國家不算低,但是疫苗的效力會隨時間下滑,所以定期要打加強劑,
此外仍有特定族群無法接種疫苗,特別為小孩與慢性病者,應儘快加速孩童疫苗的進口與接種.
而之前打氣下滑導致疫苗過期而被丟棄,實在十分浪費。

指揮中心發言人莊人祥 3 月底曾表示,莫德納及 BNT 約有 26.4 萬劑、AZ 則約 30 萬劑,
共計 56.4 萬劑疫苗因到期得銷毀。根據疾管署最新統計,
台灣 COVID-19 疫苗接種人口涵蓋率第 1 劑 84.57%、第 2 劑 79.84%、追加劑接種率 58.24%。

ETtoday <<3 天 1.6 萬例!瘋搶疫苗 醫嘆:丟掉你罵浪費,現在罵打不到?>> 記者陳俊宏/綜合報導

第二個,在不破壞醫療量能前提下,儘可能讓抵抗力高的人感染而達成自然免疫。
不論如何會有人因此死亡,特別是未接種者,
雖然接種疫苗中壯年在數據上中重率不高,
但是對於個體而言,代表的是一個活生生的生命。
沒有人想當那個 0.03 %。
不要過渡消耗醫療量能,
目前快篩的排隊或是 PCR 的排隊,有些人是為了領取保險金,取得確診証明。  
無症或輕症者,其實正在壓縮真正需要就醫的人的生命,  
讓醫護人員花費大量時間在作文書與檢驗作業。

無症或輕症者,雖是下策,可以透過線上或是購買成藥等方式來撐過疫情。
如果藥物足夠的話,快篩陽且有診狀,醫師就該判斷是否進行投藥,避免後續重症的發生。

(fin)

[活動筆記] 線上活動 TypeScript tips and Tricks with Matt

前情提要

YouTube 頻道Visual Studio Code
前陣子推出了由大神 Matt 所介紹有關 TypeScript 奇技淫巧影片
相當燒腦,特別作此記錄。

5:06 generic-table-component

難度:★★☆☆☆

這題需要有一些 React 的基礎概念,
首先我們有一些 React Component(以下簡稱 RC),
並且用 interface 建立相對應 props。

問題是如果要讓我們的 RC 更泛用應該怎麼作,
在這個例子中,我們建立了一個 Table RC(以下簡稱 Table) ,並且在 props 中傳入一些參數,
這個參數的型別,直接與 Table 產生了耦合,我們可用泛型(Generic)處理。

Before

1
2
3
4
5
6
7
8
interface TableProps {
items: { id: string }[];
renderItem: (item: { id: string }) => React.ReactNode;
}

export const Table = (props: TableProps) => {
return null;
};

After

1
2
3
4
5
6
7
8
interface TableProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}

export const Table = <T,>(props: TableProps<T>) => {
return null;
};

這裡是很簡單的泛型觀念,如果有寫過具備泛型的語言(ex:C#)應該不難理解,
另外需要注意的一個有關 React 的小小 Tricky
注意到 <T,> 這裡有一個小小的逗號,其實是不必要的,
但是在 React 的開發環境之中,角括號<>會被視為未封閉的 RC;
封閉的例子,ex:<myComponent></myComponent> 或是 <myComponent />
未了必免編輯器的警告,需要加上一個逗號,

另外這裡在寫 React Component 時,使用了 arrow function,
我們也可以換成一般的 function 寫法,這種寫法就可以省略 <T,> 這樣 tricky 的寫法

1
2
3
export const Table = function <T>(props: TableProps<T>) {
return null;
};

泛型不難理解,但是考量到需要具備的 React 知識,難度我給他兩顆星

11:19 get-deep-value

難度:★★★☆☆

這題要動態的取出物件深層的屬性的型別,
具體好處是可以讓編輯型別檢查

看一下影片中的範例 getDeepValue 是一個函數,可以用來取物件深層屬性的值
這裡我們看到 obj:any Matt 說道可以視作一個壞味道(不知道他對 unknown 的看法)

1
2
3
export const getDeepValue = (obj: any, firstKey: string, secondKey: string) => {
return obj[firstKey][secondKey];
};

看一下原始範例如下

1
2
3
4
5
6
7
8
9
10
const obj = {
foo: {
a: true,
b: 2,
},
bar: {
c: "12",
d: 18,
},
};

我的作法

如果比較直觀的寫法可以考慮寫個 ObjectType,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// this one is NOT from the sample

const obj: ObjType = {
foo: {
a: true,
b: 2,
},
bar: {
c: "12",
d: 18,
},
};

type FirstKeyType = "foo" | "bar";
type SecondKeyType = "a" | "b" | "c" | "d";
type ObjType = {
[key in FirstKeyType]: { a: boolean; b: number } | { c: string; d: number };
};
export const getDeepValue = (obj: ObjType //...

註:這裡有用到 index-signatures 的概念
看看官方文件的說法:

1
2
3
Sometimes you don’t know all the names of a type’s properties ahead of time, but you do know the shape of the values.

In those cases you can use an index signature to describe the types of possible values
1
const value = getDeepValue(obj, "foo", "a");

Matt 在影片中的作法

1
2
3
4
5
6
7
8
9
10
11
export const getDeepValue = <
TObj,
TFirstKey extends keyof TObj,
TSecondKey extends keyof TObj[TFirstKey]
>(
obj: TObj,
firstKey: TFirstKey,
secondKey: TSecondKey
) => {
return obj[firstKey][secondKey];
};

keyof 關鍵字可以直接參考官方文件

比較兩者的作法

我們加上了 Type,就是要為程式碼加上保護
我跟 Matt 的作法,下面的程式碼都會提出警告

1
var error_value = getDeepValue(obj, "error", "wrong");

但是無法避免以下的錯誤

1
var error_value = getDeepValue(obj, "foo", "c");

obj.foo 是不存在 c 屬性,這樣我們就需要動態判斷型別
看看 Matt 使用 keyof 作法。

1
2
3
4
5
6
7
export const getDeepValue = <TObj, TFirstKey extends keyof TObj>(
obj: TObj,
firstKey: TFirstKey,
secondKey: keyof TObj[TFirstKey]
) => {
return obj[firstKey][secondKey];
};

這裡有點不可思議,首先是 ts 的動態型別推導,
在呼叫方法 getDeepValue 的第 1 個參數會動態推導出 TObj 的型別
大概等價以下的型別

1
2
3
4
5
6
7
8
9
10
type TObj = {
foo: {
a: boolean;
b: number;
};
bar: {
c: string;
d: number;
};
};

keyof TObj 的值會是 foo | bar
而 TFirstKey 的型別,在第 2 個參數傳入之時決定是 foobar
不是上述兩種的參數傳入時,編輯會拋出錯誤警告,非常好用。
下一個燒腦的部份,第 3 個參數 secondKey: keyof TObj[TFirstKey]
透過類似陣列取值的手法,我們可以在傳入第 2 個參數時決定第 3 個參數的值,並且讓編輯器提供保護。
例如,第 2 個參數為 foo 時,第 3 個參數會限定只能傳入 "a"|"b"

這裡需要思考一下,並不難理解,所以給 3 顆星。

如果能應用得好的話,對於開發 ts Library 會相當有幫助
不過我覺得自已無法用得很得心應手,比如說,如果物件深度不固定,應該如處理?
又或者如果不用 keyof 的技巧,對一個已知的物件,我能不能作到類似的效果(編輯器動態檢查)?

28:54 conditional-types

難度:★★★☆☆

這其實是 TypeScript 一個基本的概念與技巧,
可以在官方文件看到更多資訊

一個標準的寫法如下

1
SomeType extends OtherType ? TrueType : FalseType;

記得這裡最終只會回傳 TrueType 或是 FalseType,而 SomeType 只是作為條件檢查。
當然也可以作多重的條件檢查

1
2
3
4
5
SomeType extends OtherType ?
TrueType :
SomeType extends OtherConditionType?
OtherThing :
FalseType;

35:20 更進一步

我們看一下怎麼應用 Condition Type 這個技巧?
參考error-messages-in-ts這個教案
在 deepEqualCompare 這個方法之中,陣列比較需要例外處理,
因為陣列比較永遠會回傳 false
原本的作法,會在程式的 runtime 作型別檢查並拋出例外

1
2
3
4
5
6
7
export const deepEqualCompare = <Arg>(a: Arg, b: Arg): boolean => {
// runtime handle
if (Array.isArray(a) || Array.isArray(b)) {
throw new Error("You cannot compare two arrays using deepEqualCompare");
}
return a === b;
};

而透過 condition type,可以讓編輯器在開發時直接檢查,看起來更加的優雅

1
2
3
4
5
6
export const deepEqualCompare = <Arg>(
a: Arg extends any[] ? "not allow array" : Arg,
b: Arg extends any[] ? "not allow array" : Arg
): boolean => {
return a === b;
};

這應該算是 TypeScript 的基本工,也有在官方文件上有很清楚的講解,
對我來說,第一次看到仍然相當驚訝,我給他三顆星
另外,我有兩個延申問題,

  1. 程式碼中有 any 算不算壞味道? 我是不是應該用 unknown[] 取代 any[]
  2. Arg extends any[] ? "not allow array" : Arg 明顯出現了重複,我有沒有什麼技巧可以消除這樣的重複?

其它

整個影片還有許多燒腦的部分,
Q&A 或是最後的挑戰都有相當多有趣的東西的可挖掘,
有機會我再補上。

參考

(fin)

[實作筆記] 自定義 Hexo Helper

前情提要

我的 Blog 目前是使用 Hexo 搭建的,
使用的主題是 landscape
隨著時光推移, Hexo 漸漸從人們的目光淡出,
主功能、文件、翻譯或是外掛、主題等…, 更新速度變慢很多

我想改寫主題當中的某個輔助函數(Helper), 稍微記錄一下作法。

實作

首先是檔案位置,一個是在根目錄底下建立 scripts 資料夾,
或者你正在使用主題(Theme)底下。
以我的例子來說,我會放在 themes/landscape/scripts 底下

參考下面的圖示

1
2
3
4
5
6
7
8
├── scripts
│ ├── index.js
│ └── your_helper_here.js
├── themes
│ └── the_theme_you_are_using
│ └── scripts
│ ├── index.js
│ └── your_helper_here.js

下一步,你需要建立 index.js,並在其入註冊你的函數,

1
2
3
4
//hexo.extend.helper.register(name, function)
hexo.extend.helper.register("helper_name", function () {
return "<div>your helper function </div>";
});

通常 helper 不會是一個簡單的函數,且會有 UI 相關的程式,
有為了方便管理,我會拆檔,再用 require 來引入函數

1
hexo.extend.helper.register("helper_name", require("./helper_file_name"));

最後,在 *.ejs 檔案中使用 helper

1
<%- helper_name() %>

參考

(fin)

[實作筆記] Dotnet Core 實作隨筆

Web/Api 專案統一錯誤處理

首先建立一個新的類別 ErrorHandlingFilter
繼承 ExceptionFilterAttribute 並複寫 OnException 函數

1
2
3
4
5
6
7
public class ErrorHandlingFilter : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
//do your logical here
}
}

在 Program.cs 中注入

1
builder.Services.AddControllers(options => { options.Filters.Add(new ErrorHandlingFilter()); });

專案更改 Root Namespace

預設會使用專案名稱作為整個專案的 Root Namespace,
但是我們可以在 csproj 檔中設定 RootNamespace 讓它與專案名稱有所差異

1
2
3
4
5
6
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>MyNameSpace</RootNamespace>
</PropertyGroup>
</Project>

Nuget

add nuget registry

1
dotnet nuget add source <registry url> -n SourceName -u UserName -p Password --store-password-in-clear-text

pack

1
dotnet pack -o <output folder> -c NUGET --no-dependencies --force  -p:PackageVersion=<version>

push

1
dotnet nuget push -s SourceName <Packed Version>

建立共用的 HttpClient

在 Program.cs 中註冊

1
2
3
4
5
6
7
builder.Services.AddHttpClient("ThirdService", (provider, client) =>
{
var o = provider.GetRequiredService<IOptions<ThirdServiceOptions>>();
client.BaseAddress = new Uri($"{o.Value.Url}/");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", o.Value.Token);
});

再透過 HttpClientFactory 具名取用

1
2
3
4
public MyService(IHttpClientFactory httpClientFactory)
{
HttpClient _httpClient = httpClientFactory.CreateClient("ThirdService");
}

最小化 Docker Image

這一種被稱為 Multiple Stage Builds 的 Docker 技巧,
主要的概念是將建置(Build)與部署(Deploy)的步驟分開,
最終只部署出營運所需的映象檔(Image)。

比如說下面的 Docker File 就是將 dotnet 專案作了兩步驟的處理

此外,如果我們更換適當的 Image,
比如說,在 Create Runtime Image Step,
mcr.microsoft.com/dotnet/aspnet:6.0 改為 mcr.microsoft.com/dotnet/aspnet:6.0-alpine
映象檔的 Size 將減少 46.6% (216.11 MB → 115.4 MB)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Build Step
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /source
# copy everything else and build app
COPY /src/WebAPI/. ./WebApi/
WORKDIR /source/WebApi
RUN dotnet restore 'WebApi.csproj'
RUN dotnet build 'WebApi.csproj' -c release -o /app --no-restore
RUN dotnet publish 'WebApi.csproj' -c release -o /app --no-restore

# Create Runtime Image Step
FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine
ENV ASPNETCORE_URLS=http://+:5110
WORKDIR /app
COPY --from=build /app ./
EXPOSE 5110
ENTRYPOINT ["dotnet", "WebApi.dll"]

參考

(fin)

[實作筆記] ASP.Net Core Config 的隨筆

提要

  • DI Configuration
  • 組態的層次
  • Options Pattern

DI Configuration

dotnet webapi 專案預設會注入 IConfiguration ,
在專案中的程式建構子放入 IConfiguration 就可以直接取得組態設定

1
2
3
4
5
6
7
8
9
10
11
12
private readonly IConfiguration _config;

public WeatherForecastController(IConfiguration config)
{
_config = config;
}

[HttpGet(Name = "GetWeatherForecast")]
public string Get()
{
return _config["ASPNETCORE_ENVIRONMENT"]!;
}

你可以將組態設定在 appsettings.json 之中
或是透過環境變數,使用環境變數有幾種作法

  • 設定在機器上
  • 設定在 Cloud Service 所提供的組態管理工具之中
  • 使用 launchSettings.json

在開發環境我會使用 launchSettings 好處是可以不需要真的在開發機的環境中設定環境變數,
對同時擁有多個專案的開發者而言,可以有效隔離不同的專案變數,避免彼此影響污染

組態的層次

為了方便管理,我們可以透過組態的層次來加以分門別類。
比如說:

1
2
3
4
5
6
7
8
"Level": {
"First": {
"Key": "1st Value"
},
"Second": {
"Key": "2nd Value"
}
},

一般的 JSON 或 XML 要作到這樣的層次結構都相當的簡單,
而要取用組態的方式如下:

1
var key = _config["Level:First:Key"];

不過我們要注意環境變數並無法使用:字符,也沒有 JSON 或是 XML 所能提供的結構,
這裡要注意可以用 __(雙底線(Double underscore))來建立層次。

1
2
$Level__First__Key
$Level__Second__Key

Options Pattern

Options Pattern 可以提供一種更物件導向的組態配置解決方案,
你只需要兩個步驟,即可實作 Options Pattern

  1. 建立 Option 物件
    我在這裡提供二種不同的方法,後面會再說明

    1
    2
    3
    4
    5
    6
    //方法一 Dictionary
    public class LevelOptions
    {
    public const string Level = "Level";

    public Dictionary<string,string> First { get; set; }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //方法二 Object
    public class LevelOption
    {
    public const string Level = "Level";

    public FirstOptions First { get; set; }
    }

    public class FirstOptions
    {
    public string Key { get; set; } = string.Empty;
    }
  2. 註冊 Option 物件

1
builder.Services.Configure<LevelOptions>(LevelOption.Level)

最後是讀取組態,在方法一(使用 Dictionary)的方法讀取

1
var key = _options.Value.First["Key"];

或是方法二(使用 Object)的方法讀取

1
var key = _options.Value.First.Key;

兩種方法都可以,物件化的方法需要建立多個物件,
但是在取用組態的過程就不會有 hard coding string;
反之 Dictionary 的方法可以少寫很多物件。

對我而言我比較偏向使用 Object 的方法去處理,只要適當的分類,
物件多並不是問題,而且通常一次不會增加太多的組態,可以迭代增量。

順代一提,如果組態檔層次太多,會是另一個困擾 ? 我的建議是,如果組態的深度到達 3 時,
需要考慮這是不是一個壞味道,是不是過度優化了 ?
實務上來說,我會避免建立超過 3 層,也很難會有一定要超過 3 層的情境,
如果你有什麼情境上需要超過 3 層,可以留言給我,我們可以一起討論。

在 Program.cs 使用

如果在 Program.cs 的注入階段,如果要取得組態,
可以用
(new ConfigurationManager()).GetSection("First")["Key"]
或是 Environment.GetEnvironmentVariable("Level__First__Key"); 等方法取得組態。
但是我更推薦 ServiceProvider 的 GetRequiredService 取得 Options

1
2
3
4
5
6
7
8
9
10
//注入在前
builder.Services.Configure<LevelOption>(builder.Configuration.GetSection(LevelOption.Level));
builder.Services.AddHttpClient("OtherService", (provider, client) =>
{
//透過 provider 取得 config
var o = provider.GetRequiredService<IOptions<LevelOptions>>();
client.BaseAddress = new Uri($"{o.Value.Url}/");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", o.Value.Token);
});

這樣就可以用一致的方法取得組態了。

參考

(fin)

[實作筆記] Hexo 更新 Google Analytics 4

前情提要

幾天前收到來自 Google Analytics 的這樣一封信

1
2
3
4
5
Google Analytics (分析) 4 即將取代通用 Analytics (分析)

我們即將以新一代的成效評估解決方案 Google Analytics (分析) 4,
取代通用 Analytics (分析)。從 2023 年 7 月 1 日起,通用 Analytics (分析) 資源將停止處理新的命中資料;
如果通用 Analytics (分析) 仍是您常用的工具,建議您採行相應措施,準備好改用 Google Analytics (分析) 4

實作步驟

參考以前我寫的這篇–如何讓 Google Analytics 追踪你的 Hexo Blog
我們知道在 hexo 的佈景主題 landscape 有提供舊版 GA 的設定,
我們只需要修改 theme/layout/_partial/google-analytics.ejs 即可
程式參考如下

1
2
3
4
5
6
7
8
9
10
11
12
13
<% if (theme.google_analytics){ %>
<!-- Google Analytics -->
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=<%= theme.google_analytics %>"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());

gtag('config', '<%= theme.google_analytics %>');
</script>
<!-- End Google Analytics -->
<% } %>

由於通用 GA 在 2023 年後就不再支援,所以我就不特地處理相容,直接切換到 GA4

參考

(fin)

[實作筆記] 簡單的方法 Mock TypeScript Interface

前情提要

我目前在實作一個 React 的專案,並結合 Storybook 來實現 CDD(component driven development),
我很喜歡 CDD 的概念,這裡解決 TDD 對 View 難以趨動開發的痛點,
但是又維持了 Unit Test 的基本精神,進一步結合 Chromatic 可以提供視覺化的檢核頁面,
作為與 PO/PM/UX/UI/DESIGNER 之間的溝通工具非常好用。

問題

我作了一個簡單的 Header 組件,
這通常是網站或 APP 上方的區塊,通常會提供一些額外的資訊或功能,
例如: 登入/登出,而這裡我使用 Firebase 作為實作登入/登出的功能,
作為主要的邏輯判斷,firebase 的 Auth 物件會被傳入

1
2
3
Header = (props: { sighOut: () => void; auth?: Auth }) => {
//...
};

這裡我們先不討論 Auth 的邏輯是否應該與 Header 相依(嗯,這裡看起來是個壞味道),
而是在我們的 Storybook 撰寫測試案例時,應該如何 mock Auth 物件。
我的程式都是用 TypeScript 與 tsx 寫的,
所以如果不符合型別,IDE 會直接報錯,程式跟本無法執行

1
2
3
4
5
6
7
8
const Template: ComponentStory<typeof Header> = () => (
<Header
sighOut={() => {
alert("mocked Signout");
}}
auth={mockAuth}
/>
);

試過的方法

  • jest Mock
  • ts-mockito
  • ts-auto-mock

以上的方法有的成功,有的失敗,最大的問題是在配置與撰寫其實很麻煩,
甚至會有同事說為什麼要用 TypeScript 自找麻煩呢 ?

逐一 mock 所有 interface 的屬性,對於一個像 Auth 這樣複雜的介面實作上相當麻煩,
而且大多 mock 的屬性都用不到

我的想法是,應該是我對 TypeScript 與其生態圈不夠了解,寫起來才會覺得綁手綁腳的,
不能推缷給程式語言,同時 TypeScript 強型別的好處,其實也幫了我不少。
仔細研究後,我找到一個簡單方法如下:

解法

不需要 mock Library , 不需要麻煩的設定或是第三方套件,
最簡單的寫法 as 就可以將物件 mock 成為你想要的 Interface

1
2
3
const mockAuth = {
currentUser: { email: "[email protected]" },
} as Auth;

這樣的寫法除了可以只 mock 必要的屬性外,
如果你物件中包含了不屬於介面應有的屬性,
TypeScript 將會提出警告,非常好用。

(fin)

[實作筆記] 升級現有基於 TypeScript 的 React 17 專案到 React 18

前情提要

升級 React 在官方的文件之中,似乎是一件極為簡單的事,
但是如果你的專案是基於 TypeScript 開發,
在前幾周應該無法順利升級,主要的原因是相對應的 @types 套件並未同時更新,
幸運的是,現在看到文章的你,已經可以順利更新了,請參考以下的步驟。

記錄

安裝 react、react-dom、@types/react、@types/react-dom

1
npm i [email protected] [email protected] @types/[email protected] @types/[email protected]

更改 ReactDOM.render 的寫法

原本的寫法

1
2
3
4
import React from "react";
import ReactDOM from "react-dom";

ReactDOM.render(<App />, document.getElementById("root"));

後來的寫法

1
2
3
4
5
6
import React from "react";
import ReactDOM from "react-dom/client";

const ele = document.getElementById("root") as Element;
const root = ReactDOM.createRoot(ele);
root.render(<App />);

特別注意要處理 Element 的型別

參考

(fin)

[踩雷筆記] Brave 瀏覽器與本機開發 http

前情提要

Brave 是一款主打安全、注重隱私的瀏覽器,雖然目前仍相當小眾,
但在開發者中有一定使用者。

https 通訊協定現在已經是一個 Web 服務、網站、api 的基本安全配備,但是對於開發者來說,
在本地開發常常僅使用 http 通訊協定就足以作功能性的驗証。

情境

使用 dotnet core 建立了一個 web api 的服務,預設的啟動站台 end point 為 http://localhost:5000
而 Brave 作為預設瀏覽器開啟後,會看到 403 的錯誤頁面。

1
2
3
找不到 localhost
網頁找不到此網址的網頁:http://localhost:5000/
HTTP ERROR 404

解決方法

正常的思路,應該是讓本地開發環境建立起 https 的連線,這裡又會有相關的憑証問題要處理,
應該有蠻多不同的作法,改天我再寫一篇相關的介紹。

短解很簡單,換個瀏覽器就可以了,
如果堅持要用 Brave ,可以至設定 > 防護 > 關閉「升級連線至 HTTPS」的開關即可。
順代一提,當我使用 hexo、React 等工具建立靜態網站時不會有這個問題。

參考

(fin)