【开发背景】

接口数据加解密是前后端分离开发非常常见的应用场景。

前端:vue3+typescript+vite

后端:SpringBoot

【前端代码】

1. 安装crypto-js

npm install crypto-js

2. src/utils下新建secret.ts

import CryptoJS from 'crypto-js'

export interface CrypotoType {

encrypt: any

decrypt: any

}

// 默认的 KEY 与 iv 如果没有给

const KEY = CryptoJS.enc.Utf8.parse('yourkeycodexxxx')

const IV = CryptoJS.enc.Utf8.parse('yourivcodexxxx')

/**

* AES加密 :字符串 key iv 返回base64

*/

export function Encrypt(word: any, keyStr?: any, ivStr?: any) {

let key = KEY

let iv = IV

if (keyStr) {

key = CryptoJS.enc.Utf8.parse(keyStr)

iv = CryptoJS.enc.Utf8.parse(ivStr)

}

const srcs = CryptoJS.enc.Utf8.parse(word)

const encrypted = CryptoJS.AES.encrypt(srcs, key, {

iv: iv,

mode: CryptoJS.mode.CBC,

padding: CryptoJS.pad.ZeroPadding

})

return CryptoJS.enc.Base64.stringify(encrypted.ciphertext)

}

/**

* AES 解密 :字符串 key iv 返回base64

*

* @return {string}

*/

export function Decrypt(word: any, keyStr?: any, ivStr?: any) {

let key = KEY

let iv = IV

if (keyStr) {

key = CryptoJS.enc.Utf8.parse(keyStr)

iv = CryptoJS.enc.Utf8.parse(ivStr)

}

const base64 = CryptoJS.enc.Base64.parse(word)

const src = CryptoJS.enc.Base64.stringify(base64)

const decrypt = CryptoJS.AES.decrypt(src, key, {

iv: iv,

mode: CryptoJS.mode.CBC,

padding: CryptoJS.pad.ZeroPadding

})

return CryptoJS.enc.Utf8.stringify(decrypt)

}

需要注意的是,在转化成字符串的过程中一定要指定编码格式为UTF-8。

3. 在request/response拦截器里进行加密解密

/src/http/index.ts

// 引入AES加解密

import { Encrypt, Decrypt } from '@/utils/secret.js'

(1)请求发送之前,对请求数据进行加密处理

config.data = Encrypt(JSON.stringify(config.data))

config.headers!['Content-Type'] = 'application/json' // 指定传送数据格式为JSON

(2)接收数据之前,对接收数据进行解密处理

res.data = JSON.parse(Decrypt(res.data))

完整代码

/** 封装axios **/

import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'

import { Toast } from 'vant'

// 引入AES加解密

import { Encrypt, Decrypt } from '@/utils/secret.js'

// axios配置

const config = {

// baseURL: '/server-address/', // 发布地址

baseURL: 'http://localhost:8080/', // 本地测试地址

timeout: 5000 // request timeout

}

// 定义返回值类型

export interface Result {

code: number,

msg: string,

data: T

}

// axios封装

class Http {

// 定义一个axios的实例

private instance: AxiosInstance

// 构造函数:初始化

constructor(config: AxiosRequestConfig) {

// 创建axios实例

this.instance = axios.create(config)

// 配置拦截器

this.interceptors()

}

// 拦截器:处理请求发送和请求返回的数据

private interceptors() {

// 请求发送之前的处理

this.instance.interceptors.request.use((config: AxiosRequestConfig) => {

console.log(config.data)

/** 数据加密 json -> string -> encrypt **/

config.data = Encrypt(JSON.stringify(config.data))

// 修改headers的Content-Type

config.headers!['Content-Type'] = 'application/json'

/** 在请求头部添加token **/

// let token = ''; // 从cookies/sessionStorage里获取

// if(token){

// // 添加token到头部

// config.headers!['token'] = token

// }

return config

}, error => {

error.data = {}

error.data.msg = '服务器异常,请联系管理员!'

return error

})

// 请求返回数据的处理

this.instance.interceptors.response.use((response: AxiosResponse) => {

const { data } = response

// 数据不解密

const res = data

// 数据整体解密 decrypt -> string -> JSON

// const res = JSON.parse(Decrypt(data))

// 数据res.data部分解密

if(res.data!==null){

res.data = JSON.parse(Decrypt(res.data))

}

console.log(res) // res: { code: 200, data: null, msg: '请求成功' }

if (res.code !== 200) {

Toast({

message: res.msg || '服务器出错!',

type: 'fail',

duration: 5 * 1000

})

return Promise.reject(new Error(res.msg || '服务器出错!'))

} else {

return res

}

}, error => {

console.log('进入错误')

error.data = {}

if (error && error.response) {

switch (error.response.status) {

case 400:

error.data.msg = '错误请求'

break

case 500:

error.data.msg = '服务器内部错误'

break

case 404:

error.data.msg = '请求未找到'

break

default:

error.data.msg = `连接错误${error.response.status}`

break

}

Toast({

message: error.data.msg || '服务器连接出错!',

type: 'fail',

duration: 5 * 1000

})

} else {

error.data.msg = '连接到服务器失败!'

Toast({

message: error.data.msg,

type: 'fail',

duration: 5 * 1000

})

}

return Promise.reject(error)

})

}

/** RestFul api封装 **/

// Get请求:注意这里params被解构了,后端获取参数的时候直接取字段名

get(url: string, params?: object): Promise {

return this.instance.get(url, { params })

}

// Post请求

post(url: string, data?: object): Promise {

return this.instance.post(url, data)

}

// Put请求

put(url: string, data?: object): Promise {

return this.instance.put(url, data)

}

// DELETE请求

delete(url: string): Promise {

return this.instance.delete(url)

}

}

