前言
一般的对外服务都是需要服务器支持
https
传输的,那既然有了
https
,数据已经加密了,为什么还要做这个事情呢?
现在大多数应用服务都是使用的前后端分离的方式来开发的,以后端提供的接口来进行业务数据交互。相信有过
web
开发经验的都打开过浏览器的调试控制台,在
Network
中能够看到当前页面发过哪些请求,而且能够看到请求的参数和返回值。
这里就是希望我们的传输/返回数据不被别人看到,直接给参数和返回值加密,主要不是为了防止数据传输过程中被第三方拦截而做的,而是为了防止不法分子调试获取到数据,对服务器进行恶意攻击。
加解密
简单说一下加解密相关的知识,本篇我们只需要了解下
对称加密
和
非对称加密
即可:
-
对称加密:只有一个密钥,使用同一个密钥对数据加解密,常见的有
DES
,
AES
等。 -
非对称加密:两个密钥,一个公钥,一个私钥,公钥用于加密数据,私钥解密数据。常常将公钥发给前端,私钥保存在后端(一定不能泄露),例如
RSA
,
DSA
等。
思路
这里提供两种思路供大家参考,相应的变种很多,可以自己根据需求实现。
第一版
客户端服务端使用对称加密传输数据,客户端服务端定好传输密钥,直接写死在代码里。
客户端加密
url
的
query
参数(即问号后面那串
id=xxx&name=xxx
)或者传输的
body
内容(序列化后加密传输),服务端进行相应的解密操作。
模拟一下客户端服务器的代码,对称加密算法使用
AES
,密钥为
4335dfgeredhfdsd
。
服务端使用
gin
框架中添加中间件
middlewareDecryptReq
,用来解析客户端加密后的数据,并使用统一的数据返回入口
EncryptWriter
函数来做数据加密返回。
示例代码
如下:
package main
import (
"bytes"
"encoding/base64"
"fmt"
"github.com/duke-git/lancet/v2/cryptor"
"github.com/gin-gonic/gin"
"io"
"io/ioutil"
"net/http"
)
var (
aesKey = []byte("4335dfgeredhfdsd")
)
func main() {
engine := gin.Default()
engine.Use(middlewareDecryptReq())
engine.GET("/g", func(c *gin.Context) {
EncryptWriter(c, []byte(c.Request.URL.RawQuery))
})
engine.POST("/p", func(c *gin.Context) {
buf, err := c.GetRawData()
if err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
EncryptWriter(c, buf)
})
engine.Run(":4780")
}
func middlewareDecryptReq() func(c *gin.Context) {
return func(c *gin.Context) {
if c.Request.URL.RawQuery != "" {
res, err := AesCbcDecryptBase64([]byte(c.Request.URL.RawQuery), aesKey)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
c.Abort()
return
}
c.Request.URL.RawQuery = string(res)
}
data, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
c.Abort()
return
}
defer c.Request.Body.Close()
if len(data) == 0 {
c.Next()
return
}
plainBuf, err := AesCbcDecryptBase64(data, aesKey)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
c.Abort()
return
}
r := bytes.NewBuffer(plainBuf)
rd := io.NopCloser(r)
c.Request.Body = rd
}
}
func EncryptWriter(c *gin.Context, data []byte) {
cipherBuf := AesCbcEncryptBase64(data, aesKey)
c.String(http.StatusOK, string(cipherBuf))
}
func AesCbcEncrypt(plainText, secretKey []byte) []byte {
return cryptor.AesCbcEncrypt(plainText, secretKey)
}
func AesCbcDecrypt(cipherText, key []byte) []byte {
return cryptor.AesCbcDecrypt(cipherText, key)
}
func AesCbcEncryptBase64(plainText, secretKey []byte) (cipherTextBase64 []byte) {
encryBytes := AesCbcEncrypt(plainText, secretKey)
cipherTextBase64 = make([]byte, base64.StdEncoding.EncodedLen(len(encryBytes)))
base64.StdEncoding.Encode(cipherTextBase64, encryBytes)
return
}
func AesCbcDecryptBase64(cipherTextBase64, key []byte) (res []byte, err error) {
plainTextBytes := make([]byte, base64.StdEncoding.DecodedLen(len(cipherTextBase64)))
n, err := base64.StdEncoding.Decode(plainTextBytes, cipherTextBase64)
if err != nil {
return
}
res = AesCbcDecrypt(plainTextBytes[:n], key)
return
}
func tServerGet() {
queryData := []byte("id=xxx&name=xxx")
cipherBuf := AesCbcEncryptBase64(queryData, aesKey)
resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:4780/g?%s", string(cipherBuf)))
if err != nil {
fmt.Println(err)
return
}
fmt.Println(resp.StatusCode)
buf, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("原始数据:", string(buf))
plainBuf, err := AesCbcDecryptBase64(buf, aesKey)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("解密成功:", string(plainBuf))
}
func tServerPost() {
data := []byte(`{"id":"xxx","name":"法外狂徒"}`)
cipherBuf := AesCbcEncryptBase64(data, aesKey)
resp, err := http.Post("http://127.0.0.1:4780/p", "application/json", bytes.NewReader(cipherBuf))
if err != nil {
fmt.Println(err)
return
}
fmt.Println(resp.StatusCode)
buf, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("原始数据:", string(buf))
plainBuf, err := AesCbcDecryptBase64(buf, aesKey)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("解密成功:", string(plainBuf))
}
缺点:
- 客户端密钥容易找,拿到了密钥,那也就能对数据进行加解密操作,可以篡改数据内容。
- 密钥写死在代码里,如果要改个密钥比较麻烦。
第二版
在第一版的基础上,添加了一些复杂性。借鉴了
https
的实现原理,同时使用
非对称加密
和
对称加密
。
客户端先向服务器要
非对称加密公钥
,然后根据时间戳或者其他算法生成
对称加密密钥
,然后将这个
对称加密密钥
使用
非对称加密公钥
加密后传给服务器。
服务器解密后,再将返回数据使用
对称加密密钥
加密后返回给客户端,客户端再解密获取数据。
示例代码
如下:
package main
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"github.com/duke-git/lancet/v2/cryptor"
"github.com/gin-gonic/gin"
"io"
"io/ioutil"
"net/http"
"sync"
)
var (
onceGenerateKey sync.Once
rsaPubKey []byte
rsaPrivateKey []byte
)
const ginAesKey = "ginAesKey"
func main() {
engine := gin.Default()
engine.GET("/rsa_key", func(c *gin.Context) {
onceGenerateKey.Do(func() {
private, pub, er := GenRsaKey()
if er != nil {
fmt.Println(er)
return
}
rsaPubKey = pub
rsaPrivateKey = private
})
c.String(http.StatusOK, string(rsaPubKey))
})
cryptRouter := engine.Group("", middlewareDecryptReq())
cryptRouter.GET("/g", func(c *gin.Context) {
EncryptWriter(c, []byte(c.Request.URL.RawQuery))
})
cryptRouter.POST("/p", func(c *gin.Context) {
buf, err := c.GetRawData()
if err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
EncryptWriter(c, buf)
})
engine.Run(":4780")
}
func middlewareDecryptReq() func(c *gin.Context) {
return func(c *gin.Context) {
// aes 密钥放在 query 参数里,没有值直接报错
if c.Request.URL.RawQuery == "" {
c.String(http.StatusBadRequest, "参数错误")
c.Abort()
return
}
res, err := RsaDecryptBase64([]byte(c.Request.URL.RawQuery), rsaPrivateKey)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
c.Abort()
return
}
c.Request.URL.RawQuery = string(res)
ak := c.Query("aesKey")
if ak == "" {
c.String(http.StatusBadRequest, "参数错误")
c.Abort()
return
}
c.Set(ginAesKey, ak)
data, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
c.Abort()
return
}
defer c.Request.Body.Close()
if len(data) == 0 {
c.Next()
return
}
plainBuf, err := RsaDecryptBase64(data, rsaPrivateKey)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
c.Abort()
return
}
rd := io.NopCloser(bytes.NewBuffer(plainBuf))
c.Request.Body = rd
}
}
func EncryptWriter(c *gin.Context, data []byte) {
cipherBuf := AesCbcEncryptBase64(data, []byte(c.GetString(ginAesKey)))
c.String(http.StatusOK, string(cipherBuf))
}
func Base64Encrypt(data []byte) []byte {
res := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
base64.StdEncoding.Encode(res, data)
return res
}
func Base64Decrypt(data []byte) ([]byte, error) {
res := make([]byte, base64.StdEncoding.DecodedLen(len(data)))
n, err := base64.StdEncoding.Decode(res, data)
return res[:n], err
}
func AesCbcEncrypt(plainText, secretKey []byte) []byte {
return cryptor.AesCbcEncrypt(plainText, secretKey)
}
func AesCbcDecrypt(cipherText, key []byte) []byte {
return cryptor.AesCbcDecrypt(cipherText, key)
}
func AesCbcEncryptBase64(plainText, secretKey []byte) (cipherTextBase64 []byte) {
encryptBytes := AesCbcEncrypt(plainText, secretKey)
return Base64Encrypt(encryptBytes)
}
func AesCbcDecryptBase64(cipherTextBase64, key []byte) (res []byte, err error) {
plainTextBytes, err := Base64Decrypt(cipherTextBase64)
if err != nil {
return
}
res = AesCbcDecrypt(plainTextBytes, key)
return
}
func GenRsaKey() (prvkey, pubkey []byte, err error) {
// 生成私钥文件
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
return
}
derStream := x509.MarshalPKCS1PrivateKey(privateKey)
block := &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: derStream,
}
prvkey = pem.EncodeToMemory(block)
publicKey := &privateKey.PublicKey
derPkix, err := x509.MarshalPKIXPublicKey(publicKey)
if err != nil {
return
}
block = &pem.Block{
Type: "RSA PUBLIC KEY",
Bytes: derPkix,
}
pubkey = pem.EncodeToMemory(block)
return
}
// 公钥加密
func RsaEncrypt(data, keyBytes []byte) ([]byte, error) {
//解密pem格式的公钥
block, _ := pem.Decode(keyBytes)
if block == nil {
return nil, errors.New("public key error")
}
// 解析公钥
pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
// 类型断言
pub := pubInterface.(*rsa.PublicKey)
//加密
ciphertext, err := rsa.EncryptPKCS1v15(rand.Reader, pub, data)
if err != nil {
return nil, err
}
return ciphertext, nil
}
func RsaEncryptBase64(data, keyBytes []byte) ([]byte, error) {
encryptBuf, err := RsaEncrypt(data, keyBytes)
if err != nil {
return nil, err
}
res := Base64Encrypt(encryptBuf)
return res, nil
}
// 私钥解密
func RsaDecrypt(ciphertext, keyBytes []byte) ([]byte, error) {
//获取私钥
block, _ := pem.Decode(keyBytes)
if block == nil {
return nil, errors.New("private key error!")
}
//解析PKCS1格式的私钥
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
// 解密
data, err := rsa.DecryptPKCS1v15(rand.Reader, priv, ciphertext)
if err != nil {
return nil, err
}
return data, nil
}
func RsaDecryptBase64(ciphertext, keyBytes []byte) ([]byte, error) {
buf, err := Base64Decrypt(ciphertext)
if err != nil {
return nil, err
}
return RsaDecrypt(buf, keyBytes)
}
func serverGetRsaKey() (pubKey []byte, err error) {
resp, err := http.Get("http://127.0.0.1:4780/rsa_key")
if err != nil {
return
}
return io.ReadAll(resp.Body)
}
func tServerGet() {
pubKey, err := serverGetRsaKey()
if err != nil {
fmt.Println(err)
return
}
//随机生成 aes key
aes_key := "4335dfgeresdheud"
queryData := []byte("id=xxx&name=xxx&aesKey=" + aes_key)
cipherBuf, err := RsaEncryptBase64(queryData, pubKey)
resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:4780/g?%s", string(cipherBuf)))
if err != nil {
fmt.Println(err)
return
}
fmt.Println(resp.StatusCode)
buf, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("原始数据:", string(buf))
plainBuf, err := AesCbcDecryptBase64(buf, []byte(aes_key))
if err != nil {
fmt.Println(err)
return
}
fmt.Println("解密成功:", string(plainBuf))
}
func tServerPost() {
pubKey, err := serverGetRsaKey()
if err != nil {
fmt.Println(err)
return
}
//随机生成 aes key
aes_key := "4335dfgeresdheud"
queryCipher, err := RsaEncryptBase64([]byte("aesKey="+aes_key), pubKey)
if err != nil {
fmt.Println(err)
return
}
data := []byte(`{"id":"xxx","name":"法外狂徒"}`)
cipherBuf, err := RsaEncryptBase64(data, pubKey)
if err != nil {
fmt.Println(err)
return
}
resp, err := http.Post("http://127.0.0.1:4780/p?"+string(queryCipher), "application/json", bytes.NewReader(cipherBuf))
if err != nil {
fmt.Println(err)
return
}
fmt.Println(resp.StatusCode)
buf, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("原始数据:", string(buf))
plainBuf, err := AesCbcDecryptBase64(buf, []byte(aes_key))
if err != nil {
fmt.Println(err)
return
}
fmt.Println("解密成功:", string(plainBuf))
}
这样,就算攻击者获取到密钥了,也不能解密数据,但是还是防止不了反编译代码,打断点调试获取数据。
攻击者也可以自己在中间模拟生成
非对称加密密钥
,当客户端发起请求获取服务器
公钥
时,可以将自己模拟的
公钥
发给客户端,保存下服务器传回的
公钥
。然后在客户端发起请求时,可以先用自己生成的
私钥
解密数据,然后再篡改数据,使用刚刚保存的服务器
公钥
加密后传给客户端。
前端
前端
js
可以使用
jsencrypt
库,本篇不做详细介绍了。相关的客户端加密解密代码都用
go
实现了,贴在上面的 示例代码 的末尾。
总结
这里做的加密传输,
仅仅只能添加破解的复杂性,不能真的保证数据不泄露
。
所以一般的服务在做数据交互时也并不会刻意去做这种前后端的数据加密,一般性的是做服务器的
session
或者客户端的
cookie
校验,来保证
数据不泄露,不被篡改
。
但你要是说我做
session
校验的
token
在前端泄露了,那这种也是用户自己的问题(进入黑客网站或其他做了鉴权),服务防不了这种,顶多是增加校验复杂性来让其更加繁琐。所以说在类似付款的操作的时候,都会再进行一次校验(输入密码/验证指纹等),来再做一次权限校验。
本篇前后端做数据加密,主要的一个场景就是:我有一个平台服务,这时候我又做了一个配套的单机应用,单机应用是可以随意给其他人使用的,为了保证我的软件不被别人轻易的破解获取数据而做的一层防护。