Go web中cookie值安全securecookie庫使用原理

引言

今天給大傢推薦的是web應用安全防護方面的另一個包:securecookie。該包給cookie中存儲的敏感信息進行編、解碼及解密、解密功能,以保證數據的安全。

securecookie小檔案

securecookie小檔案      
star 595 used by
contributors 19 作者 Gorilla
功能簡介 對cookie中存儲的敏感信息進行編碼、解碼以及加密、解密功能,以保證數據不能被偽造。    
項目地址 github.com/gorilla/sec…    
相關知識 web安全、加密解密、HMAC編碼解碼、base64編碼    

一、安裝

 go get github.com/gorilla/securecookie 

二、使用示例

明文的cookie值輸出

我們先來看下未進行編碼或未加密的cookie輸出是什麼樣的。本文以beego框架為例,當然在beego中已經實現瞭安全的cookie輸出,稍後再看其具體的實現。這裡主要是來說明cookie中未編碼的輸出和使用securecookie包後cookie的值輸出。

package main
import (
	"github.com/beego/beego"
)
func main() {
	beego.Router("/", &MainController{})
	beego.RunWithMiddleWares(":8080")
}
type MainController struct {
	beego.Controller
}
func (this *MainController) Get() {
	this.Ctx.Output.Cookie("userid", "1234567")
	this.Ctx.Output.Body([]byte("Hello World"))
}

執行go run main.go,然後在瀏覽器中輸入http://localhost:8080/,查看cookie的輸出是明文的。如下:

使用securecookie包對cookie值進行編碼

securecookie包的使用也很簡單。首先使用securecookie.New函數實例化一個securecookie實例,在實例化的時候需要傳入一個32位或64位的hashkey值。然後調用securecookie實例的Encode對明文值進行編碼即可。如下示例:

package main
import (
	"github.com/beego/beego"
	"github.com/gorilla/securecookie"
)
func main() {
	beego.Router("/", &MainController{})
	beego.RunWithMiddleWares(":8080")
}
type MainController struct {
	beego.Controller
}
func (this *MainController) Get() {
	// Hash keys should be at least 32 bytes long
	var hashKey = []byte("keep-it-secret-keep-it-safe-----")
	// 實例化securecookie
	var s = securecookie.New(hashKey, nil)
	name := "userid"
	value := "1234567"
    // 對value進行編碼
	encodeValue, _ := s.Encode(name, value)
    // 輸出編碼後的cookie值
	this.Ctx.Output.Cookie(name, encodeValue)
	this.Ctx.Output.Body([]byte("Hello World"))
}

以下是經過securecookie編碼後的cookie值輸出結果:

在調用securecookie.New時,第一個參數hashKey是必須的,推薦使用32字節或64字節長度的key。因為securecookie底層編碼時是使用HMAC算法實現的,hmac算法在對數據進行散列操作時會進行加密。

securecookie包不僅支持對字符串的編碼和加密。還支持對結構體及自定義類型進行編碼和加密。下面示例是對一個map[string]string類型進行編/解碼的實例。

package main
import (
	"fmt"
	"github.com/beego/beego"
	"github.com/gorilla/securecookie"
)
func main() {
	beego.Router("/", &MainController{})
	beego.RunWithMiddleWares(":8080")
}
type MainController struct {
	beego.Controller
}
func (this *MainController) Get() {
	// Hash keys should be at least 32 bytes long
	var hashKey = []byte("keep-it-secret-keep-it-safe-----")
	// Block keys should be 16 bytes (AES-128) or 32 bytes (AES-256) long.
	// Shorter keys may weaken the encryption used.
	var blockKey = []byte("1234567890123456")
	// 實例化securecookie
	var s = securecookie.New(hashKey, blockKey)
	value := map[string]string{
		"id": "1234567",
	}
	name := "userid"
	//value := "1234567"
	//
	encodeValue, err := s.Encode(name, value)
	fmt.Println("encodeValue:", encodeValue, err)
    // 解析到decodeValue中
	decodeValue := make(map[string]string)
	s.Decode(name, encodeValue, &decodeValue)
	fmt.Println("decodeValue:", decodeValue)
	this.Ctx.Output.Cookie(name, encodeValue)
	this.Ctx.Output.Body([]byte("Hello World"))
}

當然,其他類型也是支持的。大傢有興趣的可以自行看下源碼。

使用securecookie對value加密

securecookie不止可以對明文值進行編碼,而且還可以對編碼後的值進一步加密,使value值更安全。加密也很簡單,就是在調用securecookie.New的時候傳入第二個參數:加密秘鑰即可。如下:

	// Hash keys should be at least 32 bytes long
	var hashKey = []byte("keep-it-secret-keep-it-safe-----")
	// Block keys should be 16 bytes (AES-128) or 32 bytes (AES-256) long.
	// Shorter keys may weaken the encryption used.
	var blockKey = []byte("1234567890123456")
	// 實例化securecookie
	var s = securecookie.New(hashKey, blockKey)
	name := "userid"
	value := "1234567"
	encodeValue, err := s.Encode(name, value)

