常用 docker 指令

基本資訊

1
2
3
4
5
6
7
8
9
# 顯示 docker 的資訊
docker info

# 查看版本
docker -v
docker-compose -v

# 查看 container 佔用資源狀態
docker stats

小範例

假設本機裝好了 docker,但是沒有下載任何的 image,讓我們來跑幾個指令看看如何下載 image,以及啟動容器

範例 1

1
2
3
4
5
# 從 docker hub 下載 hello-word image
docker pull hello-world

# 啟動 hello world cotainer
docker run hello-world

範例 2

1
2
3
4
5
6
7
8
9
10
11
# 這個指令先檢查本機有沒有 ubuntu image,沒有的話會先 pull 再 run
# -i, --interactive (互動模式)
# -t, --tty (配置一個終端機)
docker run -it ubuntu bash

docker run ubuntu /bin/echo "Hi, Ubuntu"

# 也可以一開始在背景執行,後面再用 exec 指令進入環境
docker run -d ubuntu

docker exec -it CONTAINER_ID bash

Image 相關

build image

1
2
3
4
# 修改 docker-compose.yml 後 rebuild 服務,使用 --no-cache 不使用快取
docker-compose build SERVICE_NAME

docker-compose build --no-cache workspace

查看 image

1
2
3
4
5
# 查看機器上有的 image
docker images

# 查看 image 的詳細訊息,會返回一個 json 格式
docker inspect IMAGE_ID

刪除 image
注意:必須先刪除有使用這個 image 的 container 才能刪除 image

1
2
3
4
5
6
7
8
9
10
11
# 刪除指定 image,
docker rmi IMAGE_ID

# 找到無用的 images
docker images --filter "dangling=true"

# 刪除無用的 images
docker rmi $(docker images -f "dangling=true" -q)

# 刪除名字出現特定字串的 image
docker rmi $(docker images |grep 'k8s')

Container 相關

查看 container

1
2
3
4
5
6
7
8
9
10
11
12
# 查看運行中的 container
docker ps

# 查看所有 container, 包含沒有啟動的(STATUS=exited)
docker ps -a

# 查看停止的 container
docker ps -f "status=exited"

# 查看 container 的詳細訊息,會返回一個 json 格式
docker inspect CONTAINER_ID
docker inspect CONTAINER_NAME

啟動或停止 container

1
2
3
4
5
6
7
8
9
10
11
# 啟動 container
docker start CONTAINER_ID

# 重啟 container,如果 container 本身是 stop 狀態,效果同 start
docker restart CONTAINER_ID

# 停止 container
docker stop CONTAINER_ID

# 停止在運作的所有 container
docker stop $(docker ps -a -q)

或是也可以用 docker-compose 一次啟動多個 container

1
2
3
# 使用 docker-compose 啟動服務,-d 為在背景執行
docker-compose up -d apache2 mysql
docker-compose up nginx mysql redis

1
2
3
4
5
# 進入 container
docker-compose exec --use=root apache2 bash
docker-compose exec --use=laradock workspace bash
docker-compose exec --user=root mysql bash
docker-compose exec --user=root redis bash

刪除 container

1
2
3
4
5
# 刪除指定的 container
docker rm CONTAINER_ID

# 刪除所有 container
docker rm $(docker ps -a -q)

刪除所有 image & container

1
docker-compose down

log 相關

1
2
3
4
5
6
7
8
9
10
11
# 查看某個服務的 log 
docker-compose logs SERVICE_NAME

# 查看 nginx 的即時 log。好用,尤其是在啟動服務時使用了 -d 背景執行,看不到服務的執行狀況時
docker-compose logs -f nginx

# 看倒數 5 行的 log
docker-compose logs --tail 5 nginx

# 也可以用 docker 指令看倒數 5 分鐘的 log,但對象要是 container id
docker logs --since 5m CONTAINER_ID

備份和還原 mysql container 的資料

1
2
3
4
5
# Backup,以 mysqldump 把資料庫備份出來,檔案會存在當前目錄
docker exec CONTAINERI_ID /usr/bin/mysqldump -u root --password=root DATABASE_NAME > backup.sql

# Restore
cat backup.sql | docker exec -i CONTAINER_ID /usr/bin/mysql -u root --password=root DATABASE_NAME

Reference

Use the Docker command line

【Day 3】 - Docker 基本指令操作

如何快速刪除狀態 DANGLING 的特定 DOCKER IMAGES

Backup and restore a mysql database from a running Docker mysql container

常用的 MySQL 指令

查看 MySQL 帳號的權限

1
2
3
4
5
6
7
8
9
10
11
12
# 連線
mysql -u root -p

# 列出所有 user
SELECT user,host FROM mysql.user;

# 列出目前 user 的權限
SHOW GRANTS;

# 列出指定 user 的權限
SHOW GRANTS FOR 'user'@'host';
SHOW GRANTS FOR 'root'@'localhost';

