• go GCM加密解密 gin中间件加密解密,gin文件流处理


    要给已有的系统启用加密解密,目前推荐的是aes的gcm模式的加密和解密,在微服务如果向前有公共方法处理 读取数据和写返回数据,那么比较简单,修改以前的公共方法,但是这样本地调试平时肯定是明文,所以要加判断,如果以前的读数据和写数据是五花八门那就比较麻烦,在微服务体系里面一般有网关这个服务,所以加密和解密就放在网关服务,大致如下:

     常规的请求有GET,POST JSON, POST file,以及POST Form表单,返回一般是json 或者下载文件流,所以我们需要截获请求流和返回流,收到请求流解密数据 然后重新写入到请求流,收到返回流加密数据,重写返回流。

    首先来看aes加密和解密程序aes.go

    package aes
    
    import (
        "crypto/aes"
        "crypto/cipher"
        "crypto/md5"
        "crypto/rand"
        "encoding/base64"
        "encoding/hex"
        "errors"
        "io"
    )
    
    //加密字符串
    func GcmEncrypt(key, plaintext string) (string, error) {
        if len(key) != 32 && len(key) != 24 && len(key) != 16 {
            return "", errors.New("the length of key is error")
        }
    
        if len(plaintext) < 1 {
            return "", errors.New("plaintext is null")
        }
    
        keyByte := []byte(key)
        plainByte:=[]byte(plaintext)
    
        block, err := aes.NewCipher(keyByte)
        if err != nil {
            return "", err
        }
    
        aesGcm, err := cipher.NewGCM(block)
        if err != nil {
            return "", err
        }
    
        nonce := make([]byte, 12)
        if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
            return "", err
        }
    
        seal := aesGcm.Seal(nonce, nonce, plainByte, nil)
        return base64.URLEncoding.EncodeToString(seal), nil
    }
    
    //解密字符串
    func GcmDecrypt(key, cipherText string) (string, error) {
        if len(key) != 32 && len(key) != 24 && len(key) != 16 {
            return "", errors.New("the length of key is error")
        }
    
        if len(cipherText) < 1 {
            return "", errors.New("cipherText is null")
        }
    
        cipherByte, err := base64.URLEncoding.DecodeString(cipherText)
        if err != nil {
            return "", err
        }
    
        if len(cipherByte) < 12 {
            return "", errors.New("cipherByte is error")
        }
    
        nonce, cipherByte := cipherByte[:12], cipherByte[12:]
    
        keyByte := []byte(key)
        block, err := aes.NewCipher(keyByte)
        if err != nil {
            return "", err
        }
    
        aesGcm, err := cipher.NewGCM(block)
        if err != nil {
            return "", err
        }
    
        plainByte, err := aesGcm.Open(nil, nonce, cipherByte, nil)
        if err != nil {
            return "", err
        }
    
        return string(plainByte), nil
    }
    
    //生成32位md5字串
    func GetAesKey(s string) string {
            h := md5.New()
            h.Write([]byte(s))
            return hex.EncodeToString(h.Sum(nil))
    
    }

    再来看看网关转发程序proxy.go

    package middleware
    
    import (
        "fmt"
        "github.com/gin-gonic/gin"
        "github.com/valyala/fasthttp"
        "io/ioutil"
        "runtime/debug"
        "time"
    )
    
    var fastClient *fasthttp.Client
    
    func init() {
        fastClient = &fasthttp.Client{}
        fastClient.MaxIdemponentCallAttempts = 1
        fastClient.ReadTimeout = time.Second * 60
    }
    
    func GetHttpClient() *fasthttp.Client {
        return fastClient
    }
    func GateWay() gin.HandlerFunc {
        return func(c *gin.Context) {
            defer func() {
                if e := recover(); e != nil {
                    stack := debug.Stack()
                    log("GateWay Recovery: err:%v, stack:%v", e, string(stack))
                }
            }()
    
            err := Forward(c)
            if err != nil {
                response(c, 9999, "系统错误", err.Error())
            }
            return
        }
    }
    
    func Forward(ctx *gin.Context) error {
        req := &fasthttp.Request{}
    
        //请求-获取服务地址
        host := "http://localhost:8000/" + ctx.Request.URL.String()
        //请求-url
        req.SetRequestURI(host)
    
        //请求-header
        for k, v := range ctx.Request.Header {
            req.Header.Set(k, v[0])
        }
    
        //请求-body
        data, err := ioutil.ReadAll(ctx.Request.Body)
        if err != nil {
            log("Forward err:%v", err)
            return fmt.Errorf("系统错误")
        }
        req.SetBody(data)
    
        //请求-方法
        req.Header.SetMethod(ctx.Request.Method)
    
        //请求-发送
        resp := &fasthttp.Response{}
    
        //请求-新增调用链
        /*
            err = opentracing.GlobalTracer().Inject(
                opentracing.SpanFromContext(ctx.Request.Context()).Context(),
                opentracing.TextMap,
                HTTPHeadersCarrier{&req.Header},
            )
        */
    
        err = GetHttpClient().Do(req, resp)
        if err != nil {
            log("Forward GetHttpClient DO err:%v", err)
            return fmt.Errorf("系统错误")
        }
    
        //请求-响应
        ContentType := fmt.Sprintf("%s", resp.Header.Peek("Content-Type"))
        ctx.Data(resp.StatusCode(), ContentType, resp.Body())
    
        return nil
    }
    
    type HTTPHeadersCarrier struct {
        *fasthttp.RequestHeader
    }
    
    func (c HTTPHeadersCarrier) Set(key, val string) {
        h := c.RequestHeader
        h.Add(key, val)
    }

    最后来看一下gin的中间件crypto.go

    package middleware
    
    import (
        "bytes"
        "demo/aes"
        "encoding/json"
        "errors"
        "fmt"
        "github.com/gin-gonic/gin"
        "io"
        "io/ioutil"
        "mime"
        "mime/multipart"
        "net/url"
        "runtime/debug"
        "strconv"
        "strings"
    )
    
    type aesWriter struct {
        gin.ResponseWriter
        body *bytes.Buffer
    }
    
    func (w *aesWriter) Write(b []byte) (int, error) {
        return w.body.Write(b)
    }
    
    func (w *aesWriter) WriteString(s string) (int, error) {
        return w.body.WriteString(s)
    }
    
    //只有经过token 验证的才会加密 和解密
    //handleFile 表示是否处理上传文件, 默认网关不处理上传文件的encryptString数据, 如果处理会导致具体服务无法接收到具体参数
    func AesGcmDecrypt() gin.HandlerFunc {
        return func(c *gin.Context) {
            defer func() {
                if e := recover(); e != nil {
                    stack := debug.Stack()
                    log("AesGcmDecrypt Recovery: err:%v, stack:%v", e, string(stack))
                }
            }()
    
            if c.Request.Method == "OPTIONS" {
                c.Next()
            } else {
                md5key := aes.GetAesKey("gavin12345678")
                log("AesGcmDecrypt start url:%s  ,md5key:%s, Method:%s, Header:%+v", c.Request.URL.String(), md5key, c.Request.Method, c.Request.Header)
                handleAes(c, md5key)
            }
        }
    }
    
    //请求和返回都加密 解密
    func handleAes(c *gin.Context, md5key string) {
        contentType := c.Request.Header.Get("Content-Type")
        isJsonRequest := strings.Contains(contentType, "application/json")
        isFileRequest := strings.Contains(contentType, "multipart/form-data")
        isFormUrl := strings.Contains(contentType, "application/x-www-form-urlencoded")
    
        if c.Request.Method == "GET" {
            err := parseQuery(c, md5key)
            if err != nil {
                log("handleAes parseQuery  err:%v", err)
                //这里输出应该密文 一旦加密解密调试好 这里就不会走进来
                response(c, 2001, "系统错误", err.Error())
                return
            }
        } else if isJsonRequest {
            err := parseJson(c, md5key)
            if err != nil {
                log("handleAes parseJson err:%v", err)
                //这里输出应该密文 一旦加密解密调试好 这里就不会走进来
                response(c, 2001, "系统错误", err.Error())
                return
            }
        } else if isFormUrl {
            err := parseForm(c, md5key)
            if err != nil {
                log("handleAes parseForm err:%v", err)
                //这里输出应该密文 一旦加密解密调试好 这里就不会走进来
                response(c, 2001, "系统错误", err.Error())
                return
            }
        } else if isFileRequest {
            err := parseFile(c, md5key)
            if err != nil {
                log("handleAes parseFile err:%v", err)
                //这里输出应该密文 一旦加密解密调试好 这里就不会走进来
                response(c, 2001, "系统错误", err.Error())
                return
            }
        }
    
        ///截取 response body
        oldWriter := c.Writer
        blw := &aesWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
        c.Writer = blw
    
        // 走流程
        c.Next()
    
        ///获取返回数据
        responseByte := blw.body.Bytes()
    
        //日志
        c.Writer = oldWriter
        //如果返回的不是json格式 那么直接返回,应为文件下载之类的不应该加密
        if !isJsonResponse(c) {
            _, _ = c.Writer.Write(responseByte)
            return
        }
    
        ///加密
        encryptStr, err := aes.GcmEncrypt(md5key, string(responseByte))
        if err != nil {
            log("handleAes GcmEncrypt err:%v", err)
            response(c, 2001, "系统错误", err.Error())
            return
        }
    
        _, _ = c.Writer.WriteString(encryptStr)
    }
    
    //处理json
    func parseJson(c *gin.Context, md5key string) error {
        //读取数据 body处理
        payload, err := c.GetRawData()
        if err != nil {
            return err
        }
    
        ///解密body数据 请求的json是{"encryptString":{value}} value含有gcm的12字节nonce,实际长度大于32
        if payload != nil && len(payload) > 20 {
            var jsonData encryptJson
            log("AesGcmDecrypt  parseJson url:%s md5key:%s,old data:%s,", c.Request.URL.String(), md5key, string(payload))
    
            err := json.Unmarshal(payload, &jsonData)
            if err != nil {
                log("AesGcmDecrypt parseJson Unmarshal err:%v", err)
                return err
            }
    
            payloadText := jsonData.EncryptString
            if len(payloadText) > 0 {
                payloadText, err = aes.GcmDecrypt(md5key, payloadText)
                if err != nil {
                    log("AesGcmDecrypt parseJson GcmDecryptByte err:%v", err)
                    return err
                }
                payload = []byte(payloadText)
                log("AesGcmDecrypt  parseJson url:%s md5key:%s,encryptString:%s,decrypt data:%s", c.Request.URL.String(), md5key, jsonData.EncryptString, payloadText)
            }
        }
    
        c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(payload))
    
        return nil
    }
    
    func parseForm(c *gin.Context, md5key string) error {
        //读取数据 body处理
        payload, err := c.GetRawData()
        if err != nil {
            return err
        }
    
        ///解密body数据 请求的json是"encryptString= value含有gcm的12字节nonce,实际长度大于32
        if payload != nil && len(payload) > 20 {
            var jsonData encryptJson
            log("AesGcmDecrypt  parseForm url:%s md5key:%s,old data:%s,", c.Request.URL.String(), md5key, string(payload))
    
            values, err := url.ParseQuery(string(payload))
            if err != nil {
                log("AesGcmDecrypt parseForm ParseQuery err:%v", err)
                return err
            }
    
            payloadText := values.Get("encryptString")
            if len(payloadText) > 0 {
                mapData, err := gcmDecryptString(md5key, payloadText)
                if err != nil {
                    log("AesGcmDecrypt parseForm gcmDecryptString err:%v", err)
                    return err
                }
    
                for k, v := range mapData {
                    values.Add(k, getStr(v))
                }
    
                formData := values.Encode()
                log("AesGcmDecrypt  parseForm url:%s md5key:%s,encryptString:%s,decrypt data:%s", c.Request.URL.String(), md5key, jsonData.EncryptString, formData)
                payload = []byte(formData)
            }
        }
    
        c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(payload))
    
        return nil
    }
    
    //处理get url的解密
    func parseQuery(c *gin.Context, md5Key string) error {
        encryptString := c.Query("encryptString")
        log("AesGcmDecrypt parseQuery url:%s, md5key:%s, encryptString:%s", c.Request.URL.String(), md5Key, encryptString)
    
        if len(encryptString) < 1 {
            return nil
        }
    
        queryData, err := gcmDecryptString(md5Key, encryptString)
        if err != nil {
            return err
        }
    
        var args []string
        var logs []string
        for k, v := range queryData {
            val := getStr(v)
            args = append(args, fmt.Sprintf("%s=%s", k, url.QueryEscape(val)))
            logs = append(logs, fmt.Sprintf("%s=%s", k, val))
        }
    
        log("AesGcmDecrypt parseQuery  url:%s, md5key:%s, encryptString:%s, decrypt data:%s", c.Request.URL.String(), md5Key, encryptString, strings.Join(logs, "&"))
        c.Request.URL.RawQuery = strings.Join(args, "&")
        return nil
    }
    
    func parseFile(c *gin.Context, md5Key string) error {
        contentType := c.Request.Header.Get("Content-Type")
        _, params, _ := mime.ParseMediaType(contentType)
        boundary, ok := params["boundary"]
        if !ok {
            return errors.New("no multipart boundary param in Content-Type")
        }
    
        //准备重写数据
        bodyBuf := &bytes.Buffer{}
        wr := multipart.NewWriter(bodyBuf)
        mr := multipart.NewReader(c.Request.Body, boundary)
        for {
            p, err := mr.NextPart() //p的类型为Part
            if err == io.EOF {
                break
            }
    
            if err != nil {
                log("NextPart err:%v", err)
                break
            }
    
            fileByte, err := ioutil.ReadAll(p)
            if err != nil {
                log("ReadAll err:%v", err)
                break
            }
    
            pName := p.FormName()
            fileName := p.FileName()
            if len(fileName) < 1 {
                if pName == "encryptString" {
                    formData, err := gcmDecryptString(md5Key, string(fileByte))
                    if err != nil {
                        log("AesGcmDecrypt writeFile gcmDecryptString err:%v", err)
                        break
                    }
    
                    for k, v := range formData {
                        val := getStr(v)
                        err = wr.WriteField(k, val)
                        if err != nil {
                            log("AesGcmDecrypt writeFile WriteField :%s=%s, err:%v", k, val, err)
                            break
                        }
                    }
                } else {
                    wr.WriteField(pName, string(fileByte))
                }
            } else {
                tmp, err := wr.CreateFormFile(pName, fileName)
                if err != nil {
                    log("AesGcmDecrypt parseFile CreateFormFile err:%v", err)
                    continue
                }
                tmp.Write(fileByte)
            }
        }
    
        //写结尾标志
        _ = wr.Close()
        c.Request.Header.Set("Content-Type", wr.FormDataContentType())
        c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBuf.Bytes()))
    
        return nil
    }
    
    func gcmDecryptString(md5Key, encryptString string) (map[string]interface{}, error) {
        formData := make(map[string]interface{}, 0)
        if len(encryptString) < 1 {
            return formData, nil
        }
    
        plaintext, err := aes.GcmDecrypt(md5Key, encryptString)
        if err != nil {
            return formData, err
        }
    
        if len(plaintext) < 3 {
            //plaintext 应该是json 串 {}
            return formData, nil
        }
    
        err = json.Unmarshal([]byte(plaintext), &formData)
        if err != nil {
            return formData, err
        }
    
        return formData, nil
    }
    
    func isJsonResponse(c *gin.Context) bool {
        contentType := c.Writer.Header().Get("Content-Type")
        return strings.Contains(contentType, "application/json")
    }
    
    func getStr(v interface{}) string {
        val := ""
        switch v.(type) {
        case float64:
            //tmp, _ := decimal.NewFromString(fmt.Sprintf("%.10f", v))
            fl, _ := v.(float64)
            val = strconv.FormatFloat(fl, 'f', -1, 64)
        default:
            val = fmt.Sprintf("%v", v)
        }
        return val
    }
    
    type encryptJson struct {
        EncryptString string `json:"encryptString"`
    }
    
    func log(format string, arg ...interface{}) {
        fmt.Print(fmt.Sprintf(format, arg...))
    }
    func response(c *gin.Context, code int, msg string, data interface{}) {
        mapData := make(map[string]interface{}, 0)
        mapData["code"] = code
        mapData["msg"] = msg
        mapData["data"] = data
        c.JSON(200, data)
        c.Abort()
        return
    }

    最后我们来写一个demo程序main.go

    package main
    
    import (
        "demo/middleware"
        "fmt"
        "github.com/gin-gonic/gin"
        "os"
    )
    
    func main() {
        go func() {
            gateway := gin.Default()
            gateway.Use(middleware.AesGcmDecrypt())
            gateway.Use(middleware.GateWay())
            gateway.Run(":8080")
        }()
    
        // 1.创建路由
        r := gin.Default()
        r.Use(middleware.Logger())
    
        r.GET("/", func(c *gin.Context) {
            c.Writer.WriteString("pong")
        })
    
        r.GET("/demo", func(c *gin.Context) {
            req := ReqObj{}
            err := c.ShouldBindQuery(&req)
            if err != nil {
                fmt.Print(err)
            }
            response(c, 200, "ok", req)
        })
    
        r.POST("/test", func(c *gin.Context) {
            req := ReqObj{}
            err := c.ShouldBind(&req)
            if err != nil {
                fmt.Print(err)
            }
            response(c, 200, "ok", req)
        })
    
        r.POST("/form", func(c *gin.Context) {
            req := ReqObj{}
            err := c.ShouldBind(&req)
            if err != nil {
                fmt.Print(err)
            }
            response(c, 200, "ok", req)
        })
    
        r.POST("/upload", func(c *gin.Context) {
            file, err := c.FormFile("file")
            if err != nil {
                fmt.Print(err)
            }
            folder := c.Request.FormValue("folder")
            tmp, _ := os.Getwd()
            filePath := tmp + "/upload/" + folder + "/" + file.Filename
            c.SaveUploadedFile(file, filePath)
        })
    
        r.Run(":8000")
    }
    
    type ReqObj struct {
        Name       string `json:"name" form:"name"`
        Age        int64  `json:"age"  form:"age"`
        UpdateTime int64  `json:"update_time"  form:"update_time"`
        Folder     string `json:"folder"  form:"folder"`
    }
    
    func response(c *gin.Context, code int, msg string, data interface{}) {
        mapData := make(map[string]interface{}, 0)
        mapData["code"] = code
        mapData["msg"] = msg
        mapData["data"] = data
        c.JSON(200, data)
        c.Abort()
        return
    }

     来让我们一次验证一下运行结果:

    1.GET请求

      2.看看post json

     3验证postform

      最后来看一下文件上传:

    下载地址:https://github.com/dz45693/gindemo.git

  • 相关阅读:
    01《UML大战需求分析》阅读笔记之一
    软件构架实践阅读笔记四(读后感)
    软件构架实践阅读笔记三(读后感)
    软件构架实践阅读笔记二(读后感)
    软件构架实践阅读笔记一(读后感)
    阅读笔记 火球UML大战需求分析4
    阅读笔记 火球UML大战需求分析3
    阅读笔记 火球——UML大战需求分析 2
    课堂讨论
    学习进度条
  • 原文地址:https://www.cnblogs.com/majiang/p/15991552.html
Copyright © 2020-2023  润新知