跳到主要内容

http双向加密

背景

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


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


代码如下

go后端

加解密模块

crypto.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时加解密过程 Alt text

返回信息的加解密过程 Alt text