react前端与go后端双向加解密

双向加解密

在http应用中,采用SSL证书保证通讯安全是很重要的,在一些对安全要求更高的场景中,单SSL加密是不够的。通常还会要求客户端与服务端之间点对点会话级加密,如在前后端分离的项目,会要求前端提交的数据加密并由后端解密,同时后端返回的数据也是由服务端加密并由前端解密。

请添加图片描述
如上图,我们采用AES-CBC模式加解密。在react端提供两种加解方式,go后端也提供两种加解密方式,它们的对应关系如下
请添加图片描述

代码如下

go后端

加解密模块

crypto.go内容如下

package crypto

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"encoding/base64"
	"errors"
	"strconv"
)

//----------------------------------------------------------------
//---------------------CBC模式-----------------------------------
//----------------------------------------------------------------
//注: 本方式的iv由key的前16字组成(而不是自动随机产生),并存储在密文的前16个字节

//填充数据至AES块大小
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(origData []byte) []byte {
    length := len(origData)
    unpadding := int(origData[length-1])
    return origData[:(length - unpadding)]
}

//加密
func EncryptCBC(txt string, key []byte,) (string, error) {
	data := []byte(txt)
	//key长度:必须是16,24,32
	if len:= len(string(key));(len != 16) && (len != 24) && (len != 32) {
		err := errors.New("KEY length must 16, 24, or 32,current len:" + strconv.Itoa(len))
        return "",err
	}

	// 采用何种加解密算法,取决于key的长度。
	// len(key) = 16,AES-128-GCM
	// len(key) = 24,AES-256-GCM
	// len(key) = 32,AES-512-GCM
    block, err := aes.NewCipher(key)      // 分组秘钥
    if err != nil {
        return "", err
    }

	blockSize := block.BlockSize()               // 获取秘钥块的长度。此值是固定值:16
    //blockSize := aes.BlockSize
	data = pkcs5Padding(data, blockSize)         // 补全码

    //加密
	blockMode := cipher.NewCBCEncrypter(block, key[:blockSize]) // 加密模式。IV值,直接取key的一部分即可。
    encrypted := make([]byte, len(data))                        // 创建数组
    blockMode.CryptBlocks(encrypted, data)                      // 加密
    return base64.StdEncoding.EncodeToString(encrypted), nil
}


//解密
func DecryptCBC(txt string, key []byte) (string, error) {
	data,_ := base64.StdEncoding.DecodeString(txt)
	//data := []byte(txt)
	//key长度:必须是16,24,32
	if len:= len(string(key));(len != 16) && (len != 24) && (len != 32) {
		err := errors.New("KEY length must 16, 24, or 32,current len:" + strconv.Itoa(len))
        return "",err
	}

	// 采用何种加解密算法,取决于key的长度。
	// len(key) = 16,AES-128-GCM
	// len(key) = 24,AES-256-GCM
	// len(key) = 32,AES-512-GCM
    block, err := aes.NewCipher(key)    // 分组秘钥
    if err != nil {
        return "", err
    }

	blockSize := block.BlockSize()               // 获取秘钥块的长度。此值是固定值:16
    //blockSize := aes.BlockSize

	//加密
    blockMode := cipher.NewCBCDecrypter(block, key[:blockSize])  // 加密模式
    decrypted := make([]byte, len(data))                         // 创建数组
    blockMode.CryptBlocks(decrypted, data)                       // 解密
    decrypted = pkcs5UnPadding(decrypted)                        // 去除补全码
	return string(decrypted),nil
}

//----------------------------------------------------------------
//---------------------CBC模式:iv版-------------------------------
//----------------------------------------------------------------
//注:本方式的iv由外部提供,且并不存储在密文中。
//------------IV版(iv必须是16字节的切片)-------------
//加密
func EncryptCBC_IV(txt string, key []byte,iv []byte) (string, error) {
	data := []byte(txt)
	//key长度:必须是16,24,32
	if len:= len(string(key));(len != 16) && (len != 24) && (len != 32) {
		err := errors.New("KEY length must 16, 24, or 32,current len:" + strconv.Itoa(len))
        return "",err
	}

	// 采用何种加解密算法,取决于key的长度。
	// len(key) = 16,AES-128-GCM
	// len(key) = 24,AES-256-GCM
	// len(key) = 32,AES-512-GCM
    block, err := aes.NewCipher(key)      // 分组秘钥
    if err != nil {
        return "", err
    }

	blockSize := block.BlockSize()               // 获取秘钥块的长度。此值是固定值:16
    //blockSize := aes.BlockSize
	data = pkcs5Padding(data, blockSize)         // 补全码

    //加密
	blockMode := cipher.NewCBCEncrypter(block, iv) // 加密模式。IV值,直接取key的一部分即可。
    encrypted := make([]byte, len(data))                        // 创建数组
    blockMode.CryptBlocks(encrypted, data)                      // 加密
    return base64.StdEncoding.EncodeToString(encrypted), nil
}


//解密
func DecryptCBC_IV(txt string, key []byte,iv []byte) (string, error) {
	data,_ := base64.StdEncoding.DecodeString(txt)
	//data := []byte(txt)
	//key长度:必须是16,24,32
	if len:= len(string(key));(len != 16) && (len != 24) && (len != 32) {
		err := errors.New("KEY length must 16, 24, or 32,current len:" + strconv.Itoa(len))
        return "",err
	}

	// 采用何种加解密算法,取决于key的长度。
	// len(key) = 16,AES-128-GCM
	// len(key) = 24,AES-256-GCM
	// len(key) = 32,AES-512-GCM
    block, err := aes.NewCipher(key)    // 分组秘钥
    if err != nil {
        return "", err
    }

	//blockSize := block.BlockSize()               // 获取秘钥块的长度。此值是固定值:16
    //blockSize := aes.BlockSize

	//加密
    blockMode := cipher.NewCBCDecrypter(block, iv)  // 加密模式
    decrypted := make([]byte, len(data))                         // 创建数组
    blockMode.CryptBlocks(decrypted, data)                       // 解密
    decrypted = pkcs5UnPadding(decrypted)                        // 去除补全码
	return string(decrypted),nil
}

api接口

main.go

package main

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"github.com/beego/beego/v2/server/web/filter/cors"
	_ "github.com/beego/beego/v2/server/web/session/mysql"

	"beego/controllers/crypto"
	"fmt"
	"io"

	beego "github.com/beego/beego/v2/server/web"
)

type TestController struct {
	beego.Controller
}



var jsonFlow = []byte(`[{"name": "张三","address": "深圳","age": 21,"sex": "女"},{"name": "王四","address": "上海","age": 22,"sex": "男"}]`)


//加解密所需key、iv
//前后端务必保持一致。
var (
    keyEnc []byte = []byte("beego_create_key_1234567")   //加密使用,由后端产生,并提供给前端。
	keyDec []byte = []byte("react_create_key_1234567")   //解密使用,由前端提供。
	iv []byte = []byte("Test12344321Test")
)

//返回信息
type ResultData struct {
	CodeStatus int    `json:"code"`
	Info string       `json:"info"`
	Data string       `json:"data"`
}


//登录信息
type UserCredentials struct {
  Username string `json:"username" form:"username"`
  Password string `json:"password" form:"password"`
}