帳號權限的設定

基本格式:

1
GRANT SELECT,INSERT,UPDATE,DELETE ON DB_NAME.TABLE_NAME TO 'user_name'@'host' IDENTIFIED BY 'password';

host 可以是 ‘localhost’ 或是 ‘%’ 表示所有 ip

看一些範例:

1
2
3
4
5
6
7
8
9
10
11
# 設定 root 可以訪問資料庫 test 的權限,密碼為空
GRANT ALL PRIVILEGES ON test.* TO 'root'@'%';

# 設定 root 訪問全部資料庫的權限,* 表示所有資料庫
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%';

# 設定使用者 tracy 訪問所有資料庫的權限
GRANT ALL PRIVILEGES ON *.* TO 'tracy'@'localhost' IDENTIFIED BY 'mypwd';

# 指定 ip 的訪問權限
GRANT ALL PRIVILEGES ON *.* TO 'tracy'@'10.2.1.11' IDENTIFIED BY 'mypwd';

改完權限,別忘了更新授權

1
FLUSH PRIVILEGES;

移除 MySQL 帳號權限

1
2
REVOKE ALL PRIVILEGES ON *.* FROM 'tracy'@'localhost';
FLUSH PRIVILEGES;

刪除 user

1
DROP USER 'testuser'@'localhost';

權限列表

僅列幾個權限,詳細可參考 官網:6.2.2 Privileges Provided by MySQL

權限 說明
GRANT OPTION 可把本帳號的許可權授予其它使用者
LOCK TABLES 鎖定指定資料表
SHOW DATABASES 可執行 SHOW DATABASES 指令
CREATE 建立資料庫和資料表
INDEX 建立或刪除索引
ALL 所有許可權,但不包括 GRANT
USAGE 無許可權許可權

遠端連線 MySQL

在 MySQL 設定裡註解掉 bind-address。

設定檔的位置 /etc/mysql/my.cnf 或是 /etc/mysql/mysql.conf.d

1
# bind-address = 127.0.0.1

Reference

Connect to a MySQL database remotely

無法遠端連接MySQL:message from server: “Host xxx is not allowed to connect to this MySQL server”

查詢 MySQL 對 此帳號 開放(GRANT)哪些權限

On-board Kit

homebrew

安裝

iTerm2

安裝

zsh

超簡單!十分鐘打造漂亮又好用的 zsh command line 環境 (包含 iTerm2 安裝)

Git

安裝

Git setting

常用 git 指令

ssh key

The Ultimate Guide to SSH - Setting Up SSH Keys

How to setup SSH config :使用 SSH 設定檔簡化指令與連線網址

如何用config管理多個網站的ssh key和如何不用每一組輸入ssh的Pass Phrase

PhpStorm

安裝設定:

簡潔高效的 PHP & Laravel 工作術:從 elementary OS 下手的聰明改造提案 #22

PhpStorm terminal 亂碼

PHPStorm 快捷键大全(Win/Linux/Mac)

Deployment 設定
SFTP 檔案自動同步

Docker

Docker for Mac 安裝

不需要 sudo 來執行 Docker 指令

常用 docker 指令

Laradock

安裝

.env 的設定

程式碼路徑的設定

1
2
3
4
5
專案程式碼的 local path
APP_CODE_PATH_HOST=../larabbs

container 裡的 code path
APP_CODE_PATH_CONTAINER=/var/www

有關 document root 的設定:

  • apache 預設為 /var/www,像是這樣 APACHE_DOCUMENT_ROOT=/var/www。若是 Laravel 專案就要改成 APACHE_DOCUMENT_ROOT=/var/www/public
  • nginx 預設為 /var/www/public

詳細可以見

  • laradock/apache2/sites/default.apache.conf
  • laradock/nginx/sites/default.conf

若是要在 nginx 同時架兩個站

要各別設定 nginx 的設定檔,包含 server_name 和 root。像是下面這樣:

/nginx/sites/site1.conf

1
2
3
4
listen 80;
listen [::]:80;
server_name site1;
root /var/www/site1/public;

/nginx/sites/site2.conf

1
2
3
4
listen 80;
listen [::]:80;
server_name site2;
root /var/www/site1/public;

如果有遇到 duplicate default server error,要把 default server 的字樣拿掉,詳細可以參考這裡 nginx- duplicate default server error

有關多站點設定可以看這 Laradock 最佳实践

更多教學

Docker – Laradock – 快速安裝筆記

安裝Laradock

資料庫連線工具

SequelPro 安裝 (MySQL 8.0 連線好像有 bug)

Workbench 安裝

常用的 MySQL 指令

xdebug

使用 Laradock 和 PhpStorm 設定 xdebug

Postman

安裝

使用 Laradock 和 PhpStorm 設定 xdebug

前言

  • 環境: mac
  • 先安裝好 PhpStorm
  • 下載 Laradock 專案

安裝 xdebug

設定 laradock/.env

1
2
WORKSPACE_INSTALL_XDEBUG=true
PHP_FPM_INSTALL_XDEBUG=true

設定 xdebug 環境參數

設定 laradock/workspace/xdebug.ini & laradock/php-fpm/xdebug.ini

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xdebug.remote_host=docker.for.mac.localhost
xdebug.remote_connect_back=0
xdebug.remote_port=9001
xdebug.idekey=PHPSTORM

xdebug.remote_autostart=1
xdebug.remote_enable=1
xdebug.cli_color=0
xdebug.profiler_enable=0
xdebug.profiler_output_dir="~/xdebug/phpstorm/tmp/profiling"

xdebug.remote_handler=dbgp
xdebug.remote_mode=req

xdebug.var_display_max_children=-1
xdebug.var_display_max_data=-1
xdebug.var_display_max_depth=-1

rebuild image

1
docker-compose build --no-cache php-fpm workspace

檢查 xdebug 是否正確安裝

使用 phpinfo() 看看 xdebug 是否開啟了,KEY 是否正確。

設定 PhpStorm

Preferences -> Languages & Frameworks -> PHP -> Debug

設定 Debug

設定 DBGp Proxy

設定 Servers

新增一個 server 設定,server name 需和 laradock 中 .env 的 PHP_IDE_CONFIG 設定一致,預設為 laradock

設定 remote debgger

點選視窗上方的 ADD CONFIGURATIONS 來新增一個 debugger

選擇 Templates -> PHP Remote debug,然後照下圖設定

點選 Validate debugger configuration on the Web Server 會出現下圖,然後照著設定
注意: 如果是 laravel 專案, Path to create validation script 路徑結尾要設到 public

設定完後按 VALIDATE,如果沒有什麼錯誤訊息,應該就是可以用了。

幾個遇過的坑點

基本上上面的設定都是測試可用的作法,下面算是曾經遇過的 issue,有遇到再看就好了。

port 的設定

php-fpm 的 port 為 9000,xdebug port 就不能設定為 9000 , 所以改設 9001,
參考:DEBUGGING PHP (WEB AND CLI) WITH XDEBUG USING DOCKER AND PHPSTORM

remote_connect_back 的設定

這個設定要改成 0,就是要關掉啦。

官網有說明這個參數的用途,基本上就是多人開發時使用:

There is also a xdebug.remote_connect_back setting that can be used if your development server is shared with multiple developers.

If enabled, the xdebug.remote_host setting is ignored and Xdebug will try to connect to the client that made the HTTP request. It checks the $_SERVER[‘HTTP_X_FORWARDED_FOR’] and $_SERVER[‘REMOTE_ADDR’] variables to find out which IP address to use.

更多訊息可以看官網說明

更多介紹可以看PHP调试工具-Xdebug

composer 超慢

image build 完重啟後,composer install 一直無反應 ,連 composer -v 也是如此,以為是 container 有問題,所以反覆刪掉 container 和 image,反覆重 build 重啟…。

直到放了一段很長時間後,composer 才開始跑,並且在跑之前有這樣的訊息:

1
1571381068.831664: There was a problem sending 289 bytes on socket 4: Broken pipe

經過超多次測試,確定是開了 debug 造成 composer 超慢,也嘗試了超多種設定,目前還沒有找到一個最佳解。
目前暫時的做法是:

  1. docker 的 xdebug 設定開啟
  2. 然後 container 啟動後以 root 權限進入 workspace, 註解掉 /etc/php/7.2/cli/con.d/20-xdebug.ini 裡面的設定,
    這樣可以暫時讓兩個服務都能使用…

UPDATE: 20200708 再次測試,已經沒有這個問題。

Reference

Laradock 使用 PhpStorm Debug 代码

使用 Xdebug 在 PHPStorm 中调试 PHP 程序(框架/原生均适用)

CGI、FastCGI 和 PHP-FPM

早期的網頁只是按照客戶端請求將保存在 web server 中的靜態資源回傳(例如圖片、CSS、HTML),這種情況下客戶端每次獲取的訊息都是同樣的內容,而現今僅僅通過靜態資源已經無法滿足客戶的需求,所以引入 CGI 以便客戶端請求能夠觸發 web server 運行另一個外部程式,連同客戶端所輸入的參數一起傳給這個外部程式,該程式會將動態生成的 HTML 和其他資訊通過 web server 再返回給客戶端(即動態請求,例如基於 PHP、Python、Java 實現的應用)。利用 CGI 可以針對用戶請求動態返回給客戶端各種各樣動態變化的訊息。

以 PHP 來說,PHP 的運作模式分成幾種:

  • CLI命令行模式
  • Module 模式
  • CGI 模式
  • Fast-CGI 模式

這篇只先討論下面兩種模式,有興趣暸解 Module 模式的可以看 PHP 的 Web 運行原理 ( 1 ) - 傳統型

CGI

CGI(Common Gateway Interface),為 web server 與 CGI 程式(如 php, python)間進行“交談”的一種工具或協議。CGI 程式可以用任何一種語言編寫,只要這種語言具有標準輸入、輸出和環境變量,如 php、perl、tcl 等。

根據使用者的請求,web server 會傳一些資訊給 PHP 解析器,例如 URL、查詢字串、POST 內容、HTTP header。CGI 就是規定要傳哪些資訊,以什麼樣的格式傳遞給後方處理這個請求的協議。web 服務器收到用戶請求,就會把請求提交給 CGI 程式(如php-cgi),CGI 程式根據請求的參數來解析程式,然後輸出標準的 html 語句,返回給 web 服務器, web 服務器再返回給客戶端,這就是 CGI 的工作流程,可以參考下圖。

common-gateway-interface-working
圖片來源

CGI program 針對每個 HTTP 請求都會 fork 一個新的 process 來做事,例如解析配置文件、初始化執行環境、處理請求等,然後把這個 process 處理完的結果通過 web server 轉發給用戶,剛剛 fork 出的 process 也隨之退出,如果下次用戶再請求動態資源,那麼 web server 又再次 fork process,如此周而復始,過程大概如下。

CGI program 的工作流程:

  1. 初始化各種相關變量
  2. 調用並初始化 zend 虛擬機
  3. 載入並解析 php.ini
    • 啟動 zend, zend 載入 php 腳本,做語法分析
    • 編譯 php 腳本成 opcode
    • 輸出結果
    • 關閉虛擬機
  4. 回傳結果給 web server

傳統 CGI 主要缺點是效能很差,因為每一次 web 請求都會有啟動和退出的過程,也就是 fork-and-execute 模式。這個模式幾乎無法處理高併發的請求,因此就誕生了 FastCGI。此外,傳統的 CGI 安全性也很差,現在已經很少被使用了。

FastCGI

FastCGI 是 CGI 的升級版本,為了提升 CGI 的效能而生,也是種協議。與 CGI 的 fork-and-execute 模式不同,FastCGI 則會先 fork 一個 master process,解析配置文件,初始化執行環境,然後再 fork 多個 worker process,當請求過來時 master process 會將請求分配給一個 worker process,然後立即可以接受下一個請求,提高了處理請求的效率,而且當 worker process 不夠用時,master process 還可以根據配置預先啟動幾個 worker process 等著,當閒置的 worker process 太多時,也會關掉一些,這樣不僅提高了效能,也節省了系統資源。另外常佇運行的 master process 同時也避免了重複的初始化操作。

FastCGI 採用 client-server 結構,可以將 web server 和腳本解析服務器分開,同時在腳本解析服務器上啟動一個或者多個腳本解析 process。當 web server 每次遇到動態程序時,可以將其直接交付給 FastCGI process 來執行,然後將得到的結果返回給瀏覽器。這種方式可以讓 web server 專注處理靜態請求,或者將動態腳本服務器的結果返回給客戶端,提高了整個應用系統的性能。

PHP-FPM

PHP-FPM (php-Fastcgi Process Manager) 是對於 FastCGI 協議的具體實現。

FastCGI 只是一個協議規範,需要每個語言具體去實現,PHP-FPM 就是 PHP 版本的 FastCGI 協議的具體實作,目的為實現 web server 與 PHP 腳本之間的溝通。

PHP-FPM 負責管理一個 process pool 來處理來自 web server 的 HTTP 動態請求,在 PHP-FPM 中,master process 負責與 Web server 進行通訊,接收 HTTP 請求,再將請求轉發給 worker process 進行處理,worker process 主要負責動態執行 PHP 程式碼,處理完成後,將處理結果返回給 web 服務器,再由 web 服務器將結果發送給客戶端。這就是PHP-FPM 的基本工作原理,概念可以看下圖:

總結

  • CGI: 介於 HTTP server 與 CGI program 溝通的工具、協定。
  • FastCGI: CGI 的改版,其 process 會持續運行,並支援分佈式運行。

  • PHP-CGI: 一種實作 FastCGI 的 process manager

  • PHP-FPM: 另一種實作 FastCGI 的 process manager,原本是 PHP 的補丁,在 PHP 5.3.2 被官方加入 PHP。

最後附上幾篇文章討論關於 PHP-FPM 的優化建議:

PHP Process Tuning

php-fpm详解 (這篇有討論關於 502 Bad Gateway & 504 Gateway Time-out 的優化建議)

Reference

通用閘道器介面

IT 考古: 什麼是 CGI?

CGI、FastCGI和PHP-FPM关系图解

What is Common Gateway Interface : Working and Its Applications