export default new Http(config)

【后端代码】

后端采用自定义注解的方式进行加解密。

1. pom.xml引入相关jar包

org.bouncycastle

bcprov-jdk15on

1.60

commons-codec

commons-codec

1.15

2. package.annotation新建2个注解Encrypt和Decrypt

package com.finance.pettycharge.annotation;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

/** 自定义解密方法:可以放在方法上,也可以放在参数上单独解密 **/

@Retention(RetentionPolicy.RUNTIME)

@Target({ElementType.METHOD, ElementType.PARAMETER})

public @interface Decrypt {

}

package com.finance.pettycharge.annotation;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

/** 自定义加密方法,仅能放在方法上 **/

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface Encrypt {

}

3. package.utils下新建工具类SecretUtils.java

package com.package.utils;

import org.apache.commons.codec.binary.Base64;

import javax.crypto.Cipher;

import javax.crypto.spec.IvParameterSpec;

import javax.crypto.spec.SecretKeySpec;

import java.nio.charset.Charset;

import java.nio.charset.StandardCharsets;

/**

* @author YSK

* @date 2020/8/24 13:13

*/

public class SecretUtils {

/***

* key和iv值可以随机生成

*/

private static String KEY = "yourkeycodexxxx";

private static String IV = "yourivcodexxxx";

/***

* 加密

* @param data 要加密的数据

* @return encrypt

*/

public static String encrypt(String data){

return encrypt(data, KEY, IV);

}

/***

* param data 需要解密的数据

* 调用desEncrypt()方法

*/

public static String desEncrypt(String data){

return desEncrypt(data, KEY, IV);

}

/**

* 加密方法

* @param data 要加密的数据

* @param key 加密key

* @param iv 加密iv

* @return 加密的结果

*/

private static String encrypt(String data, String key, String iv){

try {

//"算法/模式/补码方式"NoPadding PkcsPadding

Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");

int blockSize = cipher.getBlockSize();

byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8);

int plaintextLength = dataBytes.length;

if (plaintextLength % blockSize != 0) {

plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize));

}

byte[] plaintext = new byte[plaintextLength];

System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);

SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");

IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));

cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);

byte[] encrypted = cipher.doFinal(plaintext);

return new Base64().encodeToString(encrypted);

} catch (Exception e) {

e.printStackTrace();

return null;

}

}

/**

* 解密方法

* @param data 要解密的数据

* @param key 解密key

* @param iv 解密iv

* @return 解密的结果

*/

private static String desEncrypt(String data, String key, String iv){

try {

byte[] encrypted1 = new Base64().decode(data);

Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");

SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");

IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));

cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);

byte[] original = cipher.doFinal(encrypted1);

return new String(original, StandardCharsets.UTF_8).trim();

} catch (Exception e) {

e.printStackTrace();

return null;

}

}

}

4. package.utils下新建DecryptRequest.java

package com.package.utils;

import com.finance.pettycharge.annotation.Decrypt;

import org.springframework.core.MethodParameter;

import org.springframework.http.HttpHeaders;

import org.springframework.http.HttpInputMessage;

import org.springframework.http.converter.HttpMessageConverter;

import org.springframework.web.bind.annotation.ControllerAdvice;

import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;

import java.io.ByteArrayInputStream;

import java.io.IOException;

import java.io.InputStream;

import java.lang.reflect.Type;

import java.nio.charset.StandardCharsets;

/** 解密请求数据 **/

@ControllerAdvice

public class DecryptRequest extends RequestBodyAdviceAdapter {

/**

* 该方法用于判断当前请求,是否要执行beforeBodyRead方法

*

* @param methodParameter handler方法的参数对象

* @param targetType handler方法的参数类型

* @param converterType 将会使用到的Http消息转换器类类型

* @return 返回true则会执行beforeBodyRead

*/

@Override

public boolean supports(MethodParameter methodParameter, Type targetType, Class> converterType) {

return methodParameter.hasMethodAnnotation(Decrypt.class) || methodParameter.hasParameterAnnotation(Decrypt.class);

}

@Override

public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class> converterType) throws IOException {

byte[] body = new byte[inputMessage.getBody().available()];

inputMessage.getBody().read(body);

try {

String str = new String(body);

String newStr = SecretUtils.desEncrypt(str); // 解密加密串

// str->inputstream

/** 注意编码格式一定要指定UTF-8 不然会出现前后端解密错误 **/

InputStream newInputStream = new ByteArrayInputStream(newStr.getBytes(StandardCharsets.UTF_8));

return new HttpInputMessage() {

@Override

public InputStream getBody() throws IOException {

return newInputStream; // 返回解密串

}

@Override

public HttpHeaders getHeaders() {

return inputMessage.getHeaders();

}

};

}catch (Exception e){

e.printStackTrace();

}

return super.beforeBodyRead(inputMessage, parameter, targetType, converterType);

}

}

5. package.utils下新建EncryptResponse.java

package com.package.utils;

import com.alibaba.fastjson2.JSON;

import com.finance.pettycharge.annotation.Encrypt;

import com.finance.pettycharge.viewobject.ResultVO;

import org.springframework.core.MethodParameter;

import org.springframework.http.MediaType;

import org.springframework.http.converter.HttpMessageConverter;

import org.springframework.http.server.ServerHttpRequest;

import org.springframework.http.server.ServerHttpResponse;

import org.springframework.web.bind.annotation.ControllerAdvice;

import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

/** 加密返回数据 **/

@ControllerAdvice

public class EncryptResponse implements ResponseBodyAdvice {

@Override

public boolean supports(MethodParameter returnType, Class> converterType) {

return returnType.hasMethodAnnotation(Encrypt.class);

}

@Override

public ResultVO beforeBodyWrite(ResultVO body, MethodParameter returnType, MediaType selectedContentType, Class> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {

try{

// if(body.getMsg()!=null){

// body.setMsg(SecretUtils.encrypt(body.getMsg()));

// }

if(body.getData()!=null){ // 加密传输数据,注意先转JSON再转String格式

body.setData(SecretUtils.encrypt(JSON.toJSONString(body.getData())));

}

}catch (Exception e){

e.printStackTrace();

}

return body;

}

}

6.在controller里对需要使用的方法或者参数加上@Encrypt或者@Decrypt注解即可

// 查询

@GetMapping("/list")

@Encrypt

public ResultVO projectList(@RequestHeader(name = "user") String userId, String queryType) {

try {

List list = projectService.getProjectList(userId, queryType);

return ResultVOUtil.success("查询projectList列表成功!", list);

} catch (Exception e) {

return ResultVOUtil.error(e.getMessage());

}

}

// 更新项目状态

@PutMapping("/updatestatus")

@Decrypt

public ResultVO updateStatus(@RequestBody JSONObject data) {

if (projectService.updateProjectStatus(data.getInteger("id"), data.getString("type")) == 1) {

return ResultVOUtil.success("项目状态修改成功!", null);

}

return ResultVOUtil.error("项目状态修改失败");

}

【小结】

请注意前后端加解密一定要统一指定对应的编码格式,这里全部指定成了UTF-8。

初版本没有指定编码格式,本地测试没有任何问题,发布到服务器后出现了很多bug,排查原因都是由于编码格式不一致导致的。

只要涉及到字符串转换的、字节流的全部要指定编码格式,请务必注意这一点。

遇到的bug

前端报错=> Error: Malformed UTF-8 data

后端报错=> JSON parse error: Invalid UTF-8 middle 0xc2

参考资料:SpringBoot接口加密与解密_魅Lemon的博客-CSDN博客_springboot 接口加密

精彩链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: