Web Security Response Header

HTTP 安全Response Header,可以通過 Server 端(apache、nginx)的設定。告知瀏覽器安全方面的一些設定。用來增加網站的安全性。

先看一個Apache 的例子,以下內容 可以寫在 .htaccess 文檔中:

<IfModule mod_headers.c>
# 通知瀏覽器應該只通過 HTTPS 訪問該站點,瀏覽器應該記住的記住1年
Header always set Strict-Transport-Security "max-age=31536000" 

# 表示該頁面可以在相同域名頁面的 frame 中展示
Header set X-Frame-Options: "sameorigin"

# 阻止 請求類型是 style,且 MIME 類型不是 "text/css"。
# 阻止 請求類型是 script,且 MIME 類型不是 JavaScript MIME 類型
Header set X-Content-Type-Options: "nosniff"

# 阻止 JavaScript 通過 Document.cookie 屬性訪問 cookie
# 一個帶有安全屬性的 cookie 只有在請求使用 https: 的時候才會被發送到服務器。
Header always edit Set-Cookie (.*) "$1;HttpOnly;Secure"

# 啟用 XSS 過濾。如果檢測到攻擊,瀏覽器將不會清除頁面,而是阻止頁面加載。
Header set X-XSS-Protection "1; mode=block"
</IfModule>

Strict-Transport-Security。 (通常簡稱為 HSTS)響應標頭用來通知瀏覽器應該只通過 HTTPS 訪問該站點,並且以後使用 HTTP 訪問該站點的所有嘗試都應自動重定向到 HTTPS。

# 通知瀏覽器應該只通過 HTTPS 訪問該站點,瀏覽器應該記住的記住1年,規則也適用於該網站的所有子域名, 預加載
Header always set Strict-Transport-Security "max-age=31536000; includeSubdomains; preload"

Strict-Transport-Security VS 301重定向。 可以接受訪問 http ,譬如 curl -X POST -d 'name=xx' http://www.python.com.tw/xxx/xx 不会被强制跳转而丢掉数据。

Permissions-Policy 啟用或禁止瀏覽器特性的機制。 譬如控制是否允許當前文檔使用 Geolocation 接口。

Header set Permissions-Policy: "geolocation=*"

Content-Security-Policy。允許站點管理者控制用戶代理能夠為指定的頁面加載哪些資源。這將幫助防止跨站腳本攻擊(Cross-Site Script)

Referrer-PolicyReferer 請求頭包含了當前請求頁面的來源頁面的地址。 Referrer-Policy 是針對 Referer 的策略

# 對應 同源的請求,發送來源、路徑以及查詢字符串。strict-origin-when-cross-origin(默認值) 
Header set Referrer-Policy "strict-origin-when-cross-origin"

詳情請參考 https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security

golang – net.Dial() 獲取 whois資料

本文是 net.Dail() 的一個實例。用於獲取域名的whois資料。

已知要獲取域名 python.com.tw 的whois資料,需要訪問 whois.twnic.net.tw 的 43 端口。(通過插件 github.com/zonedb/zonedb 可以獲取whois server信息)

先看程序:

domain := "python.com.tw"

const DefaultReadLimit = 1 << 10 << 10 // 讀取 1M

conn, err := net.Dial("tcp", "whois.cnnic.cn:43") // 建立連結
if err != nil {
	fmt.Println("client dial err =", err)
}

defer conn.Close() // 執行完畢後,關閉連結

// 發送請求內容
if _, err := conn.Write([]byte(domain + "\r\n")); err != nil {
	fmt.Println("client dial write err =", err)
}

// 讀取內容
body, _ := io.ReadAll(io.LimitReader(conn, DefaultReadLimit))
fmt.Println(string(body))

conn, err := net.Dial("tcp", "whois.cnnic.cn:43") 建立和 whois server 之間的連結。

再發送要請求的內容 _, err := conn.Write([]byte(domain + "\r\n"))

最後 創建一個 reader 最大讀取 1M io.LimitReader(conn, DefaultReadLimit),然後再通過 io.ReadAll() 讀取全部內容

Golang viper – 管理設定檔的工具

大型項目中,通常把一些設定放入單獨的設定文檔中。viper 是管理這些設定文檔的工具。

安裝 viper go get github.com/spf13/viper

特點:

  • 設定預設值
  • 讀取 JSON、TOML、YAML、HCL、envfile 和 Java 屬性設定檔
  • 即時觀看和重新讀取設定檔(可選)
  • 從環境變數中讀取
  • 從遠端配置系統(etcd 或 Consul)讀取並觀察更改
  • 從命令列標誌讀取
  • 從緩衝區讀取
  • 設定明確的值

例子:

func main() {
	//viper.SetConfigName("app") // name of config file (without extension)
	//viper.SetConfigType("yaml")
	viper.SetConfigFile(".env")
	viper.AddConfigPath(".")
	err := viper.ReadInConfig() // Find and read the config file
	if err != nil {             // Handle errors reading the config file
		panic(fmt.Errorf("fatal error config file: %w", err))
	}

	fmt.Println(viper.Get("version"))
	fmt.Println(viper.Get("animal"))
}

官方文檔: https://github.com/spf13/viper

Self-signed certificates

Self-signed certificates, 自簽名憑證。在數據傳輸中是否有對數據加密?答案是肯定的,數據有加密。

Self-signed certificates 一般是用在內部網路加密數據傳輸。其特點如下:

  • 免費。自簽名證書是免費提供的,任何開發人員都可以申請。
  • 隨時簽發。自簽名證書可以隨時隨地簽發,不用等待第三方證書頒發機構的驗證和簽發。
  • 加密。自簽名SSL證書使用與其他付費SSL/TLS證書相同的方法加密傳輸數據。
  • 方便。自簽名證書不會在一段時間後過期或需要續訂,但CA頒發的證書卻會在一段時間後過期,還需要續訂。
  • 不被信任
  • 不會自動吊銷證書

生成 憑證參考:https://python.com.tw/openssl-generate-private-key-csr-crt/

Gzip static file

使用node生成靜態文檔的時候, 部分項目會出現 index-a1adc171.jsindex-a1adc171.js.gz。這樣是方便支持 gzip_static 的情況直接調用 index-a1adc171.js.gz 而不用 server 再次壓縮。

例如使用 插件 vite-plugin-compression:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import compression from "vite-plugin-compression"

export default defineConfig({
  plugins: [
    react(),
    compression(),
  ],
})

要實現以上功能,還需Http Server 設定。

Nginx設定:

gzip_static  on;
gzip_proxied expired no-cache no-store private auth;

Apache設定:

# AddEncoding allows you to have certain browsers uncompress information on the fly.
AddEncoding gzip .gz

#Serve gzip compressed CSS files if they exist and the client accepts gzip.
RewriteCond %{HTTP:Accept-encoding} gzip
RewriteCond %{REQUEST_FILENAME}\.gz -s
RewriteRule ^(.*)\.css $1\.css\.gz [QSA]

# Serve gzip compressed JS files if they exist and the client accepts gzip.
RewriteCond %{HTTP:Accept-encoding} gzip
RewriteCond %{REQUEST_FILENAME}\.gz -s
RewriteRule ^(.*)\.js $1\.js\.gz [QSA]

# Serve correct content types, and prevent mod_deflate double gzip.
RewriteRule \.css\.gz$ - [T=text/css,E=no-gzip:1]
RewriteRule \.js\.gz$ - [T=text/javascript,E=no-gzip:1]

OpenSSL 生成private key 、csr、crt

關於生成private key 、csr、crt等內容,網路上很多都是很多命令,讓人眼花。今天介紹一行命令搞定。哪怕多個域名的csr (req)一行也搞定。

  • 私有金鑰 – Pirvate Key
  • 簽署要求 – Certificate Signing Request(CSR)
  • 憑證 – CRT

先簡單說一個文檔格式。OpenSSL 命令行操作時 默認格式為PEM。

  • DER 是一種二進制格式
  • PEM 將二進制編碼成字符串。使用base64編碼。格式為:
-----BEGIN <whatever>----- 
data
-----END <whatever>----

一行命令生成 private key 和 csr

openssl req -nodes -newkey rsa:4096 \
    -keyout private.pem \
    -out csr.pem \
    -subj "/C=TW/ST=Zuid Holland/L=Rotterdam/O=ACME Corp/OU=IT Dept/CN=example.org"

參數說明

  • -nodes -newkey rsa:4096 生成 沒有加密的 private key, rsa:4096 是加密算法
  • -keyout private.pem key保存到 private.pem 這個文檔
  • -out csr.pem Csr 保存到 csr.pem 這個文檔
  • -subj csr 信息
  • /C Country Name 國家代碼,例如 EN TW 等
  • /ST State or Province Name ,省份
  • /L Locality Name
  • /O Organization Name 組織名稱
  • /OU Organizational Unit Name
  • /CN Common Name 通用名 例如 abc.com 或 *.abc.com

一行命令生成 private key 和 crt (自簽名 self signed cert)

openssl req -x509 -nodes -newkey rsa:4096 \
    -keyout private.pem \
    -out crt.pem \
    -days 356 \
    -subj "/C=TW/ST=Zuid Holland/L=Rotterdam/O=ACME Corp/OU=IT Dept/CN=example.org"

比上面的多了幾個參數

  • -x509 直接生成 crt
  • -days 356 有效日期

一行命令生成 private key 和 csr 多個Domain

OpenSSL 1.1.1 或以上版本

openssl req -nodes -newkey rsa:4096 \
    -keyout private.pem \
    -out csr.pem \
    -subj "/C=TW/ST=Zuid Holland/L=Rotterdam/O=ACME Corp/OU=IT Dept/CN=example.org"
    -addext "subjectAltName = DNS:localhost,DNS:example.org"

OpenSSL 1.1.1 以下版本:

openssl req -nodes -newkey rsa:4096 \
    -keyout private.pem \
    -out csr.pem \
    -subj "/C=TW/ST=Zuid Holland/L=Rotterdam/O=ACME Corp/OU=IT Dept/CN=example.org"
    -extensions san \
    -config <( \
    echo '[req]'; \
    echo 'distinguished_name=req'; \
    echo '[san]'; \
    echo 'subjectAltName=DNS:localhost,DNS:example.org')

使用比較流行的 ECC 编码

只要把 rsa:4096 替換為 ec:<(openssl ecparam -name secp384r1)

使用 openssl ecparam -list_curves 命令,可以查看 支持哪些ECC編碼。

jQuery Form Plugin – jquery.form.js

jQuery Form Plugin可以很方便的提交form表單。提供了2個函數ajaxFormajaxSubmit

ajaxFormajaxSubmit參數是一樣的,兩者之間的區別是: ajaxForm不會立即提交,需要額外執行提交。ajaxForm 可以描述為對 form 的設定。ajaxSubmit則會立即提交Form

可以定義 beforeSubmit 發送ajax提交之前的操作。以及 success 提交後返回內容。

在負載的一些操作中返回數據是json,程序讀寫會方便一些。 以下是提交數據為 json,返回數據也為 json的例子:

function  sendSuccess(data, status)  {
    if(status!=='success') {
        alert('Error');
        return false;
    }

    alert(data.message);
}

$('#myForm2').ajaxSubmit({
    dataType:'json',
    success: sendSuccess
});

以下是官網的一個例子:

// prepare the form when the DOM is ready
$(document).ready(function() {
    var options = {
        target:        '#output2',   // target element(s) to be updated with server response
        beforeSubmit:  showRequest,  // pre-submit callback
        success:       showResponse  // post-submit callback

        // other available options:
        //url:       url         // override for form's 'action' attribute
        //type:      type        // 'get' or 'post', override for form's 'method' attribute
        //dataType:  null        // 'xml', 'script', or 'json' (expected server response type)
        //clearForm: true        // clear all form fields after successful submit
        //resetForm: true        // reset the form after successful submit

        // $.ajax options can be used here too, for example:
        //timeout:   3000
    };

    // bind to the form's submit event
    $('#myForm2').submit(function() {
        // inside event callbacks 'this' is the DOM element so we first
        // wrap it in a jQuery object and then invoke ajaxSubmit
        $(this).ajaxSubmit(options);

        // !!! Important !!!
        // always return false to prevent standard browser submit and page navigation
        return false;
    });
});

// pre-submit callback
function showRequest(formData, jqForm, options) {
    // formData is an array; here we use $.param to convert it to a string to display it
    // but the form plugin does this for you automatically when it submits the data
    var queryString = $.param(formData);

    // jqForm is a jQuery object encapsulating the form element. To access the
    // DOM element for the form do this:
    // var formElement = jqForm[0];

    alert('About to submit: \n\n' + queryString);

    // here we could return false to prevent the form from being submitted;
    // returning anything other than false will allow the form submit to continue
    return true;
}

// post-submit callback
function showResponse(responseText, statusText, xhr, $form)  {
    // for normal html responses, the first argument to the success callback
    // is the XMLHttpRequest object's responseText property

    // if the ajaxSubmit method was passed an Options Object with the dataType
    // property set to 'xml' then the first argument to the success callback
    // is the XMLHttpRequest object's responseXML property

    // if the ajaxSubmit method was passed an Options Object with the dataType
    // property set to 'json' then the first argument to the success callback
    // is the json data object returned by the server

    alert('status: ' + statusText + '\n\nresponseText: \n' + responseText +
        '\n\nThe output div should have already been updated with the responseText.');
}

jQuery Validation — jquery.validate.js

jQuery Validation 插件為表單提供了強大的驗證功能,讓客戶端表單驗證變得更簡單,同時提供了大量的定制選項,滿足應用程序各種需求。功能如下:

  • 自帶多種規則,譬如 email url number 等
  • 自定義 錯誤提示 標籤
  • 自定義 錯誤提示 內容
  • 自定義 驗證函數

加載&安裝

<script src="https://cdn.jsdelivr.net/npm/jquery-validation@1.19.5/dist/jquery.validate.min.js"></script>

列子:

 $(function (){
    $.validator.methods.length4 = function(value, element, param) {
        return value.length == param;
    };
    $("#contactForm").validate({
        // errorClass: "check",
        errorElement: "span",
        errorPlacement: function(error, element) {
            error.appendTo(element.parent());
        },
        submitHandler: function(form) {
            jQuery(form).ajaxSubmit({
                dataType:'json',
                success: sendSuccess
            });
        }
    });
});
  • length4 是自定義 判斷長度的規則。
  • errorElement 定義錯誤信息的標籤。
  • errorClass 定義錯誤內容的 css 樣式
  • errorPlacement 定義錯誤信息出現在哪裡。

以上設定會在指定的地方出現:

<span id="lastName-error" class="error">Required</span>

submitHandler 自定義了當 驗證都通過後需要執行的內容。

PhpStorm advanced metadata — .phpstorm.meta.php

PhpStorm 是一款很只能的PHP編輯器,絕大部分的class 都能智能的提示對應類型。但是有一些使用 __call() 來實現的映射功能 無法自動獲取。 譬如 laravel或自定義的一些特殊映射功能。 PhpStorm 的 .phpstorm.meta.php 這時候就派上了用場。

直接在項目的根目錄創建一個文檔.phpstorm.meta.php內容格式如下:

<?php
namespace PHPSTORM_META {
    //metadata directives
}

override() 定義函數返回類型,例如:

override(\app(), map([
    "" => "@",
    "tree" => \App\Services\TreeService::class,
    "common" => \App\Services\CommonService::class,
]));

上面列子中 app('tree') 自動會匹配 \App\Services\TreeService

expectedArguments() 定義函數接受的參數,例如:

expectedArguments(
    \fun1(),
    0,
    'tree',
    'common'
);

上列中 在編輯器中輸入 fun1() 自動彈出提示參數 tree common

更詳細的參考文檔: https://www.jetbrains.com/help/phpstorm/ide-advanced-metadata.html

golang – log 日誌包

log是 Go 標準庫提供的,不需要另外安裝。可直接使用。它提供了3組函數:

  • Print/Printf/Println:正常輸出日誌;
  • Panic/Panicf/Panicln:輸出日誌後,以拼裝好的字符串為參數調用panic
  • Fatal/Fatalf/Fatalln:輸出日誌後,調用os.Exit(1)退出程序。
import "log"

func main() {
	Name := "howard"
	Age := 11

	log.Printf("%s login, age:%d", Name, Age)
	log.Panicf("Oh, system error when %s login", Name)
	log.Fatalf("Danger! hacker %s login", Name)
}

可在每條輸出的文本前增加一些額外信息,如日期時間、文件名等。

// src/log/lgo.go
const (
	Ldate         = 1 << iota     // the date in the local time zone: 2009/01/23
	Ltime                         // the time in the local time zone: 01:23:23
	Lmicroseconds                 // microsecond resolution: 01:23:23.123123.  assumes Ltime.
	Llongfile                     // full file name and line number: /a/b/c/d.go:23
	Lshortfile                    // final file name element and line number: d.go:23. overrides Llongfile
	LUTC                          // if Ldate or Ltime is set, use UTC rather than the local time zone
	Lmsgprefix                    // move the "prefix" from the beginning of the line to before the message
	LstdFlags     = Ldate | Ltime // initial values for the standard logger
)
  • Ldate:輸出當地時區的日期,如2021/02/07
  • Ltime:輸出當地時區的時間,如12:45:45
  • Lmicroseconds:輸出的時間精確到微秒,設置了該選項就不用設置Ltime了。如12:45:45.123123
  • Llongfile:輸出長文件名+行號,含包名
  • Lshortfile:輸出短文件名+行號,不含包名,如main.go:20
  • LUTC:如果設置了Ldate或Ltime,將輸出 UTC 時間,而非當地時區
func main() {
	Name := "howard"
	Age := 11

	log.SetFlags(log.Lshortfile | log.Ldate | log.Lmicroseconds)

	log.Printf("%s login, age:%d", Name, Age)
  // 輸出: 2023/09/11 16:29:26.833582 log1.go:11: howard login, age:11
}

標準庫log並沒有日誌等級的設定。 如果有用到日誌等級,可以參考 zap 。以及日誌切割 包lumberjack