PHP-FPM是什么?

FastCGI Process Manager (FPM)

Differences between CGI, FastCGI, PHP-CGI, PHP-FPM, Spawn-FCGI

PHP 的 Web 運行原理 ( 1 ) - 傳統型

CGI、FastCGI、PHP-CGI與PHP-FPM

php-fpm與fastcgi、php-cgi之間的關係及源碼解析

【PHP核心】PHP的執行模式

CSRF 介紹

CSRF 是什麼?

CSRF(Cross Site Request Forgery) 為跨站請求偽造。簡單講,被害者點擊了攻擊者提供的網頁後,網頁裡面埋了一些惡意的程式碼,
目的為假冒被害者的身份去對目標網站發出請求,請求可以是刪除資源或是轉帳等動作,通常是發生在目標網站使用 cookie 做身份認證的情況,利用 cookie 會隨著請求一起被送出的特性來達到攻擊的效果。

解法

  • SameSite cookie
  • Referer
  • CSRF Token
  • Double Submit Cookie

此做法是在 server 設定 cookie 時,加上 SameSite 設定,表示只有在同網站的情況下瀏覽器才隨請求發送 cookie 給 server。

1
Set-Cookie: session_id=zxcvbn19rtyu89; SameSite

但是有幾個缺點:

  • 不是所有瀏覽器版本都支援,使用前須先確認
  • 對於如果是從 Google 搜尋頁或是別人分享的連結來點擊進入網頁,會無法保持登入的效果,對使用者體驗不利

解法有兩種:

  • 設定兩種類型的 cookie
    把 cookie 分成兩種,一種是維持登入狀態用的 cookie,不設為 SameSite,讓點擊連結進入網站時仍然對 server 發送 cookie,讓使用者保持登入狀態;
    另一種是敏感操作時候用的 cookie,要設為 SameSite,避免偽造請求的攻擊。
1
2
Set-Cookie: session_id=zxcvbn19rtyu89 
Set-Cookie: name=secret; SameSite
  • 使用 Lax & Strict
    先簡單介紹一下 SameSite 的幾種設定:Strict、Lax、None。

Strict:要求最嚴格,只有 same site 的情況下,才會送出 cookie。
Lax:比 Strict 寬鬆一點,除了 GET 請求以外,不發送 cookie 到第三方。
None: 允許所有網站對 cookie 的存取。

所以我們可以使用 SameSite=Lax,讓 POST、PUT、DELETE 請求在跨域的情況下不發送 cookie,避免 CSRF 攻擊,又可以在透過點擊連結進入網站時保持登入狀態。

1
Set-Cookie: session_id=zxcvbn19rtyu89; SameSite=Lax

詳細的細節可以看看 RFC 對於 SameSite cookie 的規範

Referer

檢查 request header 的 referer,看 domian 是否合法來避掉偽造請求。
但是有風險:

  • 瀏覽器可能不支援
  • 使用者也可以關閉 referer 的傳送,這樣就會遺失真正的請求

如果對 referer 不太暸解可以看HTTP Referer 教程

CSRF Token

最常見的解法,許多框架也直接支援這個做法。

手法為在 session 和 form 裡面直接埋入一個 CSRF token,然後 server 收到請求後比對是否相同,偽造請求因為不會有這個 CSRF token,所以可以避免造假攻擊,但是缺點為 session 也要存一份,無法 stateless。

Double Submit Cookie 是 CSRF token 的變形,一樣是要產出一組 CSRF token,但是改存在 cookie 裡面,作法也分兩種:

  • 由 server 產生 token:
    server 產生 token 後,不存在 session,改存在 cookie 裡,然後 form 也一樣埋入這個 token,等 server 收到請求後一樣把兩者做比對,因為偽造攻擊的網頁裡面的 form ㄧ定沒有這個 token,所以比對後會發現是非法的請求。

  • 由 client 產生 token:
    client 產生 token 後,同時設定在 cookie 裡和 header 上,等 server 收到請求後一樣把兩者做比對,概念是因為攻擊者無法偽造出目標網站的 cookie 。

Reference

讓我們來談談 CSRF

wiki:跨站請求偽造

JWT 到底要存哪裡?

前言

之前討論過關於 JWT 的概念跟使用方法,但是關於 JWT 的儲存方式只講了一般的用法,其實這一部分在網路上眾說紛紜,今天就來聊聊。

存在 web storage

上一篇講到 JWT 是以 header 的方式傳給 server,這個是指把 JWT 存在瀏覽器的 web storage(local storage 或 session storage)上。
這種做法需要注意的幾件事:

  • 不會有 CSRF 危險
  • 可能會有 XSS 攻擊的疑慮,因為 JWT 可以透過 js 來取得。

也有人把 JWT 存在 cookie 裡,存在 cookie 的話可能要注意的幾件事:

  • 需要注意 CSRF 的風險,但是現代前端框架已經開始支援保護 cookie。
  • 為了防止 XSS 攻擊,要設定 HttpOnly ,防止由 javascript 取得 cookie,詳見MDN:HTTP cookies。
  • 如果有跨域需求,要在 header 加上 domain 資訊,但有可能有瀏覽器版本不支援的問題。

結論

關於 JWT 要存在哪到現在還是各有各的說法,沒有一個定論,我想無論如何實作,知道分別的優缺點以及針對需要注意的資安風險做出對應的保護是必須的。

Reference

Where to Store your JWTs – Cookies vs HTML5 Web Storage

MDN:Http Cookies

淺談 JWT (Json Web Token)

前言

當服務開始龐大,server 開始 scale out,client 端每次的請求可能會交由不同的 server 處理,傳統的 session 模式必須解決 session consistency 的問題,常見的用法可以使用 Memcached 或是 Redis server 來管理。而 JWT 則提供了另一種身份認證的解決方案,有別於 session 具狀態性(Stateful)的形式,jwt 是無狀態的(Stateless),也就是不用考慮今天請求被分配到哪一台 server,根據隨著請求而來的 token ,server 就可以知道 client 是誰。

client 使用帳號密碼登入或是第三方登入,經 server 認證其身份,驗證成功後回傳一組 JWT 給 client,之後 client 在發起請求時都應夾帶這組 JWT 給 server 核查身份。過程中 server 都不用向資料庫發出查詢,也不用額外紀錄或是開啟 session,並且任何 server 拿到 token 都可以辨別其身份,過程如下圖:

JWT 驗證流程

JWT 的組成分成3個部份:header、payload、signature,結構如下:

1
Header.Payload.Signature

header 夾帶兩個資訊,一個是類型,這裡就是指 JWT,第二個為使用的演算法,如 HMAC、SHA256、RSA。

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

然後將它用 base64 編碼,此為 JWT 的第一部份。

Payload

payload 用來存放需要傳遞的資料,RFC7519 列有7個官方推薦的參數,但是這些為非必要項目,沒有也沒關係。

變量名 英文全寫 備註
iss Issuer JWT 的核發者
sub Subject JWT 的主體或者用戶
aud Audience 接收 JWT 的用戶
exp Expiration Time 過期時間(單位為秒)
nbf Not Before 開始時間(單位為秒),在該時間之前無效
iat Issued At 發佈時間(單位為秒)
jti JWT ID JWT唯一標識,區分不同發布者的統一的標識

除此之外,也可以自己定義要夾帶的其他資訊,例如:

1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

再來,一樣也是把 payload 用 base64 編碼,此為 JWT 的第二個部份。

Signature

第三個來做簽章(signature),簽章是 JWT 的核心,是為了避免有人更改 payload ,偽造身份發出請求,除非 secret 外洩,否則偽造者無法做出合法的簽章。作法是使用剛剛 base64 後的 header 以及 base64 後的 payload 和一個只有 server 才知道的 secret ,使用的演算法就是 header 裡面指定的演算法,作法如下:

1
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

最後把 header、payload、signature 用 .組起來,JWT 就完成了,可以直接來個線上測試

使用方法

client 發起請求的時候,header 需夾帶下面的資訊才能通過身份驗證。

1
Authorization: Bearer <token>

注意事項

  • JWT 代表了一個人的認證身份,任何人拿到它都可以盜用其身份對 server 發出請求,故 token 應該安全保存,並且存活的有效期不應設太長,對於一些比較重要的應用應該要再驗證一次身份。
  • 任何人拿到 token 都可以解析 payload 的內容,除非有額外做加密處理,否則不應夾帶敏感資訊。
  • 一但核發出去的 token ,除非後端有額外的機制,否則無法註銷,只能等待其過期。
  • 根據上一點,故也無法實踐單點登入。
  • 應結合SSL使用。
  • 不要重複造輪子,善用官方建議的 Library

結論

JWT 不是萬靈丹,有其優點也有其缺點,端看應用是否適合,並做好安全性的防護。
有關 JWT 的儲存方式各有各的說法,下次再開一篇新章討論。

Reference

Introduction to JSON Web Tokens
JSON Web Token 入門教程

XSS 攻擊和防堵

XSS(Cross Site Scripting) 是一種從網頁的漏洞下手,插入惡意程式碼的攻擊方式。攻擊本身是不給 server 帶來傷害,會造成傷害的是其他使用者。手法是在網站上一些可以讓使用者輸入的地方埋入 html 或是 JavaScript 的惡意腳本,讓其他使用者在瀏覽這個網頁時可以在背後竊取使用者的 cookie 送到指定伺服器或是引導到虛假頁面。

大致分成三種類型:

Stored XSS (儲存型)

經由使用者輸入,然後被存在 server 資料庫中的 JavaScript,若其後用來作為網頁顯示的時候,沒有過濾或是 encode 處理會被視為正常的 JavaScript 執行,藉此達到攻擊別的使用者的效果。常見的場景如論壇文章、留言板等公開的頁面。

舉個例子,假設有一個 input 可以給使用者留言,如下

1
<input type="text" placeholder="請留言" />

然後攻擊者在 input 輸入下面的內容

1
><script>alert("XSS攻擊");</script>

這樣惡意程式碼會被存在資料庫,之後在瀏覽器解析的時候若沒有經過處理則會被當成一般的 JavaScript 執行,達到攻擊的效果。

Reflected XSS (反射型)

反射型 xss 不會被儲存在資料庫中,而是 server 收到來自使用者的請求後,未做檢查就將使用者的請求原封不動顯示於回應的網頁當中以達成攻擊效用。可能會出現在 GET 請求中,常見的場景如網頁的搜尋功能,並且只要將帶有惡意程式碼的網址埋在受害者可能會點擊的地方或是分享給受害者點擊,等待受害者上鉤即可。

假設有一個網站有個搜尋功能如下

1
https://example.com?search=xxx

在搜尋完後會顯示結果在網頁上,DOM 可能會長這樣:

1
有關<span name="search">{{$search}}</span>的搜尋結果:

如果我們在搜尋的時候加入如下的惡意程式碼,等到網頁回傳搜尋結果時就達成攻擊

1
2
3
4
5
6
7
http://example.com?search=<script>alert(document.cookie);</script>

http://example.com?search="><script >alert(document.cookie)</script >

http://example.com?search="><ScRiPt>alert(document.cookie)</ScRiPt>

http://example.com?search="%3cscript%3ealert(document.cookie)%3c/script%3e

DOM-Based XSS

藉由 DOM 的漏洞來攻擊,在 DOM 埋入惡意程式碼,藉此來攻擊。

假設網頁上有一段 DOM 如下,

1
2
3
4
5
6
7
8
9
10
<script>
var send = function() {
var name = document.getElementById('your_name').value;
document.getElementById('name').innerHTML = name;
}

</script>
<span id="name"></span>
<input type="text" id="your_name" />
<button id="btn" onclick="send();">send</button>

如果輸入下面的內容,就可以破壞 DOM,執行 JavaScript 達成攻擊了,可以到這裡測試。

1
<img src=# onerror="alert('XSS 攻擊');">

但被害者不可能自己輸入這種惡意程式碼,除非攻擊者親自到受害者電腦前輸入,否則無法達成攻擊。因此 DOM-Based XSS 通常需要搭配前面兩個手法。讓內容保存在伺服器資料庫中,或是以反射型的方式製造出內容,再藉由 JavaScript 動態產生有效的 DOM 物件來運行惡意程式碼。

根據這三種類型可以整理出以下的表格:

惡意程式碼 存放的位置 插入點
儲存型 XSS 後端資料庫 HTML
反射型 XSS URL HTML
DOM 型 XSS 後端資料庫/前端儲存/URL 前端 JavaScript

防堵方法

一般來說,防堵的方式不外乎幾種,像是對於使用者傳來的內容做驗證、過濾或是轉譯。驗證的話,有些若是有明確需求的欄位如名字、電話、email 可以使用正則式做驗證,拒絕驗證失敗的內容,此為白名單的作法。而另外一種是只要遇到指定的字元如 <、>、”、’、& 一律過濾掉或是轉譯,但是即便是這樣,攻擊者還是有很多手法可以規避,例如改變字元大小寫、填充額外字元像是空白、使用 URL 編碼、或是使用等價字元等都可以規避驗證。而且可以被惡意注入的地方從 HTML 到 JS 再到 CSS 都有可能 ,所以 XSS 手法變化萬千,如何防堵是個長遠的議題。

個人傾向在輸出時做防堵,不要在存進資料庫時做 encode。
原因如下:

  • 如果 encode 的方式選錯或是要修改處理會很麻煩
  • 如果有搜尋的需求一樣要根據 encode 的方式處理
  • 根據資料之後可能會被應用的場景不同,可能需要做不同的處理,事先 encode 可能會很麻煩

特別說明一下,這邊是指輸入至 database 不過濾或是 encode html 和 JavaScript,但是仍要防堵 SQL injection。

那麼具體來說防堵要怎麼做呢?
以下針對不同類型的 XSS 討論一下各別的防堵方法:
首先,儲存型和反射型 XSS 都是在前端把惡意程式碼插入到 HTML 上,然後讓瀏覽器執行惡意的 JavaScript 達成攻擊。

防堵的方法有兩種:

  • 改成純前端渲染,把程式碼和資料分隔開
    一開始先載入一個不含資料的 HTML,資料都是後來由 js append 上 HTML,並且使用屬性明確的函式處理,例如 .innerText、.setAttribute、.style,避免惡意的程式碼被執行。
  • 對 HTML 做充分轉義
    如果應用不適合使用純前端渲染的模式,例如有 SEO 需求的考量,需要拼接 HTML,就必須善用模版引擎(template engine) 或是轉譯庫套件

而 DOM-Based XSS 則是因為 JavaScript 在執行過程中未對使用者輸入的資料做完善的檢查,導致惡意程式碼被插入 DOM 所產生的攻擊。防堵的方式為儘量減少使用 .innerHTML、.outerHTML、.document.write()等方法,轉而使用.textContent、.setAttribute等。此外,DOM 裡的一些監聽事件如 location、onclick、onerror、onload、onmouseover、以及 <a> 標籤的 href 屬性都能把字串作為程式碼執行,使用時需要非常小心。

結論

XSS 的重點在於輸出端的防堵,所以謹慎處理從使用者端取得的資料以及過濾敏感字元是可以防堵簡單的 XSS 攻擊,但仍可能被破解。XSS 的攻擊往往道高一尺魔高一丈,善用成熟的模版引擎以及轉譯套件庫也是可以參考的選項。並且時常閱讀 XSS Prevention Cheat Sheet 相關的文件以及注意 OWASP ESAPI(Enterprise Security API)這個專案上的最新攻擊手法,才能儘量降低服務被 XSS 攻擊的風險。

Reference

XSS Filter Evasion Cheat Sheet

前端安全系列(一):如何防止XSS攻擊?

DOM Based XSS

XSS與ESAPI

Testing for Reflected Cross site scripting (OTG-INPVAL-001)

CORS 的介紹與實作

在介紹CORS 之前先討論一下什麼是同源政策

同源政策 (same origin policy)

意思就是兩個網頁有相同的domainprotocol以及port號,只要三者有一個不相同即為不同源。

不同源的舉例:

  • 從自己的網站向一個第三方 API 發出請求。
  • 從 CDN 載入一些網路資源,例如 CSS、JS、圖片等

現代瀏覽器基於安全性考量在你發出非同源的請求時,request 會發出,但是 response 會被瀏覽器擋下來,並且在瀏覽器的 console 裡可以看到錯誤訊息,例如下面這張圖:

same origin policy error

那要怎麼解呢?
只要依循 CORS 就可以了。
因為每當發起一個非同源的請求就會受到 CORS 的控制。

CORS 是什麼

cors (Cross Origin Resource Shraring) 就是 跨來源資源共用。

當你發起一個非同源的請求時, 被請求的 server 必須在 response 的 header 裡加上 Access-Control-Allow-Origin 的設定。
當瀏覽器收到 response 後,會查看 response 的 header,如果 header 上的 Access-Control-Allow-Origin 有出現發起請求的 origin ,瀏覽器就會解析 response,結果不會被擋下來。然後 header 大概會長下面這個樣子:

1
2
3
4
5
6
7
8
Content-Type: application/json
Content-Length: 250
Connection: keep-alive
Server: nginx
Access-Control-Allow-Origin: *
Cache-Control: no-cache, no-store, must-revalidate, private
Expires: 0
Pragma: no-cache

其中 Access-Control-Allow-Origin: * 表示任何來源都接受。
通常除非是公開的 API 服務,不然 server 上都會設定針對 origin 以及 http method 的限制。

Simple Request & Preflight Request

其實 CORS 又分為兩種情況,一種是簡單請求(simple request),另一種則是預檢請求(preflight request)。

simple request 的條件需同時滿足以下條件:

  • request method 為 GET、HEAD、POST 其一
  • Content-Type 為 application/x-www-form-urlencoded、multipart/form-data、text/plain 其一
  • 除了一些開放的 header,沒有指定 custom header

詳細的限制可以參考 MDN 簡單請求

反之,若是使用其他 HTTP method,如 DELETE 或是 PATCH 等,可能需要 server 驗證的情況,這時候會用到 preflight request。
假設我們要 DELETE 一筆資源,preflight request 會先發出一個 OPTION 的請求確認 server 是否接受該請求,如果接受才會發出真正的請求,如果不接受的話請求就會在這裡被中斷, DELETE 不會真的被送出。

結論

  • 對於非同源的請求,server 還是會接受請求並處理,但是同時 server 也要在 header 設定許可的 Access-Control-Allow-Origin ,瀏覽器才會解析請求結果,否則結果會被瀏覽器所擋下
  • simple request 只會有一次的請求
  • preflight request 會先發出一個 OPTION 請求,待 server 許可後 client 才會發出真正的請求,故會有兩次 request 發出

Reference

輕鬆理解 Ajax 與跨來源請求

跨來源資源共用(CORS)

伺服器端存取控制(CORS)

跨域資源共享CORS 詳解

How does Access-Control-Allow-Origin header work?

CORS for Developers