[Серия] — промежуточное ПО маршрутизации go-gin-api — проверка подписи (7)

Go Gin

Обзор

Сначала синхронизируйте обзор проекта:

Была опубликована последняя статья, Routing Middleware - Jaeger Link Tracking (Actual Combat), реакция на статью была действительно неожиданной, также был перенаправлен публичный аккаунт "Go China", и многие друзья добавили меня для общения, назвав меня Богом, в факт Какой я бог, я просто практикую это локально. Я все еще новичок в использовании языка Go. Спасибо за вашу любовь здесь!

В этой статье мы делимся: промежуточное ПО для маршрутизации — проверка подписи.

Зачем использовать проверку подписи?

Излишне говорить, что это в основном для обеспечения безопасности интерфейса и идентификации личности вызывающего абонента.Основываясь на этих двух моментах, давайте вместе разработаем подпись.

Вызывающий абонент должен подать заявку на получение ключа приложения и секрета приложения. Ключ приложения используется для идентификации личности вызывающего абонента, а секрет приложения используется для шифрования и создания подписи.

Конечно, сгенерированная подпись также должна соответствовать следующим пунктам:

  • Изменяемость: подпись каждый раз должна быть разной.
  • Своевременность: своевременность каждого запроса, недействительного по истечении срока действия.
  • Уникальность: Каждая подпись уникальна.
  • Целостность: входящие данные могут быть проверены для предотвращения вмешательства.

Например:

/api?param_1=xxx&param_2=xxx, где param_1 и param_2 — два параметра.

Если добавлена ​​проверка подписи, необходимо передать еще несколько параметров:

  • ak представляет собой ключ приложения, который используется для идентификации вызывающего абонента.
  • ts представляет отметку времени, которая используется для проверки своевременности интерфейса.
  • sn представляет зашифрованную строку подписи, которая используется для проверки целостности данных и предотвращения подделки данных.

sn зашифрован с помощью секрета приложения и переданных параметров.

Окончательные передаваемые параметры следующие:

/api?param_1=xxx&param_2=xxx&ak=xxx&ts=xxx&sn=xxx

Вот отладочный трюк, слишком хлопотно вручную генерировать параметры ts и sn каждый раз при передачеdebug=1Когда ts и sn будут возвращены, это будет ясно, если вы посмотрите на код.

В этой статье представлены три способа реализации подписей, а именно: комбинированное шифрование MD5, симметричное шифрование AES и асимметричное шифрование RSA.

Без лишних слов, давайте перейдем к теме.

MD5 комбинация

Создать подпись

Сначала оберните метод Go MD5:

func MD5(str string) string {
	s := md5.New()
	s.Write([]byte(str))
	return hex.EncodeToString(s.Sum(nil))
}

Чтобы зашифровать:

appKey     = "demo"
appSecret  = "xxx"
encryptStr = "param_1=xxx&param_2=xxx&ak="+appKey+"&ts=xxx"

// 自定义验证规则
sn = MD5(appSecret + encryptStr + appSecret)

Подтвердить подпись

При передаче параметров подпись генерируется повторно, если переданная подпись сравнивается с сгенерированной подписью.

То же самое, это означает, что проверка подписи прошла успешно.

отличается, что указывает на то, что проверка подписи не удалась.

Промежуточное ПО — реализация кода

var AppSecret string

// MD5 组合加密
func SetUp() gin.HandlerFunc {

	return func(c *gin.Context) {
		utilGin := util.Gin{Ctx: c}

		sign, err := verifySign(c)

		if sign != nil {
			utilGin.Response(-1, "Debug Sign", sign)
			c.Abort()
			return
		}

		if err != nil {
			utilGin.Response(-1, err.Error(), sign)
			c.Abort()
			return
		}

		c.Next()
	}
}

// 验证签名
func verifySign(c *gin.Context) (map[string]string, error) {
	_ = c.Request.ParseForm()
	req   := c.Request.Form
	debug := strings.Join(c.Request.Form["debug"], "")
	ak    := strings.Join(c.Request.Form["ak"], "")
	sn    := strings.Join(c.Request.Form["sn"], "")
	ts    := strings.Join(c.Request.Form["ts"], "")

	// 验证来源
	value, ok := config.ApiAuthConfig[ak]
	if ok {
		AppSecret = value["md5"]
	} else {
		return nil, errors.New("ak Error")
	}

	if debug == "1" {
		currentUnix := util.GetCurrentUnix()
		req.Set("ts", strconv.FormatInt(currentUnix, 10))
		res := map[string]string{
			"ts": strconv.FormatInt(currentUnix, 10),
			"sn": createSign(req),
		}
		return res, nil
	}

	// 验证过期时间
	timestamp := time.Now().Unix()
	exp, _    := strconv.ParseInt(config.AppSignExpiry, 10, 64)
	tsInt, _  := strconv.ParseInt(ts, 10, 64)
	if tsInt > timestamp || timestamp - tsInt >= exp {
		return nil, errors.New("ts Error")
	}

	// 验证签名
	if sn == "" || sn != createSign(req) {
		return nil, errors.New("sn Error")
	}

	return nil, nil
}

// 创建签名
func createSign(params url.Values) string {
	// 自定义 MD5 组合
	return util.MD5(AppSecret + createEncryptStr(params) + AppSecret)
}

func createEncryptStr(params url.Values) string {
	var key []string
	var str = ""
	for k := range params {
		if k != "sn" && k != "debug" {
			key = append(key, k)
		}
	}
	sort.Strings(key)
	for i := 0; i < len(key); i++ {
		if i == 0 {
			str = fmt.Sprintf("%v=%v", key[i], params.Get(key[i]))
		} else {
			str = str + fmt.Sprintf("&%v=%v", key[i], params.Get(key[i]))
		}
	}
	return str
}

Симметричное шифрование AES

Прежде чем использовать его, давайте сначала разберемся, что такое симметричное шифрование?

Симметричное шифрование заключается в использовании одного и того же ключа для шифрования и дешифрования.Этот метод называется симметричным шифрованием.

Обычно используемые алгоритмы: DES, AES.

Среди них AES — это обновленная версия DES с большей длиной ключа, большим выбором, большей гибкостью, более высокой безопасностью и более высокой скоростью.Давайте начнем шифрование AES напрямую.

преимущество

Алгоритм открытый, объем вычислений небольшой, скорость шифрования высокая, эффективность шифрования высокая.

недостаток

Отправитель и получатель должны согласовать ключ, и тогда обе стороны могут сохранить ключ, и управление ключами становится бременем обеих сторон.

Сценарии применения

Объем данных или шифрование критических данных относительно велик.

Создать подпись

Во-первых, инкапсулируйте метод шифрования Go AesEncrypt и метод дешифрования AesDecrypt.

// 加密 aes_128_cbc
func AesEncrypt (encryptStr string, key []byte, iv string) (string, error) {
	encryptBytes := []byte(encryptStr)
	block, err   := aes.NewCipher(key)
	if err != nil {
		return "", err
	}

	blockSize := block.BlockSize()
	encryptBytes = pkcs5Padding(encryptBytes, blockSize)

	blockMode := cipher.NewCBCEncrypter(block, []byte(iv))
	encrypted := make([]byte, len(encryptBytes))
	blockMode.CryptBlocks(encrypted, encryptBytes)
	return base64.URLEncoding.EncodeToString(encrypted), nil
}

// 解密
func AesDecrypt (decryptStr string, key []byte, iv string) (string, error) {
	decryptBytes, err := base64.URLEncoding.DecodeString(decryptStr)
	if err != nil {
		return "", err
	}

	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}

	blockMode := cipher.NewCBCDecrypter(block, []byte(iv))
	decrypted := make([]byte, len(decryptBytes))

	blockMode.CryptBlocks(decrypted, decryptBytes)
	decrypted = pkcs5UnPadding(decrypted)
	return string(decrypted), nil
}

func pkcs5Padding (cipherText []byte, blockSize int) []byte {
	padding := blockSize - len(cipherText)%blockSize
	padText := bytes.Repeat([]byte{byte(padding)}, padding)
	return append(cipherText, padText...)
}

func pkcs5UnPadding (decrypted []byte) []byte {
	length := len(decrypted)
	unPadding := int(decrypted[length-1])
	return decrypted[:(length - unPadding)]
}

Чтобы зашифровать:

appKey     = "demo"
appSecret  = "xxx"
encryptStr = "param_1=xxx&param_2=xxx&ak="+appKey+"&ts=xxx"

sn = AesEncrypt(encryptStr, appSecret)

Подтвердить подпись

decryptStr = AesDecrypt(sn, app_secret)

Сравните зашифрованную строку с расшифрованной строкой.

Если то же самое, проверка подписи прошла успешно.

отличается, что указывает на то, что проверка подписи не удалась.

Промежуточное ПО — реализация кода

var AppSecret string

// AES 对称加密
func SetUp() gin.HandlerFunc {

	return func(c *gin.Context) {
		utilGin := util.Gin{Ctx: c}

		sign, err := verifySign(c)

		if sign != nil {
			utilGin.Response(-1, "Debug Sign", sign)
			c.Abort()
			return
		}

		if err != nil {
			utilGin.Response(-1, err.Error(), sign)
			c.Abort()
			return
		}

		c.Next()
	}
}

// 验证签名
func verifySign(c *gin.Context) (map[string]string, error) {
	_ = c.Request.ParseForm()
	req   := c.Request.Form
	debug := strings.Join(c.Request.Form["debug"], "")
	ak    := strings.Join(c.Request.Form["ak"], "")
	sn    := strings.Join(c.Request.Form["sn"], "")
	ts    := strings.Join(c.Request.Form["ts"], "")

	// 验证来源
	value, ok := config.ApiAuthConfig[ak]
	if ok {
		AppSecret = value["aes"]
	} else {
		return nil, errors.New("ak Error")
	}

	if debug == "1" {
		currentUnix := util.GetCurrentUnix()
		req.Set("ts", strconv.FormatInt(currentUnix, 10))

		sn, err := createSign(req)
		if err != nil {
			return nil, errors.New("sn Exception")
		}

		res := map[string]string{
			"ts": strconv.FormatInt(currentUnix, 10),
			"sn": sn,
		}
		return res, nil
	}

	// 验证过期时间
	timestamp := time.Now().Unix()
	exp, _    := strconv.ParseInt(config.AppSignExpiry, 10, 64)
	tsInt, _  := strconv.ParseInt(ts, 10, 64)
	if tsInt > timestamp || timestamp - tsInt >= exp {
		return nil, errors.New("ts Error")
	}

	// 验证签名
	if sn == "" {
		return nil, errors.New("sn Error")
	}

	decryptStr, decryptErr := util.AesDecrypt(sn, []byte(AppSecret), AppSecret)
	if decryptErr != nil {
		return nil, errors.New(decryptErr.Error())
	}
	if decryptStr != createEncryptStr(req) {
		return nil, errors.New("sn Error")
	}
	return nil, nil
}

// 创建签名
func createSign(params url.Values) (string, error) {
	return util.AesEncrypt(createEncryptStr(params), []byte(AppSecret), AppSecret)
}

func createEncryptStr(params url.Values) string {
	var key []string
	var str = ""
	for k := range params {
		if k != "sn" && k != "debug" {
			key = append(key, k)
		}
	}
	sort.Strings(key)
	for i := 0; i < len(key); i++ {
		if i == 0 {
			str = fmt.Sprintf("%v=%v", key[i], params.Get(key[i]))
		} else {
			str = str + fmt.Sprintf("&%v=%v", key[i], params.Get(key[i]))
		}
	}
	return str
}

Асимметричное шифрование RSA

Как и выше, прежде чем использовать его, давайте сначала разберемся, что такое асимметричное шифрование?

Для асимметричного шифрования требуется два ключа для шифрования и дешифрования. Два ключа — открытый ключ и закрытый ключ.Этот метод называется асимметричным шифрованием.

Обычно используемый алгоритм: RSA.

преимущество

По сравнению с симметричным шифрованием безопасность выше.Шифрование и дешифрование требуют разных ключей, и как открытый, так и закрытый ключи могут быть зашифрованы и расшифрованы друг с другом.

недостаток

Шифрование и дешифрование отнимают много времени и медленны, и подходят только для шифрования небольшого объема данных.

Сценарии применения

Он подходит для сценариев с высокими требованиями к безопасности, подходит для шифрования небольшого объема данных, таких как платежные данные, данные для входа и т. д.

Создать подпись

Во-первых, инкапсулируйте метод шифрования с открытым ключом GO RSAPUBLICENCRYPT и метод дешифрования RSAPRIVATECRYPT.

// 公钥加密
func RsaPublicEncrypt(encryptStr string, path string) (string, error) {
	// 打开文件
	file, err := os.Open(path)
	if err != nil {
		return "", err
	}
	defer file.Close()

	// 读取文件内容
	info, _ := file.Stat()
	buf := make([]byte,info.Size())
	file.Read(buf)

	// pem 解码
	block, _ := pem.Decode(buf)

	// x509 解码
	publicKeyInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return "", err
	}

	// 类型断言
	publicKey := publicKeyInterface.(*rsa.PublicKey)

	//对明文进行加密
	encryptedStr, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, []byte(encryptStr))
	if err != nil {
		return "", err
	}

	//返回密文
	return base64.URLEncoding.EncodeToString(encryptedStr), nil
}

// 私钥解密
func RsaPrivateDecrypt(decryptStr string, path string) (string, error) {
	// 打开文件
	file, err := os.Open(path)
	if err != nil {
		return "", err
	}
	defer file.Close()

	// 获取文件内容
	info, _ := file.Stat()
	buf := make([]byte,info.Size())
	file.Read(buf)

	// pem 解码
	block, _ := pem.Decode(buf)

	// X509 解码
	privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err != nil {
		return "", err
	}
	decryptBytes, err := base64.URLEncoding.DecodeString(decryptStr)

	//对密文进行解密
	decrypted, _ := rsa.DecryptPKCS1v15(rand.Reader,privateKey,decryptBytes)

	//返回明文
	return string(decrypted), nil
}

Вызывающий запрашивает открытый ключ и шифрует его:

appKey     = "demo"
appSecret  = "公钥"
encryptStr = "param_1=xxx&param_2=xxx&ak="+appKey+"&ts=xxx"

sn = RsaPublicEncrypt(encryptStr, appSecret)

Подтвердить подпись

decryptStr = RsaPrivateDecrypt(sn, app_secret)

Сравните зашифрованную строку с расшифрованной строкой.

Если то же самое, проверка подписи прошла успешно.

отличается, что указывает на то, что проверка подписи не удалась.

Промежуточное ПО — реализация кода

var AppSecret string

// RSA 非对称加密
func SetUp() gin.HandlerFunc {

	return func(c *gin.Context) {
		utilGin := util.Gin{Ctx: c}

		sign, err := verifySign(c)

		if sign != nil {
			utilGin.Response(-1, "Debug Sign", sign)
			c.Abort()
			return
		}

		if err != nil {
			utilGin.Response(-1, err.Error(), sign)
			c.Abort()
			return
		}

		c.Next()
	}
}

// 验证签名
func verifySign(c *gin.Context) (map[string]string, error) {
	_ = c.Request.ParseForm()
	req   := c.Request.Form
	debug := strings.Join(c.Request.Form["debug"], "")
	ak    := strings.Join(c.Request.Form["ak"], "")
	sn    := strings.Join(c.Request.Form["sn"], "")
	ts    := strings.Join(c.Request.Form["ts"], "")

	// 验证来源
	value, ok := config.ApiAuthConfig[ak]
	if ok {
		AppSecret = value["rsa"]
	} else {
		return nil, errors.New("ak Error")
	}

	if debug == "1" {
		currentUnix := util.GetCurrentUnix()
		req.Set("ts", strconv.FormatInt(currentUnix, 10))

		sn, err := createSign(req)
		if err != nil {
			return nil, errors.New("sn Exception")
		}

		res := map[string]string{
			"ts": strconv.FormatInt(currentUnix, 10),
			"sn": sn,
		}
		return res, nil
	}

	// 验证过期时间
	timestamp := time.Now().Unix()
	exp, _    := strconv.ParseInt(config.AppSignExpiry, 10, 64)
	tsInt, _  := strconv.ParseInt(ts, 10, 64)
	if tsInt > timestamp || timestamp - tsInt >= exp {
		return nil, errors.New("ts Error")
	}

	// 验证签名
	if sn == "" {
		return nil, errors.New("sn Error")
	}

	decryptStr, decryptErr := util.RsaPrivateDecrypt(sn, config.AppRsaPrivateFile)
	if decryptErr != nil {
		return nil, errors.New(decryptErr.Error())
	}
	if decryptStr != createEncryptStr(req) {
		return nil, errors.New("sn Error")
	}
	return nil, nil
}

// 创建签名
func createSign(params url.Values) (string, error) {
	return util.RsaPublicEncrypt(createEncryptStr(params), AppSecret)
}

func createEncryptStr(params url.Values) string {
	var key []string
	var str = ""
	for k := range params {
		if k != "sn" && k != "debug" {
			key = append(key, k)
		}
	}
	sort.Strings(key)
	for i := 0; i < len(key); i++ {
		if i == 0 {
			str = fmt.Sprintf("%v=%v", key[i], params.Get(key[i]))
		} else {
			str = str + fmt.Sprintf("&%v=%v", key[i], params.Get(key[i]))
		}
	}
	return str
}

Как звонить?

Как и другие методы вызова промежуточного программного обеспечения, вы можете свободно выбирать в соответствии со своими потребностями.

Например, используя композицию MD5:

.Use(sign_md5.SetUp())

Используйте симметричное шифрование AES:

.Use(sign_aes.SetUp())

Используйте асимметричное шифрование RSA:

.Use(sign_rsa.SetUp())

Тестирование производительности

Поскольку RSA асимметричное шифрование является наиболее безопасным, оно должно использоваться равномерно.

НЕТ! НЕТ! НЕТ! Никогда!

Почему я должен волноваться, ведь я уже сталкивался с этой ямой, это урок слез ...

Давайте проверим производительность один за другим:

MD5

func Md5Test(c *gin.Context) {
	startTime  := time.Now()
	appSecret  := "IgkibX71IEf382PT"
	encryptStr := "param_1=xxx&param_2=xxx&ak=xxx&ts=1111111111"
	count      := 1000000
	for i := 0; i < count; i++ {
		// 生成签名
		util.MD5(appSecret + encryptStr + appSecret)

		// 验证签名
		util.MD5(appSecret + encryptStr + appSecret)
	}
	utilGin := util.Gin{Ctx: c}
	utilGin.Response(1, fmt.Sprintf("%v次 - %v", count, time.Since(startTime)), nil)
}

Имитация одного миллиона запросов, а время выполнения составляет около 1,1 ~ 1,2 с.

AES

func AesTest(c *gin.Context) {
	startTime  := time.Now()
	appSecret  := "IgkibX71IEf382PT"
	encryptStr := "param_1=xxx&param_2=xxx&ak=xxx&ts=1111111111"
	count      := 1000000
	for i := 0; i < count; i++ {
		// 生成签名
		sn, _ := util.AesEncrypt(encryptStr, []byte(appSecret), appSecret)

		// 验证签名
		util.AesDecrypt(sn, []byte(appSecret), appSecret)
	}
	utilGin := util.Gin{Ctx: c}
	utilGin.Response(1, fmt.Sprintf("%v次 - %v", count, time.Since(startTime)), nil)
}

Имитация одного миллиона запросов, а время выполнения составляет около 1,8 ~ 1,9 с.

RSA

func RsaTest(c *gin.Context) {
	startTime  := time.Now()
	encryptStr := "param_1=xxx&param_2=xxx&ak=xxx&ts=1111111111"
	count      := 500
	for i := 0; i < count; i++ {
		// 生成签名
		sn, _ := util.RsaPublicEncrypt(encryptStr, "rsa/public.pem")

		// 验证签名
		util.RsaPrivateDecrypt(sn, "rsa/private.pem")
	}
	utilGin := util.Gin{Ctx: c}
	utilGin.Response(1, fmt.Sprintf("%v次 - %v", count, time.Since(startTime)), nil)
}

Я не рискну моделировать миллион запросов, и не знаю, когда это будет сделано, давайте смоделируем 500 раз.

Имитировать 500 запросов, а время выполнения составляет около 1 с.

Вышеуказанное является моим локальным реализацией результатов, вы можете подвергнуть сомнению мою плохую производительность компьютеров, методы упаковки имеют проблему ...

Вы также можете попробовать и посмотреть, настолько ли велик разрыв в производительности.

Как взаимодействуют методы шифрования PHP и Go?

Я пишу PHP, можно ли реализовать метод генерации подписей в PHP?

Конечно может!

Я также реализовал вышеперечисленные 3 метода с помощью PHP, могут быть небольшие корректировки, но в целом проблема невелика, соответствующая демонстрация была загружена на github:

GitHub.com/Бессердечный не…

Хорошо, это все.

Адрес источника

GitHub.com/Бессердечный не…

серия статей go-gin-api