//API 用户login
//认证后,把凭证写入cookie或session
func (c *TestController) Login() {  //react提交post用户帐号和密码以加密方式提交给接口
	var user UserCredentials = UserCredentials{}
	var result ResultData = ResultData{0,"",""}
	//从headers中获取客户端提取token认息
	//fmt.Printf("Token:%v\n",c.Ctx.Request.Header.Get("Token"))
	//从body中客户端提取加密认息
	bodyEnc := c.Ctx.Input.RequestBody
	//fmt.Println("收到的前端发达的密文:",string(bodyEnc))
	//body,_:= crypto.DecryptCBC_IV(string(bodyEnc),keyDec,iv) //解密
	body,_:= crypto.DecryptCBC(string(bodyEnc),keyDec)         //解密
	//fmt.Println(body)
	json.Unmarshal([]byte(body), &user)     //将解密后的信息转换为结构体
	//fmt.Printf("%+v\n",user) //查看用户端提供的body数据
	//c.ShowHeadCookie()     //查看header、cookie.回显到服务端console口

	//通过验证后产生新token返回组用户
	if ((user.Username == "guofs") && (user.Password == "123321")) {
		c.Ctx.Output.Header("Token","beego_submit_token_"+c.CreateToken(10))  
		result.CodeStatus = 200
		result.Info = "验证成功"

		//json字串美化
		var result1 []map[string]interface{}
        json.Unmarshal(jsonFlow, &result1)
        formattedJson, _ := json.MarshalIndent(result1, "", "  ")
		//fmt.Println(string(formattedJson))

		//加密
		//enc_data,_:= crypto.EncryptCBC_IV(string(formattedJson),keyEnc,iv)
		enc_data,_:= crypto.EncryptCBC(string(formattedJson),keyEnc)
		result.Data = enc_data

	} else {
		c.Ctx.Output.Header("Token","")
		result.CodeStatus = 408
		result.Info = "验证失败"
		result.Data = ""
	}
	result_Format, _:= json.Marshal(&result);
	c.Ctx.WriteString(string(result_Format))
	//fmt.Println(string(result_Format))
}

//创建Token:以随机字串模拟token
func (c *TestController) CreateToken(length int) string {
	k := make([]byte, length)
    _, err := io.ReadFull(rand.Reader, k)
    if err != nil {
        panic(err.Error())
    }
    return base64.StdEncoding.EncodeToString(k)
}

//遍历所有Header和cookie
func (c *TestController) ShowHeadCookie() {
	//header打印
	//单一值
	//fmt.Println("Content-Type:", c.Ctx.Request.Header.Get("Content-Type"))
    //遍历所有Header
	fmt.Println("-------------Header-------------------")
	for k, v := range c.Ctx.Request.Header {
		fmt.Printf("k:%v,v:%+v\n", k, v)
	}
    //cookie查看
	fmt.Println("-------------Cookie-------------------")
	//单一值
	//ck, _ := c.Ctx.Request.Cookie("username")
	//遍历所有cookie
	for _, v := range c.Ctx.Request.Cookies() {
		// fmt.Printf("%v=%v,%s\n", v.Name,v.Value,v.Expires.Format("2006-01-02 15:04:05"))
		fmt.Printf("%v=%v\n", v.Name,v.Value)
	}
}


func init() {
	beego.BConfig.WebConfig.AutoRender = false
	beego.BConfig.CopyRequestBody = true        //bind方式将json转结构体时需要此参数

	var corsconf = cors.Options {
		AllowAllOrigins: true,
		AllowCredentials: true,
		AllowOrigins:     []string{"*"},
		AllowMethods:[]string{"GET","POST","PUT","DELETE","OPTIONS"},
		AllowHeaders:[]string{"Origin","Authorization","Access-Control-Allow-Origin","Access-Control-Allow-Headers","Content-Type","Token"},
		ExposeHeaders:[]string{"Content-Length","Access-Control-Allow-Origin","Access-Control-Allow-Headers","Content-Type","Token"},
	}
	//所有url需要命中该中间函数,即打开url前先运行该配置。
	beego.InsertFilter("/*", beego.BeforeRouter, cors.Allow(&corsconf))
}

//主函数
func main() {
	tp := &TestController{}
	beego.Router("/show", tp, "get:ShowHeadCookie")         //查看所有cookie
	beego.Router("/login", tp, "post:Login")
	beego.Run()
}

react前端

加解密模块

crypto.js

import CryptoJS from 'crypto-js';

//-------------------------普通方式,无iv----------------------------------------
// 如下函数应用于它们之间或与go配合,都是可以的。
//将key的前16字节作为iv
//加密
function Encrypt(data,key) {
    const KEY = CryptoJS.enc.Utf8.parse(key);
    const iv = key.slice(0,16)
    console.log("iv:",iv)
    let encrypted = CryptoJS.AES.encrypt(data, KEY,         //CryptoJS.AES.encrypt已将iv隐藏在密文的前16个字节中。
        {
          iv: CryptoJS.enc.Utf8.parse(iv),  //偏移量
          mode: CryptoJS.mode.CBC,          //加密模式
          padding: CryptoJS.pad.Pkcs7       //填充
        }
    );
    return encrypted.toString();
    //return encrypted.toString(CryptoJS.enc.Utf8.);
    //return CryptoJS.enc.Utf8.stringify(encrypted.toString())
}

//解密
//准备工作:
//    解密前需了解密文来源,了解加密者的加密算法,如iv生成办法、iv存储方式,block填充方式等等。
//    例如:有些加密者会采用随机数用为iv,并隐藏在密文件的前12或16字节中,或其它位置。
//    而有些加密者会直接采用key的前16字节作为iv。
//
//本例的解密操作,是针对密文件为basse64格式,并 iv 隐藏在密文件的前16字节中
//取IV
function getIV(data) {
    const encoder = new TextEncoder()
    const DataByte = encoder.encode(data)                  //将密文转字节切片
    const ivByte = DataByte.slice(0, 16)                   //取字节切片前16字节,即iv字节切片
    const ivStr = new TextDecoder('utf-8').decode(ivByte)  //将iv字节切片转字串
    return ivStr
}
//解密
function Decrypt(data,key) {
    //iv提取:办法一,从密文中提取前16字节。通用。
    /*
    const KEY = CryptoJS.enc.Utf8.parse(key);
    const iv = getIV(data)
    */
    //由于本次解密的密文的iv是由key的前16字节组成,可从key中直接提取。
    const KEY = CryptoJS.enc.Utf8.parse(key);
    const iv = key.slice(0,16)
    console.log("iv:",iv)

    let decrypted = CryptoJS.AES.decrypt(data, KEY, 
        {
          iv : CryptoJS.enc.Utf8.parse(iv),
          mode: CryptoJS.mode.CBC,
          padding: CryptoJS.pad.Pkcs7
        }
    );
    return decrypted.toString(CryptoJS.enc.Utf8);
    //return CryptoJS.enc.Utf8.stringify(decrypted)   //此类书写也是可以的。
}



//---------------------------iv版,加解密双方明确录入iv值--------------------------------------
// 如下函数应用于它们之间或与go配合,都是可以的。
//加密
function EncryptIV(data,key,iv) {
    const KEY = CryptoJS.enc.Utf8.parse(key)
    const IV = CryptoJS.enc.Utf8.parse(iv)
    let encrypted = CryptoJS.AES.encrypt(data, KEY, 
        {
          iv: IV,                      //偏移量
          mode: CryptoJS.mode.CBC,     //加密模式
          padding: CryptoJS.pad.Pkcs7  //填充
        }
    );
    return encrypted.toString();
}

//解密
function DecryptIV(data,key,iv) {
    const KEY = CryptoJS.enc.Utf8.parse(key)
    const IV = CryptoJS.enc.Utf8.parse(iv)
    let decrypted = CryptoJS.AES.decrypt(data, KEY, 
        {
          iv: IV,
          mode: CryptoJS.mode.CBC,
          padding: CryptoJS.pad.Pkcs7
        }
    );
    return decrypted.toString(CryptoJS.enc.Utf8);
    //return CryptoJS.enc.Utf8.stringify(decrypted)   //此类书写也是可以的。
}


//对外开放定义的函数
export {
    Encrypt,
    Decrypt,
    EncryptIV,
    DecryptIV,
}

测试页面

index.js

import React, {useState } from 'react';
import ReactDOM from 'react-dom/client';
import { Encrypt, Decrypt, EncryptIV, DecryptIV } from './crypto.js';

const keyEnc = 'react_create_key_1234567'    //react加密使用, 由前端自定义,并提供给后端。前后端务必保持一致。
const keyDec = 'beego_create_key_1234567'    //react解密使用,由后端提供。前后端务必保持一致。
const iv = 'Test12344321Test'


function App() {
  const Token = "react_submit_token_8483"
  const [users, setUsers] = useState([]);
  //const navigate = useNavigate();
  function handleSubmit(event) {
    event.preventDefault();        // 阻止表单提交
    const jsonData = 
       {
         username: document.getElementById('username').value,
         password: document.getElementById('password').value,
       }
    //console.log(JSON.stringify(jsonData, null, 2));          //输出到浏览器的console接口
    //const txt_enc = EncryptIV(JSON.stringify(jsonData),keyEnc,iv)   //加密数据,此数据post会到后台进行解密
    const txt_enc = Encrypt(JSON.stringify(jsonData),keyEnc)   //加密数据,此数据post会到后台进行解密
    //console.log("传输前的密文:",txt_enc)
  
    //识别提交的按扭
    var url = ''
    if (event.target.id === 'login') {
        url = 'http://192.168.3.110:8080/login'
        console.log("login被单击")
      }
    if (event.target.id === 'register') {
        url = 'http://192.168.3.110:8080/login'
        console.log("register被单击")
    }
  
    //写入数据
    fetch(url,{
        method: 'POST',
        headers: {
         'Content-Type': 'application/json',
         'Token': Token,
        },
        //body: JSON.stringify(jsonData)
        body: txt_enc     //加密的数据提交给后台解密
    })
    .then((response) => {
      if (!response.ok) {
          console.log('Failed to submit data');
          throw new Error('Network response was not ok.');
      }
      //提取后台提供的token,并写入cookie
      const token = response.headers.get('token')
      console.log("token值:",token)
      return response.json()     //将url的返回body信息转换为json格式
    })
    .then((data) => {               //返回body数据处理
      console.log("data:",data)     //将返回信息回显到浏览器的console接口
      document.getElementById('opmsg').textContent = data.info   //将post时返回信息显示到指定标签上。
      if (data.info === '验证成功') {
          //const txt_dec = DecryptIV(data.data,keyDec,iv)   //解密.返回加密数据是base64字串。
          const txt_dec = Decrypt(data.data,keyDec)        //解密。
          document.getElementById('opmsg').textContent = "验证成功,请跳转到管理admin页面"
          console.log("txt_dec:",txt_dec)
          //navigate("/admin",{ replace: false });   //跳转到管理admin页面
          //console.log("name:",txt_dec[0].name)
          setUsers(txt_dec)
         }
    })
    .catch(error => {
      console.error('Error:', error);
      document.getElementById('opmsg').textContent = '后台服务异常若其它'
    });
    // console.log("test2") 此行的执行次序早于上面的 console.log("test1")
  }


  /*
  const tpp = 
  [
    {
      "address": "深圳",
      "age": 21,
      "name": "张三",
      "sex": "女"
    },
    {
      "address": "上海",
      "age": 22,
      "name": "王四",
      "sex": "男"
    }
  ]
  const list = tpp.map (
    (user) => <div>{user.name},{user.address},{user.age},{user.sex}</div>
  );

  const list = users.map (
    (user) => <div>{user.name},{user.address},{user.age},{user.sex}</div>
  );
  */

  return (
    <div>
      <form action="">
        用户名称:<input type="text" id="username" /><br/>
        用户密码:<input type="password" id="password" /><br/>
        <button id="login" type="submit" onClick={handleSubmit}>登录</button>
        {/* <button id="register" type="submit" onClick={handleSubmit}>注册</button> */}
      </form>
      <p id="opmsg"></p>        {/* 显示提示信息 */}
      <pre>{users}</pre>
    </div>
  )
 }


const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <div>
      <App />
  </div>
)

加解密过程示例

post时加解密过程
请添加图片描述
返回信息的加解密过程
请添加图片描述

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值