在完成中間件的介紹和日志中間件的代碼后,我們的程序已經基本能正常跑通了,但如果要上生產,還少了一些必要的功能,例如鑒權、異常捕捉等。本章我們介紹如何編寫鑒權中間件。
鑒權訪問,說白了就是給用戶的請求增加一些限制條件,過濾掉不符合要求的請求。完善的鑒權模塊可以讓我們的服務跑得更加安全,特別是面向公共的服務。
常用的無狀態鑒權方式
- 網絡鑒權
通常有IP白名單方式,通過獲取客戶端的真實IP來對請求進行過濾
- 用戶鑒權
通過賬號密碼或者分配的密鑰、Token等方式進行認證,常用的cookies、oauth2.0都是這種方式
- 加密算法鑒權
客戶端使用加密算法對用戶的參數進行計算加密得到Token,並將參數和Token一起發送;服務端使用同樣的加密算法對請求參數進行加密后比較Token的值是否一致。
Gin-IPs 鑒權訪問
為了讓我們的服務更加安全,我們通常是混合多種鑒權方式使用。本文采取“用戶鑒權”和“加密算法鑒權”混合方式。
- 鑒權算法介紹
通過某種方式生成或者手動指定分配公鑰和私鑰對給用戶,用戶使用公鑰和請求參數組成消息內容,使用私鑰對消息內容進行哈希計算,得到固定長度的字符串。
服務器使用同樣的方式對用戶請求的參數進行哈希計算后比較。由於這里面的算法和密鑰對都是私有的,所以安全性較高,適用於大多數場景。
- 加密代碼
// 簽名算法如下
/*
Signature = HMAC-SHA1('SecretKey', UTF-8-Encoding-Of( StringToSign ) ) );
StringToSign = method + "\n" +
URL + "\n" +
Sort-UrlParams + "\n" +
Content-MD5 + "\n" + // md5(params)
Expires + "\n" +
AccessKey;
*/
func genSignature(accessKey, secretKey, uri, method, urlParams, params, nowTS string) (string, error) {
if params != "" {
md5Ctx := md5.New()
_, _ = io.WriteString(md5Ctx, params)
params = fmt.Sprintf("%x", md5Ctx.Sum(nil))
}
strSign := method + "\n" + uri + "\n" + urlParams + "\n" + params + "\n" + nowTS + "\n" + accessKey
sign := hmacSHA1Encrypt(strSign, secretKey)
return sign, nil
}
// hmacSHA1Encrypt encrypt the encryptText use encryptKey
func hmacSHA1Encrypt(encryptText, encryptKey string) string {
key := []byte(encryptKey)
mac := hmac.New(sha1.New, key)
mac.Write([]byte(encryptText))
var str = hex.EncodeToString(mac.Sum(nil))
return str
}
- Gin鑒權中間件使用
func Validate() gin.HandlerFunc {
return func(c *gin.Context) {
response := route_response.Response{}
response.Data.List = []interface{}{} // 初始化為空切片,而不是空引用
traceId := SnowWorker.GetId()
c.Writer.Header().Set("X-Request-Trace-Id", traceId)
uri := c.Request.URL.Path
// remoteAddr := c.ClientIP() // 也可以對客戶端IP進行限制
contentType := c.Request.Header.Get("Content-Type")
if contentType != "application/json" {
c.Abort()
response.Code, response.Message = configure.RequestParameterTypeError, "Content-Type 類型只支持 application/json"
c.JSON(http.StatusUnauthorized, response)
return
}
accessKey := c.DefaultQuery("accesskey", "")
expires := c.DefaultQuery("expires", "")
signature := c.DefaultQuery("signature", "")
if accessKey == "" {
c.Abort()
response.Code, response.Message = configure.RequestParameterMiss, "Token缺失"
c.JSON(http.StatusUnauthorized, response)
return
}
secret, err := dao.FetchSecret(accessKey)
if err != nil || "valid" != secret.State {
c.Abort()
response.Code, response.Message = configure.RequestKeyNotFound, "無效的Token"
c.JSON(http.StatusUnauthorized, response)
return
}
c.Writer.Header().Set("X-Request-User", secret.User)
if expires == "" {
c.Abort()
response.Code, response.Message = configure.RequestParameterMiss, "有效期參數缺失"
c.JSON(http.StatusUnauthorized, response)
return
}
if signature == "" {
c.Abort()
response.Code, response.Message = configure.RequestParameterMiss, "簽名缺失"
c.JSON(http.StatusUnauthorized, response)
return
}
secretKey := secret.SecretKey
if nowTs, err := strconv.ParseInt(expires, 10, 64); err != nil {
c.Abort()
response.Code, response.Message = configure.RequestParameterTypeError, "有效期參數類型錯誤"
c.JSON(http.StatusUnauthorized, response)
return
} else {
passTime := time.Now().Unix() - nowTs
if passTime < 0 || passTime >= configure.GinConfigValue.Expires {
// 容錯時間越大越容易被攻擊,服務器時間必須准
c.Abort()
response.Code, response.Message = configure.RequestExpired, "請求已過期"
c.JSON(http.StatusUnauthorized, response)
return
}
}
method := strings.ToUpper(c.Request.Method)
var urlParams, params string
if "POST" == method || "PUT" == method {
body, _ := ioutil.ReadAll(c.Request.Body)
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) // 重設body
params = string(body)
} else if "GET" == method || "DELETE" == method {
queryParams := c.Request.URL.Query()
allParams := make(map[string]string)
for k, v := range queryParams {
if k != "accesskey" && k != "expires" && k != "signature" {
allParams[k] = v[0] // 如果某個key傳入了2個,只用第一個的值
}
}
keys := getMapKeysSorted(allParams)
for _, k := range keys {
urlParams += k + allParams[k]
}
}
if signatureString, err := genSignature(accessKey, secretKey, uri, method, urlParams, params, expires); err != nil {
c.Abort()
response.Code, response.Message = configure.ApiGenSignatureError, "API內部錯誤"
c.JSON(http.StatusUnauthorized, response)
return
} else {
if signature != signatureString {
c.Abort()
response.Code, response.Message = configure.RequestAuthorizedFailed, "API認證失敗"
c.JSON(http.StatusUnauthorized, response)
return
}
}
c.Next()
}
}
本文關於鑒權訪問的介紹和使用到此為止,下一章我們將使用異常捕捉中間件來完善我們的程序。
Github 代碼
請訪問 Gin-IPs 或者搜索 Gin-IPs