以下是經過securecookie加密後的cookie值輸出結果:

在securecookie包中,是否對cookie值進行加密是可選的。在調用New時,如果第二個參數傳nil,則cookie值隻進行hash,而不加密。如果給第二個參數傳瞭一個值,即秘鑰,則該包還會對hash後的值再進行加密處理。這裡需要註意,加密秘鑰的長度必須是16字節或32字節,否則會加密失敗。

對cookie值進行解碼

有編碼就有解碼。在收到請求中的cookie值後,就可以使用相同的securecookie實例對cookie值進行解碼瞭。如下:

package main
import (
	"fmt"
	"github.com/beego/beego"
	"github.com/gorilla/securecookie"
)
func main() {
	beego.Router("/", &MainController{})
	beego.RunWithMiddleWares(":8080")
}
type MainController struct {
	beego.Controller
}
func (this *MainController) Get() {
	// Hash keys should be at least 32 bytes long
	var hashKey = []byte("keep-it-secret-keep-it-safe-----")
	// Block keys should be 16 bytes (AES-128) or 32 bytes (AES-256) long.
	// Shorter keys may weaken the encryption used.
	var blockKey = []byte("1234567890123456")
	// 實例化securecookie
	var s = securecookie.New(hashKey, blockKey)
	encodeValue := this.Ctx.GetCookie("userid")
	value := ""
	s.Decode("userid", encodeValue, &value)
	fmt.Println("decode value is :", value, encodeValue)
	this.Ctx.Output.Cookie("userid", value)
	this.Ctx.Output.Body([]byte("Hello World"))
}

該示例是我們把上次加密的cookie值發送給本次請求,服務端進行解碼後寫入到cookie中。本次輸出正好是明文“1234567”。

這裡需要註意的是,解碼的時候Decode的第一個參數是cookie的name值。第二個參數才是cookie的value值。這是成對出現的。後面在講編碼的實現原理時會詳細講解。

三、實現原理

securecookie包Encode函數的實現主要有兩點:加密和hash轉換。同樣Decode的過程與Encode是相反的。

Encode函數的實現流程如下:

序列化

第一步為什麼要把value值進行序列化呢?我們看securecookie.Encode接口,如下:

func (s *SecureCookie) Encode(name string, value interface{}) (string, error)

我們知道cookie中的值是key-value形式的。這裡name就是cookie中的key,value是cookie中的值。我們註意到value的類型是interface{}接口,也就是說value可以是任意數據類型(結構體,map,slice等)。但cookie中的value隻能是字符串。所以,Encode的第一步就是把value值進行序列化。

序列化有兩種方式,分別是內建的包encoding/json和encoding/gob。securecookie包默認使用gob包進行序列化:

func (e GobEncoder) Serialize(src interface{}) ([]byte, error) {
	buf := new(bytes.Buffer)
	enc := gob.NewEncoder(buf)
	if err := enc.Encode(src); err != nil {
		return nil, cookieError{cause: err, typ: usageError}
	}
	return buf.Bytes(), nil
}

 知識點:encoding/json和encoding/gob的區別:gob包比json包生成的序列化數據體積更小、性能更高。但gob序列化的數據隻適用於go語言編寫的程序之間傳遞(編碼/解碼)。而json包適用於任何語言程序之間的通信。

如果在編碼過程中想使用json對value值進行序列化,那麼可以通過SetSerialize方法進行設置,如下:

cookie := securecookie.New([]byte("keep-it-secret-keep-it-safe-----")
cookie.SetSerializer(securecookie.JSONEncoder{})

加密

加密是可選的。如果在調用secrecookie.New的時候指定瞭第2個參數,那麼就會對序列化後的數據加密操作。如下:

	// 2. Encrypt (optional).
	if s.block != nil {
		if b, err = encrypt(s.block, b); err != nil {
			return "", cookieError{cause: err, typ: usageError}
		}
	}

加密使用的AES對稱加密。在Go的內建包crypto/aes中。該包有5種加密模式,5種模式之間采用的分塊算法不同。有興趣的同學可以自行深入研究。而securecookie包采用的是CTR模式。如下是加密相關代碼:

func encrypt(block cipher.Block, value []byte) ([]byte, error) {
	iv := GenerateRandomKey(block.BlockSize())
	if iv == nil {
		return nil, errGeneratingIV
	}
	// Encrypt it.
	stream := cipher.NewCTR(block, iv)
	stream.XORKeyStream(value, value)
	// Return iv + ciphertext.
	return append(iv, value...), nil
}

該對稱加密算法其實還可以應用其他具有敏感信息的傳輸中,比如價格信息、密碼等。

base64編碼

經過上述編碼(或加密)後的數據實際上是一串字節序列。如果轉換成字符串大傢可以看到會有亂碼的出現。這裡的亂碼實際上是不可見字符。如果想讓不可見字符變成可見字符,最常用的就是使用base64編碼。 base64編碼是將二進制字節轉換成文本的一種編碼方式。該編碼方式是將二進制字節轉換成可打印的asc碼。就是先預定義一個可見字符的編碼表,參考RFC4648文檔。然後將原字符串的二進制字節序列以每6位為一組進行分組,然後再將每組轉換成十進制對應的數字,在根據該數字從預定義的編碼表中找到對應的字符,最終組成的字符串就是經過base64編碼的字符串。在base64編碼中有4種模式:

  • base64.StdEncoding:標準模式是依據RFC 4648文檔實現的,最終轉換成的字符由A到Z、a-z、0-9以及+和 / 符號組成的。
  • base64.URLEncoding: URLEncoding模式最終轉成的字符是由A到Z、a-z、0-9以及 – 和 _ 組成的。就是把標準模式中的+和/字符替換成瞭-和/。因為該模式主要應用於URL地址傳輸中,而在URL中+和/是保留字符,不能出現,所以講其做瞭替換。
  • base64.RawEncoding: 該模式使用的字符集和StdEncoding一樣。但該模式是按照位數來的,每6bits換為一個base64字符,就沒有在尾部補齊到4的倍數字節瞭。
  • base64.RawURLEncoding: 該模式使用的字符集和URLEncoding模式一樣。同樣該模式也是按照位數來的,每6bits換為一個base64字符,就沒有在尾部補齊到4的倍數字節瞭。

base64編碼的具體應用和實現原理大傢可參考我的另外一篇文章:

使用hmac做hash

簡單來講就是對字符串做瞭加密的hash轉換。在上文中我們提到,加密是可選的,hmac才是必需的。如果沒有使用加密,那麼經過上述序列化、base64編碼後的字符串依然是明文的。所以無論有沒有加密,都要做一次hash。這裡使用的是內建包crypto/hmac。

做hmac操作時,不是隻對value值進行hash,而是經過瞭字符串的拼接。實際上是對cookie名、日期、value值三部分進行拼接,並用 "|"隔開進行的:

代碼如下:

	// 3. Create MAC for "name|date|value". Extra pipe to be used later.
	b = []byte(fmt.Sprintf("%s|%d|%s|", name, s.timestamp(), b))
	mac := createMac(hmac.New(s.hashFunc, s.hashKey), b[:len(b)-1])
	// Append mac, remove name.
	b = append(b, mac...)[len(name)+1:]
	// 4. Encode to base64.
	b = encode(b)

這裡將name值拼接進字符串是因為在加碼驗證的時候可以對key-value對進行驗證,說明該value是屬於該name值的。 將時間戳拼接進去,主要是為瞭對cookie的有效期做驗證。在解密後,用當前時間和字符串中的時間做比較,就能知道該cookie值是否已經過期瞭。

最後,將經過hmac的hash值除去name值後再和b進行拼接。拼接完,為瞭在url中傳輸,所以再做一次base64的編碼。

相關知識:HMAC是密鑰相關的哈希運算消息認證碼(Hash-based Message Authentication Code)的縮寫,由H.Krawezyk,M.Bellare,R.Canetti於1996年提出的一種基於Hash函數和密鑰進行消息認證的方法。其能提供兩方面的內容: ① 消息完整性認證:能夠證明消息內容在傳送過程沒有被修改。 ② 信源身份認證:因為通信雙方共享瞭認證的密鑰,接收方能夠認證發送該數據的信源與所宣稱的一致,即能夠可靠地確認接收的消息與發送的一致。

四、beego框架中的cookie安全

筆者查看瞭常用的web框架echo、gin、beego,發現隻有在beego框架中集成瞭安全的cookie設置。但也隻實現瞭用hmac算法對value值和時間戳做加密hash。該實現在Controller的SetSecureCookie函數中,如下:

// SetSecureCookie puts value into cookie after encoded the value.
func (c *Controller) SetSecureCookie(Secret, name, value string, others ...interface{}) {
	c.Ctx.SetSecureCookie(Secret, name, value, others...)
}
// SetSecureCookie Set Secure cookie for response.
func (ctx *Context) SetSecureCookie(Secret, name, value string, others ...interface{}) {
	vs := base64.URLEncoding.EncodeToString([]byte(value))
	timestamp := strconv.FormatInt(time.Now().UnixNano(), 10)
	h := hmac.New(sha256.New, []byte(Secret))
	fmt.Fprintf(h, "%s%s", vs, timestamp)
	sig := fmt.Sprintf("%02x", h.Sum(nil))
	cookie := strings.Join([]string{vs, timestamp, sig}, "|")
	ctx.Output.Cookie(name, cookie, others...)
}

五、總結

經過securecookie編碼過的cookie值是不會被偽造的,因為該值是經過hmac進行編碼的。而且還可以對編碼過的值再進行一次對稱加密。如果是敏感信息的話,建議不要存儲在cookie中。同時,敏感的信息也一定使用https進行傳輸,以降低泄露的風險。

以上就是Go web中cookie值安全securecookie庫使用原理的詳細內容,更多關於Go web securecookie庫的資料請關註WalkonNet其它相關文章!

推薦閱讀: