Compare commits
3 Commits
352059915a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 964e4f9af3 | |||
| e07a02ae69 | |||
| fb9ad5df43 |
BIN
content/posts/2026/2026-01-04_用 Modern C++ 的方式寫程式吧/featured.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
214
content/posts/2026/2026-01-04_用 Modern C++ 的方式寫程式吧/index.md
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
---
|
||||||
|
slug: "[C++ 筆記] 用 Modern C++ 的方式寫程式吧"
|
||||||
|
title: "[C++ 筆記] 用 Modern C++ 的方式寫程式吧"
|
||||||
|
description:
|
||||||
|
toc: true
|
||||||
|
authors:
|
||||||
|
- awin
|
||||||
|
tags:
|
||||||
|
- c++
|
||||||
|
categories:
|
||||||
|
- Programming
|
||||||
|
series:
|
||||||
|
- C++ 筆記
|
||||||
|
date: 2026-01-04T00:00:00
|
||||||
|
lastmod: 2026-01-04T00:00:00
|
||||||
|
featuredVideo:
|
||||||
|
featuredImage:
|
||||||
|
draft: false
|
||||||
|
enableComment: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modern C++ 已經改變很多了
|
||||||
|
|
||||||
|
<!--more-->
|
||||||
|
|
||||||
|
### 歷史包袱的重量
|
||||||
|
|
||||||
|
C++ 一直在相容性上不遺餘力,舊的語法能跑,新的語法與關鍵字又一直疊加,最後呈現出來的就是複雜。作為一個長久以來的使用者,看著其他程式語言的崛起與更新,雖然在理性上很能夠理解,但是情感上還是會想掙扎一下,讓我們看看 Modern C++ 改變的那一面,拋棄到舊的習慣,讓我們可以在不失去效能的情況下,用「比較輕鬆」的方式來寫 C++。
|
||||||
|
|
||||||
|
## Modern C++ 的改變
|
||||||
|
|
||||||
|
### 1. 記憶體管理
|
||||||
|
|
||||||
|
**告別手動記憶體管理(`new`/`delete`)**
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 舊式 C/C++ 方式
|
||||||
|
BigBuffer* buffer = new BigBuffer(size);
|
||||||
|
// ... 使用 buffer
|
||||||
|
delete buffer; // 容易忘記,造成記憶體洩漏
|
||||||
|
|
||||||
|
// Modern C++ 方式
|
||||||
|
auto buffer = std::make_unique<BigBuffer>(size);
|
||||||
|
// 自動管理記憶體,無需手動 delete
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用 Smart Pointer**
|
||||||
|
|
||||||
|
- `std::unique_ptr`:獨佔所有權
|
||||||
|
- `std::shared_ptr`:共享所有權
|
||||||
|
- `std::weak_ptr`:弱引用,避免循環依賴
|
||||||
|
|
||||||
|
### 2. 用容器取代原生陣列
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 危險的 C 陣列
|
||||||
|
int arr[100]; // 無邊界檢查,容易越界
|
||||||
|
|
||||||
|
// 安全的 Modern C++ 容器
|
||||||
|
std::array<int, 100> arr; // 編譯時大小固定
|
||||||
|
std::vector<int> vec; // 動態大小
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 自動型別推導
|
||||||
|
|
||||||
|
C++ 11 開始支援 [`auto`](https://en.cppreference.com/w/cpp/keyword/auto.html) 關鍵字
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 舊的方式,冗長的型別宣告
|
||||||
|
std::vector<std::string>::iterator it = vec.begin();
|
||||||
|
|
||||||
|
// 簡潔的 auto
|
||||||
|
auto it = vec.begin();
|
||||||
|
|
||||||
|
// 結構化綁定(C++17)
|
||||||
|
auto [key, value] = map_pair;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 現代迴圈語法
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 傳統 C 風格迴圈
|
||||||
|
for (int i = 0; i < arr.size(); ++i) {
|
||||||
|
process(arr[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modern C++ 範圍迴圈
|
||||||
|
for (const auto& elem : arr) {
|
||||||
|
process(elem);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Lambda 表達式
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 傳統函式物件
|
||||||
|
struct Comparator {
|
||||||
|
bool operator()(int a, int b) const {
|
||||||
|
return a < b;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Modern C++ Lambda
|
||||||
|
auto compare = [](int a, int b) { return a < b; };
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modern C++ 的其他好東西
|
||||||
|
|
||||||
|
### 使用 `std::optional` 明確表達「可能為空」的情況
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::optional<int> findValue(const std::map<int, int>& mapping, int key) {
|
||||||
|
auto it = mapping.find(key);
|
||||||
|
if (it != mapping.end()) {
|
||||||
|
return it->second; // 找到了,返回值
|
||||||
|
}
|
||||||
|
return std::nullopt; // 沒找到,返回空值
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用前必須檢查
|
||||||
|
auto value = findValue(numbers, 42);
|
||||||
|
if (value.has_value()) {
|
||||||
|
std::cout << "Found value:" << value.value() << std::endl;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
像 Python 可以 `return None` 來表示空值,`std::nullopt` 也提供了一個表達空值的方式。
|
||||||
|
傳統的方式只能像是:
|
||||||
|
```cpp
|
||||||
|
bool findResult(const std::vector<int>& mapping, int target, int& result) {
|
||||||
|
for (const auto& value : mapping) {
|
||||||
|
if (value == target) {
|
||||||
|
result = value; // 找到了,設置結果
|
||||||
|
return true; // 返回成功
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false; // 沒找到,返回失敗
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `constexpr` 可以在編譯時計算
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
constexpr int factorial(int n) {
|
||||||
|
return (n <= 1) ? 1 : n * factorial(n - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在編譯時就能計算出結果
|
||||||
|
constexpr int fact5 = factorial(5); // 編譯時計算為 120
|
||||||
|
```
|
||||||
|
### 用 `using` 簡化程式
|
||||||
|
|
||||||
|
`using`(稱為型別別名,Type Alias)和 `typedef` 的功能基本上是一樣的:它們都是為現有的型別建立一個「別名」,而不是建立新的型別。
|
||||||
|
|
||||||
|
- **`using`:** `using [新名字] = [舊型別];`
|
||||||
|
- **`typedef`:** `typedef [舊型別] [新名字];` // 比較不直覺
|
||||||
|
|
||||||
|
`using` 有一個 `typedef` 做不到的是模板別名(Template Alias)。
|
||||||
|
|
||||||
|
例如
|
||||||
|
```cpp
|
||||||
|
template <typename T> using Handler = std::function<void(const T&)>;
|
||||||
|
Handler<int> intHandler;
|
||||||
|
```
|
||||||
|
|
||||||
|
用 `using` 來簡化 `std::function` 的例子。例如說我們的參數是一個 callback function
|
||||||
|
```cpp
|
||||||
|
void setOnProgress(std::function<void(double, size_t)> callback) {
|
||||||
|
progressCb = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用 using 簡化
|
||||||
|
using ProgressCallback = std::function<void(double percentage, size_t bytesSent)>;
|
||||||
|
|
||||||
|
void setOnProgress(ProgressCallback callback) {
|
||||||
|
progressCb = callback;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 表達力
|
||||||
|
|
||||||
|
Modern C++ 讓程式碼更接近自然語言:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto result = numbers
|
||||||
|
| std::views::filter([](int x) { return x % 2 == 0; }) // 挑偶數
|
||||||
|
| std::views::transform([](int x) { return x * x; }) // 轉平方
|
||||||
|
| std::ranges::to<std::vector>(); // 直接收進 vector
|
||||||
|
```
|
||||||
|
|
||||||
|
C++23 加入了 [`std::ranges::to`](https://en.cppreference.com/w/cpp/ranges/to.html)
|
||||||
|
更重要的是不用再寫一堆 Iterator。
|
||||||
|
|
||||||
|
### 效能
|
||||||
|
|
||||||
|
Modern C++ 的效能提升,精確來說是實踐了 **「零成本抽象(Zero-overhead abstraction)」**。
|
||||||
|
|
||||||
|
- **`constexpr`**:這真的很好用。能把計算直接塞進編譯階段,執行時完全不用花 CPU 時間。到了 C++20 更有 `consteval` 強制編譯期計算。
|
||||||
|
- **移動語意 (Move Semantics)**:這是 C++ 的續命符。它解決了以前回傳大型物件時,為了怕拷貝太慢得傳指標或寫得扭扭捏捏的問題。現在直接 `return` 沒負擔。
|
||||||
|
|
||||||
|
## 試試看吧,漸進式升級
|
||||||
|
|
||||||
|
一開始先試著用 `std::array` 或 `std::vector` 來取代傳統的 array。
|
||||||
|
然後用 smart pointer 來取代 `new/delete`。
|
||||||
|
過程中用 `auto` 來少打一些字。
|
||||||
|
用 `using` 來簡化落落長的型別。
|
||||||
|
用 Lambda 來簡化 callback(不用在遠遠的地方定義 function)。
|
||||||
|
|
||||||
|
試試看,當冗長的 code 變得清爽,思路也比較不容易打結。
|
||||||
|
|
||||||
|
## 最後
|
||||||
|
|
||||||
|
雖然 C++ 還是有很多可以改進的地方,例如 package 的使用跟 Python 相比就麻煩很多。
|
||||||
|
但還是有在改變,試試看吧。
|
||||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 209 KiB |
121
content/posts/2026/2026-01-16_Windows Git Bash 改用 Zsh/index.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
---
|
||||||
|
slug: Windows Git Bash 改用 Zsh
|
||||||
|
title: Windows Git Bash 改用 Zsh
|
||||||
|
description:
|
||||||
|
toc: true
|
||||||
|
authors:
|
||||||
|
- awin
|
||||||
|
tags:
|
||||||
|
- windows
|
||||||
|
- terminal
|
||||||
|
categories:
|
||||||
|
- System
|
||||||
|
series:
|
||||||
|
- Windows
|
||||||
|
date: 2026-01-16T00:00:00
|
||||||
|
lastmod: 2026-01-16T00:00:00
|
||||||
|
featuredVideo:
|
||||||
|
featuredImage:
|
||||||
|
draft: false
|
||||||
|
enableComment: true
|
||||||
|
---
|
||||||
|
[zsh](https://zh.wikipedia.org/zh-tw/Z_shell) 是一個很好用方便的 shell,在 Linux 安裝非常方便,在 Windows 上如果有安裝 Git,也可以透過 Git Bash 來使用,操作環境可以大幅貼近 Linux。
|
||||||
|
|
||||||
|
<!--more-->
|
||||||
|
|
||||||
|
## 0. 準備
|
||||||
|
|
||||||
|
- 要先裝 [Windows Terminal](https://apps.microsoft.com/detail/9n0dx20hk701?hl=zh-TW&gl=TW)
|
||||||
|
- 要安裝 [Git for Windows](https://git-scm.com/install/windows)
|
||||||
|
|
||||||
|
## 1. 安裝ZSH
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
到 [Msys2 Package: zsh](https://packages.msys2.org/packages/zsh?repo=msys&variant=x86_64)下載
|
||||||
|

|
||||||
|
|
||||||
|
將壓縮檔內容複製到 Git 安裝目錄(一般而言是 `C:\Program Files\Git` )
|
||||||
|
|
||||||
|
## 2. [安裝 oh-my-zsh](https://ohmyz.sh/)
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 修改 `~/.bashrc`
|
||||||
|
|
||||||
|
Windows 的 user home 路徑是 `C:\Users\<user_name>`,所以要修改此目錄下的 `.bashrc`,如果沒有這個檔案就自己新增一個。
|
||||||
|
|
||||||
|
然後在 `C:\Users\<user_name>\.bashrc` 的最後加入:
|
||||||
|
|
||||||
|
```
|
||||||
|
if [ -t 1 ]; then
|
||||||
|
exec zsh
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
這樣下次 bash 啟動的時候就會自動使用 zsh。
|
||||||
|
zsh 被啟動之後,我們的設定檔就變成了 `~/.zshrc` 而不是 `~/.bashrc` 了。
|
||||||
|
|
||||||
|
## 4. 安裝字型
|
||||||
|
|
||||||
|
等一下要使用的佈景主題會用很多字型裡的符號在UI上,所以一般正常的字型沒辦法顯示,只會看到方框裡面有一個問號。
|
||||||
|
所以我們要下載「修改過」的字型來顯示這些特殊符號。
|
||||||
|
|
||||||
|
到 [Nerd Font](https://www.nerdfonts.com/)下載你喜歡的字型。然後安裝到 Windows 裡面。這樣就可以了。
|
||||||
|
這裡假設你下載了 FiraCode Nerd Font。
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## 5. 設定 Windows Terminal 的字型
|
||||||
|
|
||||||
|
打開 Windows Terminal,按 `Ctrl+,` 進入設定畫面,左邊選 Git Bash ,然後在右邊選 "Appearance"
|
||||||
|

|
||||||
|
|
||||||
|
在 Font Face 那邊選擇 FiraCode Nerd Font。
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## 6. 安裝 [powerlevel10k](https://github.com/romkatv/powerlevel10k)
|
||||||
|
|
||||||
|
[powerlevel10k](https://github.com/romkatv/powerlevel10k) 這個佈景主題讓 zsh 變得很好看
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k
|
||||||
|
```
|
||||||
|
|
||||||
|
然後打開 `~/.zshrc` ,把佈景主題改成 powerlevel10k
|
||||||
|
找到 ` ZSH_THEME` 這一行,改成 `ZSH_THEME="powerlevel10k/powerlevel10k"`
|
||||||
|

|
||||||
|
|
||||||
|
然後重新打開 Windows Terminal,打開 Git Bash ,就會出現 powerlevel10k 的 config 流程,你可以在這裡設定自己喜歡的風格樣式。
|
||||||
|
如果字型設定正確的話,會看到正確的符號,否則就是方框裡面有一個問號。
|
||||||
|

|
||||||
|
|
||||||
|
## 7. 安裝 plugin
|
||||||
|
|
||||||
|
這步不一定要做,看需求。
|
||||||
|
|
||||||
|
### 安裝 [zsh-syntax-highlighting](https://github.com/zsh-users/zsh-syntax-highlighting)
|
||||||
|
|
||||||
|
這個 plugin 可以在 terminal 顯示 highlight syntax
|
||||||
|
安裝
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting
|
||||||
|
```
|
||||||
|
|
||||||
|
然後要修改 `~/.zshrc`,在 `plugins` 加入 `zsh-syntax-highlighting`
|
||||||
|

|
||||||
|
|
||||||
|
### 安裝 [zsh-autosuggestions](https://github.com/zsh-users/zsh-autosuggestions)
|
||||||
|
|
||||||
|
這個 plugin 可以在輸入的時候提供已經輸入過的歷史命令或是相似的命令,讓你節省一點時間。
|
||||||
|
安裝
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions
|
||||||
|
```
|
||||||
|
|
||||||
|
然後修改 `~/.zshrc`,在 `plugins` 加入 `zsh-autosuggestions` 。
|
||||||
|
After Width: | Height: | Size: 176 KiB |
@@ -0,0 +1,519 @@
|
|||||||
|
---
|
||||||
|
slug: 用 Caddy 做反向代理 + 用 Authentik 管理帳號(以 Gitea 為例)
|
||||||
|
title: 用 Caddy 做反向代理 + 用 Authentik 管理帳號(以 Gitea 為例)
|
||||||
|
description:
|
||||||
|
toc: true
|
||||||
|
authors:
|
||||||
|
- awin
|
||||||
|
tags:
|
||||||
|
- linux
|
||||||
|
- selfhosted
|
||||||
|
categories:
|
||||||
|
- System
|
||||||
|
series:
|
||||||
|
- Linux
|
||||||
|
date: 2026-02-01T00:00:00
|
||||||
|
lastmod: 2026-02-01T00:00:00
|
||||||
|
featuredVideo:
|
||||||
|
featuredImage:
|
||||||
|
draft: false
|
||||||
|
enableComment: true
|
||||||
|
---
|
||||||
|
|
||||||
|
本篇會帶你用 **Caddy** 當反向代理(Reverse Proxy)與自動 TLS,並用 **Authentik** 當身分提供者(IdP / SSO)來管理帳號;最後以 **Gitea** 當範例,完成「反向代理 + 單一登入(OIDC)」整合。
|
||||||
|
|
||||||
|
這張可愛的圖是 Gemini 生成的。
|
||||||
|
|
||||||
|
<!--more-->
|
||||||
|
|
||||||
|
本文參考文件:
|
||||||
|
|
||||||
|
- Caddy 文件:[https://caddyserver.com/docs/](https://caddyserver.com/docs/)
|
||||||
|
- Authentik 文件:[https://docs.goauthentik.io/](https://docs.goauthentik.io/)
|
||||||
|
- Authentik 整合文件:[https://integrations.goauthentik.io/](https://integrations.goauthentik.io/)
|
||||||
|
- Authentik × Gitea(以官方整合為主):[https://integrations.goauthentik.io/development/gitea/](https://integrations.goauthentik.io/development/gitea/)
|
||||||
|
|
||||||
|
範例網域:
|
||||||
|
|
||||||
|
- Authentik:`auth.www.myapp.example`
|
||||||
|
- Gitea:`git.www.myapp.example`
|
||||||
|
|
||||||
|
(以上都以 `www.myapp.example` 為「示意域名」。實作時請換成你自己的可解析網域,並確保 DNS A/AAAA 指向你的主機 IP。)
|
||||||
|
|
||||||
|
> 本文所有服務都以 Docker 方式部署,並以「分開三份 docker-compose」為主。為了降低複雜度,本文不使用共用 Docker network;Caddy 會透過 `host.docker.internal` 反向代理到宿主機上已 publish 的服務埠。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1) Caddy 的優點與功能
|
||||||
|
|
||||||
|
Caddy 是現代化的 Web Server / 反向代理,特別適合「自架服務入口」:
|
||||||
|
|
||||||
|
- **自動 HTTPS**:內建 ACME(例如 Let’s Encrypt),只要網域與 80/443 可用,Caddy 會自動申請與續期憑證。
|
||||||
|
- **設定檔簡潔**:Caddyfile 可讀性高,反向代理、gzip/zstd、header、redirect 等都很直覺。
|
||||||
|
- **反向代理體驗好**:`reverse_proxy` 直接把請求轉送到後端服務(HTTP/HTTPS/Unix socket 都可)。
|
||||||
|
- **可擴充**:模組化(插件)設計,常見需求如 forward auth、rate limit、WAF 等都有社群方案(是否採用依你需求而定)。
|
||||||
|
- **適合容器化**:用 Docker 跑 Caddy 非常普遍,資料(憑證、設定)用 volume 掛載即可。
|
||||||
|
|
||||||
|
在這篇架構裡,Caddy 的角色很單純:
|
||||||
|
|
||||||
|
- 對外只有一個入口(80/443)
|
||||||
|
- 負責 TLS、HTTP→HTTPS、與把流量分流到 `Authentik` 和 `Gitea`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2) Authentik 的優點與功能
|
||||||
|
|
||||||
|
Authentik 是一個自架的身分與存取管理(IAM)/ 單一登入(SSO)平台,你可以把它當成「你自己的 Google/Microsoft/Okta Login」。
|
||||||
|
|
||||||
|
常見價值:
|
||||||
|
|
||||||
|
- **統一帳號來源**:帳號、群組、權限集中管理,不用每個服務都自己養帳。
|
||||||
|
- **多種整合方式**:OIDC(OpenID Connect)、OAuth2、SAML、LDAP、Proxy/ForwardAuth 等。
|
||||||
|
- **安全能力**:MFA、條件式存取、登入流程(flow)可控。
|
||||||
|
- **自架友善**:Docker 部署成熟,社群整合文件多。
|
||||||
|
|
||||||
|
在這篇裡 Authentik 的角色是:
|
||||||
|
|
||||||
|
- 當 **OIDC Provider**(發行 token / 提供 userinfo)
|
||||||
|
- Gitea 透過 OIDC 把「登入」交給 Authentik
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3) 以 Gitea 為例:Caddy、Authentik、Gitea 的角色與登入流程
|
||||||
|
|
||||||
|
### 三方關係(誰做什麼)
|
||||||
|
|
||||||
|
- **Caddy(反向代理 / 門口)**
|
||||||
|
|
||||||
|
- 接收瀏覽器對 `auth.www.myapp.example` 與 `git.www.myapp.example` 的連線
|
||||||
|
- 依網域把請求轉送到對應容器
|
||||||
|
- **Authentik(身分提供者 IdP / SSO)**
|
||||||
|
|
||||||
|
- 管理使用者
|
||||||
|
- 提供 OIDC discovery / authorization / token / userinfo
|
||||||
|
- **Gitea(應用服務 / Relying Party)**
|
||||||
|
|
||||||
|
- 提供 Git service 與 Web UI
|
||||||
|
- 使用者點「用 Authentik 登入」→ 把驗證交給 Authentik
|
||||||
|
|
||||||
|
### 登入流程(OIDC 概念流程)
|
||||||
|
|
||||||
|
以使用者從 `git.www.myapp.example` 進入為例:
|
||||||
|
|
||||||
|
1. 使用者進入 Gitea,選擇「用 Authentik 登入」(OIDC/OAuth2)。
|
||||||
|
2. Gitea 將瀏覽器導向 Authentik 的授權端點(authorize),並帶上 `client_id`、`redirect_uri`、`scope`、`state` 等參數。
|
||||||
|
3. 使用者在 Authentik 完成登入(可能含 MFA)。
|
||||||
|
4. Authentik 將瀏覽器導回 Gitea 的 `redirect_uri`(callback),並附上 code。
|
||||||
|
5. Gitea 以 server-to-server 方式向 Authentik 的 token endpoint 換取 token。
|
||||||
|
6. Gitea 用 token 取得使用者資訊(或驗證 ID Token),建立/綁定本地帳號並完成登入。
|
||||||
|
|
||||||
|
重點:
|
||||||
|
|
||||||
|
- **Caddy 不負責「帳號驗證」**,它只是把流量導到 Authentik/Gitea。
|
||||||
|
- **Authentik 负责身份**,Gitea 負責「使用 Authentik 身份」來登入。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4) 實作:用 Docker 部署三套服務並完成整合
|
||||||
|
|
||||||
|
### 4.1 先準備:DNS 與防火牆
|
||||||
|
|
||||||
|
- DNS:
|
||||||
|
- `auth.www.myapp.example` → 你的主機 IP
|
||||||
|
- `git.www.myapp.example` → 你的主機 IP
|
||||||
|
- 對外開放:
|
||||||
|
- TCP 80、443(給 Caddy 申請/使用 TLS)
|
||||||
|
|
||||||
|
- 建議不要「對外網」放行(但會在主機上 publish 給 Caddy 用):
|
||||||
|
- Authentik 的 9000(本文用它讓 Caddy 反向代理)
|
||||||
|
- Authentik 的 9443(通常不需要;除非你自己另外 publish 來除錯)
|
||||||
|
- Gitea 的 8020(本文用它讓 Caddy 反向代理)
|
||||||
|
- 2244/22(SSH)是否對外開放依你需求(可先不開)
|
||||||
|
|
||||||
|
> 如果你用「Caddy 反向代理」模式,後端服務的 port 原則上不應對外網可達,避免繞過入口直接連到後端。
|
||||||
|
|
||||||
|
> 本文為了不使用共用 network,後端服務會用 `ports:` publish 到主機;因此「不要對外放行」指的是:不要在路由器/NAT 或雲端安全群組把這些埠開給 Internet。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5) Caddy:docker-compose 與 Caddyfile
|
||||||
|
|
||||||
|
### 5.1 Caddy 的 docker-compose.yml(範例)
|
||||||
|
|
||||||
|
檔案位置示意:`docker/caddy/docker-compose.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
caddy:
|
||||||
|
image: caddy:2.10.2
|
||||||
|
container_name: caddy
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
volumes:
|
||||||
|
- ./config/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- ./data/data:/data
|
||||||
|
- ./data/config:/config
|
||||||
|
```
|
||||||
|
|
||||||
|
說明:
|
||||||
|
|
||||||
|
- `host.docker.internal` 在 Docker Desktop(Windows/macOS)通常可用。
|
||||||
|
- 在 Linux/Ubuntu 上常見需要加 `extra_hosts: host.docker.internal:host-gateway`,才能讓容器用 `host.docker.internal` 連到宿主機。
|
||||||
|
|
||||||
|
### 5.2 Caddyfile(範例)
|
||||||
|
|
||||||
|
檔案位置示意:`docker/caddy/config/Caddyfile`
|
||||||
|
|
||||||
|
```caddyfile
|
||||||
|
{
|
||||||
|
# 可選:管理者信箱,用於 ACME
|
||||||
|
email admin@www.myapp.example
|
||||||
|
}
|
||||||
|
|
||||||
|
# Authentik(對外入口)
|
||||||
|
auth.www.myapp.example {
|
||||||
|
encode zstd gzip
|
||||||
|
# 反向代理到宿主機上 publish 的 Authentik 9000(HTTP)
|
||||||
|
reverse_proxy host.docker.internal:9000
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gitea(對外入口)
|
||||||
|
git.www.myapp.example {
|
||||||
|
encode zstd gzip
|
||||||
|
# 反向代理到宿主機上 publish 的 Gitea 8020
|
||||||
|
reverse_proxy host.docker.internal:8020
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
說明:
|
||||||
|
|
||||||
|
- 這份教學為了降低複雜度,讓 **TLS 只由 Caddy 對外負責**。
|
||||||
|
- 所以 Caddy 反向代理到 Authentik 的 `9000`(HTTP)即可,避免 `9443` 牽涉到後端 TLS 憑證驗證問題。
|
||||||
|
|
||||||
|
> 如果你堅持要反向代理到 Authentik 的 `9443`(HTTPS),才需要在 Caddy 端處理後端 TLS(例如自簽憑證可能需要 `tls_insecure_skip_verify`)。但這會讓設定更複雜,也比較不建議。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6) Authentik:docker-compose 與 .env
|
||||||
|
|
||||||
|
### 6.1 Authentik 的 .env(範例,請自行填值)
|
||||||
|
|
||||||
|
檔案位置示意:`docker/authentik/.env`
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
# Database
|
||||||
|
PG_PASS=請填入強密碼
|
||||||
|
PG_USER=authentik
|
||||||
|
PG_DB=authentik
|
||||||
|
|
||||||
|
# Authentik
|
||||||
|
AUTHENTIK_SECRET_KEY=請填入長度足夠的隨機字串
|
||||||
|
|
||||||
|
# 版本(可固定,避免 latest 帶來不可預期變更)
|
||||||
|
AUTHENTIK_TAG=2025.12.2
|
||||||
|
```
|
||||||
|
|
||||||
|
安全提醒:
|
||||||
|
|
||||||
|
- `.env` 不要上傳到公開 repo。
|
||||||
|
- `PG_PASS`、`AUTHENTIK_SECRET_KEY` 請使用密碼管理器產生。
|
||||||
|
|
||||||
|
### 6.2 Authentik 的 docker-compose.yml(範例)
|
||||||
|
|
||||||
|
檔案位置示意:`docker/authentik/docker-compose.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
postgresql:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
||||||
|
start_period: 20s
|
||||||
|
interval: 30s
|
||||||
|
retries: 5
|
||||||
|
timeout: 5s
|
||||||
|
volumes:
|
||||||
|
- ./data/postgresql_data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: ${PG_PASS:?database password required}
|
||||||
|
POSTGRES_USER: ${PG_USER:-authentik}
|
||||||
|
POSTGRES_DB: ${PG_DB:-authentik}
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
command: --save 60 1 --loglevel warning
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
|
||||||
|
start_period: 20s
|
||||||
|
interval: 30s
|
||||||
|
retries: 5
|
||||||
|
timeout: 3s
|
||||||
|
volumes:
|
||||||
|
- redis:/data
|
||||||
|
|
||||||
|
server:
|
||||||
|
image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG:-2025.12.2}
|
||||||
|
container_name: authentik-server
|
||||||
|
restart: unless-stopped
|
||||||
|
command: server
|
||||||
|
environment:
|
||||||
|
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
|
||||||
|
AUTHENTIK_REDIS__HOST: redis
|
||||||
|
AUTHENTIK_POSTGRESQL__HOST: postgresql
|
||||||
|
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
||||||
|
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
|
||||||
|
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||||
|
volumes:
|
||||||
|
- ./data/authentik/media:/media
|
||||||
|
- ./data/authentik/custom-templates:/templates
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
# 透過 ports publish 到宿主機,讓 Caddy 用 host.docker.internal 反向代理
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
depends_on:
|
||||||
|
postgresql:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
worker:
|
||||||
|
image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG:-2025.12.2}
|
||||||
|
container_name: authentik-worker
|
||||||
|
restart: unless-stopped
|
||||||
|
command: worker
|
||||||
|
environment:
|
||||||
|
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
|
||||||
|
AUTHENTIK_REDIS__HOST: redis
|
||||||
|
AUTHENTIK_POSTGRESQL__HOST: postgresql
|
||||||
|
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
||||||
|
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
|
||||||
|
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||||
|
user: root
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ./data/authentik/media:/media
|
||||||
|
- ./data/authentik/certs:/certs
|
||||||
|
- ./data/authentik/custom-templates:/templates
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
postgresql:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
redis:
|
||||||
|
driver: local
|
||||||
|
```
|
||||||
|
|
||||||
|
小提醒:
|
||||||
|
- 這個「不共用 network」的做法,後端服務需要透過 `ports:` publish 到宿主機,才能讓 Caddy 容器透過 `host.docker.internal` 連到它。
|
||||||
|
- 安全性上要靠「不要在路由器/NAT/雲端安全群組把 9000/8020 對外放行」,只放行 80/443 給 Caddy。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7) Gitea:docker-compose(範例)
|
||||||
|
|
||||||
|
檔案位置示意:`docker/gitea/docker-compose.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
gitea:
|
||||||
|
image: gitea/gitea:latest
|
||||||
|
container_name: gitea
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- USER_UID=1000
|
||||||
|
- USER_GID=1000
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
- /etc/timezone:/etc/timezone:ro
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
ports:
|
||||||
|
- "8020:3000"
|
||||||
|
- "2244:22" # SSH 是否對外開放依你需求;如果你要用 git@... 的方式推送才需要
|
||||||
|
```
|
||||||
|
|
||||||
|
反向代理補充(很重要,避免 OIDC callback 出現網址不一致):
|
||||||
|
|
||||||
|
- 請確認 Gitea 的「對外 Base URL」是 `https://git.www.myapp.example/`。
|
||||||
|
- 若 Base URL 設錯,常見症狀是:Gitea 顯示的 callback URL 會是 `http://...` 或 host/port 不對,導致 Authentik 報 `redirect_uri mismatch`。
|
||||||
|
- 設定位置依你的 Gitea 部署方式而不同(例如 `app.ini` 的 `ROOT_URL`,或在管理介面可調),原則是「以使用者實際從瀏覽器進入的網址」為準。
|
||||||
|
|
||||||
|
如果你要讓 `git.www.myapp.example` 對外提供 SSH(22)而不是用 2244,也可以改成 `"22:22"`,但請確認主機上沒有其他 SSH 服務衝突(例如 OpenSSH)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8) 啟動順序與驗證
|
||||||
|
|
||||||
|
分開三份 compose 時,建議啟動順序:
|
||||||
|
|
||||||
|
1) Authentik(含 DB/Redis)
|
||||||
|
2) Gitea
|
||||||
|
3) Caddy(最後再把入口打開)
|
||||||
|
|
||||||
|
範例指令(在各自資料夾):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
驗證:
|
||||||
|
|
||||||
|
- 先確認 Authentik 後台能打開:`https://auth.www.myapp.example/`
|
||||||
|
- 再確認 Gitea 能打開:`https://git.www.myapp.example/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9) Authentik × Gitea 整合(以官方文件流程為準)
|
||||||
|
|
||||||
|
本段建議你同時開著官方整合頁:[https://integrations.goauthentik.io/development/gitea/](https://integrations.goauthentik.io/development/gitea/)
|
||||||
|
|
||||||
|
整體思路:
|
||||||
|
|
||||||
|
1) 在 Authentik 建立一個「對 Gitea 用的 OIDC Provider」
|
||||||
|
2) 建立 Application,把 provider 掛上去
|
||||||
|
3) 把 Discovery URL / Client ID / Client Secret 填進 Gitea 的 Authentication Source
|
||||||
|
|
||||||
|
### 9.1 在 Authentik 建立 Provider + Application
|
||||||
|
|
||||||
|
在 Authentik 管理介面:
|
||||||
|
|
||||||
|
1. 建立 Provider:
|
||||||
|
|
||||||
|
- Providers → Create → **OAuth2/OpenID Provider**
|
||||||
|
- 重要欄位(名稱以示意為主):
|
||||||
|
- Name:`gitea-oidc`
|
||||||
|
- Client type:通常選 Confidential(Gitea 是 server-side)
|
||||||
|
- Redirect URIs:填入 Gitea 的 callback URL(下一小節會說怎麼確定)
|
||||||
|
- Scopes:至少包含 `openid`, `email`, `profile`
|
||||||
|
2. 建立 Application:
|
||||||
|
|
||||||
|
- Applications → Create
|
||||||
|
- Name:`Gitea`
|
||||||
|
- Slug:`gitea`
|
||||||
|
- Provider:選剛剛建立的 `gitea-oidc`
|
||||||
|
|
||||||
|
建立完成後,你會得到:
|
||||||
|
|
||||||
|
- Client ID
|
||||||
|
- Client Secret
|
||||||
|
-(通常也會有)OpenID Configuration / Discovery URL
|
||||||
|
|
||||||
|
建議使用「Discovery URL」方式整合,因為端點與簽章金鑰都能自動跟上。
|
||||||
|
|
||||||
|
Discovery URL(常見樣式,請以你的 Authentik 介面顯示為準):
|
||||||
|
|
||||||
|
- `https://auth.www.myapp.example/application/o/gitea/.well-known/openid-configuration`
|
||||||
|
|
||||||
|
### 9.2 在 Gitea 新增 Authentication Source(OIDC)
|
||||||
|
|
||||||
|
到 Gitea(需管理者權限):
|
||||||
|
|
||||||
|
1. Site Administration → Authentication Sources → Add Authentication Source
|
||||||
|
2. Type 選 OAuth2(或 OpenID Connect,依 Gitea 版本 UI)
|
||||||
|
3. Provider 選 OpenID Connect
|
||||||
|
4. 填入:
|
||||||
|
- Auto Discovery URL:`https://auth.www.myapp.example/application/o/gitea/.well-known/openid-configuration`
|
||||||
|
- Client ID:從 Authentik 複製
|
||||||
|
- Client Secret:從 Authentik 複製
|
||||||
|
- Scopes:`openid email profile`
|
||||||
|
|
||||||
|
建立後,Gitea 通常會顯示 callback URL 類似:
|
||||||
|
|
||||||
|
- `https://git.www.myapp.example/user/oauth2/<SOURCE_NAME>/callback`
|
||||||
|
|
||||||
|
把這個 callback URL 回填到 Authentik provider 的 Redirect URIs(兩邊要一致)。
|
||||||
|
|
||||||
|
### 9.3 測試登入
|
||||||
|
|
||||||
|
回到 Gitea 登入頁:
|
||||||
|
|
||||||
|
- 看到「用 Authentik 登入」的按鈕/選項
|
||||||
|
- 點下去會跳到 `auth.www.myapp.example` 完成登入
|
||||||
|
- 成功後回到 Gitea,首次登入可能會建立/連結本地使用者
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10) 常見踩雷整理(避免再踩一次)
|
||||||
|
|
||||||
|
你提供的 Perplexity 連結在這個環境下會遇到 403(無法直接讀取頁面內容),所以我沒辦法逐字引用你當時的對話;但我可以把「你目前設定中已經出現/很常遇到」的坑整理成一章,讓你下次少繞路。
|
||||||
|
|
||||||
|
### 10.1 `host.docker.internal` 在 Linux/Ubuntu 不一定可用
|
||||||
|
|
||||||
|
- 在 Docker Desktop(Windows/macOS)通常可以用 `host.docker.internal`。
|
||||||
|
- 在 Linux 常見情況是 **預設沒有** 這個 DNS 名稱。
|
||||||
|
|
||||||
|
可行解法(本文採用):在 Caddy container 加上:
|
||||||
|
|
||||||
|
- `extra_hosts: ["host.docker.internal:host-gateway"]`
|
||||||
|
|
||||||
|
### 10.2 反向代理到 Authentik 的 9443 可能遇到 TLS 驗證問題
|
||||||
|
|
||||||
|
如果你用:
|
||||||
|
|
||||||
|
```caddyfile
|
||||||
|
reverse_proxy https://...:9443
|
||||||
|
```
|
||||||
|
|
||||||
|
而 Authentik 端使用自簽憑證或內部憑證,Caddy 會因為無法驗證而拒絕。
|
||||||
|
|
||||||
|
解法:
|
||||||
|
|
||||||
|
- 最簡單:反向代理到 `host.docker.internal:9000`(HTTP),把 TLS 全交給 Caddy 對外處理。
|
||||||
|
- 若必須反向代理到 `:9443`:才處理後端 TLS(可能需要 `tls_insecure_skip_verify`),但要理解它是「跳過驗證」。
|
||||||
|
|
||||||
|
### 10.3 後端服務不要再對外暴露 port(避免繞過 Caddy)
|
||||||
|
|
||||||
|
在本文「不共用 network」的簡化架構裡,Authentik/Gitea 必須 publish port 到宿主機,Caddy 才連得到。
|
||||||
|
|
||||||
|
建議的安全作法是:
|
||||||
|
|
||||||
|
- **路由器/NAT**:只轉發 80/443 到這台機器
|
||||||
|
- **雲端安全群組(Security Group)**:只開 80/443 inbound
|
||||||
|
- 讓 9000/8020 只留在「主機本地用途」(給 Caddy 反向代理),不要對外網暴露
|
||||||
|
|
||||||
|
### 10.4 OIDC 回呼網址(Redirect URI)最容易填錯
|
||||||
|
|
||||||
|
症狀通常是:
|
||||||
|
|
||||||
|
- 登入後跳轉回來直接錯誤(redirect_uri mismatch / invalid redirect)
|
||||||
|
|
||||||
|
原則:
|
||||||
|
|
||||||
|
- 以 **Gitea 顯示的 callback URL** 為準
|
||||||
|
- 100% 原樣貼到 Authentik Provider 的 Redirect URIs
|
||||||
|
- domain、path、https 都要一致
|
||||||
|
|
||||||
|
### 10.5 反向代理後網址不對(http/host/port 亂跳)
|
||||||
|
|
||||||
|
如果你發現登入流程中 URL 變成 `http://`、或 host/port 被改掉,通常是「應用程式不知道自己在反向代理後面」造成的。
|
||||||
|
|
||||||
|
- Gitea:先回頭檢查 Base URL/`ROOT_URL` 是否為 `https://git.www.myapp.example/`。
|
||||||
|
- Authentik:確保你是透過 `https://auth.www.myapp.example/` 存取管理介面,並檢查是否有「反向代理/受信任代理(trusted proxy)」或「外部 URL(external host/url)」相關設定需要調整。
|
||||||
|
|
||||||
|
> 這一段在不同 Authentik 版本/部署方式名稱會不一樣,遇到時建議直接依官方文件用關鍵字搜尋:`reverse proxy`、`trusted proxy`、`X-Forwarded-Proto`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11) 小結
|
||||||
|
|
||||||
|
Authentik 解決了自己架站一個站就要建一次帳號的問題。
|
||||||
|
|
||||||
|
1. **Caddy**:負責自動化 TLS 與反向代理,解決了惱人的憑證問題,並統一了對外入口(80/443)。
|
||||||
|
2. **Authentik**:作為核心的身分驗證中心,未來若要新增其他服務(如 Portainer、Grafana、Nextcloud 等),只要它們支援 OIDC/OAuth2 或 ForwardAuth,都能輕鬆接入,實現真正的單一登入(SSO)。
|
||||||
|
3. **Gitea**:成功展示了從「各服務獨立帳號」轉向「集中式帳號管理」的整合流程。
|
||||||
|
|
||||||
|
雖然初期在理解 OIDC 流程與反向代理設定上需要花點時間,但建立起這套基礎設施後,未來的維護與擴充將會變得非常輕鬆且安全。希望這篇文章能幫助你順利搭建出自己的 Authentik 單一登入環境!
|
||||||
|
|
||||||
|
希望這篇文章有幫到你,謝謝你的閱讀。
|
||||||