当前位置:   article > 正文

【加解密】前后端接口交互使用AES加解密_前后端加解密

前后端加解密

【开发背景】

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

前端:vue3+typescript+vite

后端:SpringBoot

【前端代码】

1. 安装crypto-js

npm install crypto-js

2. src/utils下新建secret.ts

  1. import CryptoJS from 'crypto-js'
  2. export interface CrypotoType {
  3. encrypt: any
  4. decrypt: any
  5. }
  6. // 默认的 KEY 与 iv 如果没有给
  7. const KEY = CryptoJS.enc.Utf8.parse('yourkeycodexxxx')
  8. const IV = CryptoJS.enc.Utf8.parse('yourivcodexxxx')
  9. /**
  10. * AES加密 :字符串 key iv 返回base64
  11. */
  12. export function Encrypt(word: any, keyStr?: any, ivStr?: any) {
  13. let key = KEY
  14. let iv = IV
  15. if (keyStr) {
  16. key = CryptoJS.enc.Utf8.parse(keyStr)
  17. iv = CryptoJS.enc.Utf8.parse(ivStr)
  18. }
  19. const srcs = CryptoJS.enc.Utf8.parse(word)
  20. const encrypted = CryptoJS.AES.encrypt(srcs, key, {
  21. iv: iv,
  22. mode: CryptoJS.mode.CBC,
  23. padding: CryptoJS.pad.ZeroPadding
  24. })
  25. return CryptoJS.enc.Base64.stringify(encrypted.ciphertext)
  26. }
  27. /**
  28. * AES 解密 :字符串 key iv 返回base64
  29. *
  30. * @return {string}
  31. */
  32. export function Decrypt(word: any, keyStr?: any, ivStr?: any) {
  33. let key = KEY
  34. let iv = IV
  35. if (keyStr) {
  36. key = CryptoJS.enc.Utf8.parse(keyStr)
  37. iv = CryptoJS.enc.Utf8.parse(ivStr)
  38. }
  39. const base64 = CryptoJS.enc.Base64.parse(word)
  40. const src = CryptoJS.enc.Base64.stringify(base64)
  41. const decrypt = CryptoJS.AES.decrypt(src, key, {
  42. iv: iv,
  43. mode: CryptoJS.mode.CBC,
  44. padding: CryptoJS.pad.ZeroPadding
  45. })
  46. return CryptoJS.enc.Utf8.stringify(decrypt)
  47. }

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

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

/src/http/index.ts

  1. // 引入AES加解密
  2. import { Encrypt, Decrypt } from '@/utils/secret.js'

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

  1. config.data = Encrypt(JSON.stringify(config.data))
  2. config.headers!['Content-Type'] = 'application/json' // 指定传送数据格式为JSON

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

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

完整代码

  1. /** 封装axios **/
  2. import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
  3. import { Toast } from 'vant'
  4. // 引入AES加解密
  5. import { Encrypt, Decrypt } from '@/utils/secret.js'
  6. // axios配置
  7. const config = {
  8. // baseURL: '/server-address/', // 发布地址
  9. baseURL: 'http://localhost:8080/', // 本地测试地址
  10. timeout: 5000 // request timeout
  11. }
  12. // 定义返回值类型
  13. export interface Result<T = any> {
  14. code: number,
  15. msg: string,
  16. data: T
  17. }
  18. // axios封装
  19. class Http {
  20. // 定义一个axios的实例
  21. private instance: AxiosInstance
  22. // 构造函数:初始化
  23. constructor(config: AxiosRequestConfig) {
  24. // 创建axios实例
  25. this.instance = axios.create(config)
  26. // 配置拦截器
  27. this.interceptors()
  28. }
  29. // 拦截器:处理请求发送和请求返回的数据
  30. private interceptors() {
  31. // 请求发送之前的处理
  32. this.instance.interceptors.request.use((config: AxiosRequestConfig) => {
  33. console.log(config.data)
  34. /** 数据加密 json -> string -> encrypt **/
  35. config.data = Encrypt(JSON.stringify(config.data))
  36. // 修改headers的Content-Type
  37. config.headers!['Content-Type'] = 'application/json'
  38. /** 在请求头部添加token **/
  39. // let token = ''; // 从cookies/sessionStorage里获取
  40. // if(token){
  41. // // 添加token到头部
  42. // config.headers!['token'] = token
  43. // }
  44. return config
  45. }, error => {
  46. error.data = {}
  47. error.data.msg = '服务器异常,请联系管理员!'
  48. return error
  49. })
  50. // 请求返回数据的处理
  51. this.instance.interceptors.response.use((response: AxiosResponse) => {
  52. const { data } = response
  53. // 数据不解密
  54. const res = data
  55. // 数据整体解密 decrypt -> string -> JSON
  56. // const res = JSON.parse(Decrypt(data))
  57. // 数据res.data部分解密
  58. if(res.data!==null){
  59. res.data = JSON.parse(Decrypt(res.data))
  60. }
  61. console.log(res) // res: { code: 200, data: null, msg: '请求成功' }
  62. if (res.code !== 200) {
  63. Toast({
  64. message: res.msg || '服务器出错!',
  65. type: 'fail',
  66. duration: 5 * 1000
  67. })
  68. return Promise.reject(new Error(res.msg || '服务器出错!'))
  69. } else {
  70. return res
  71. }
  72. }, error => {
  73. console.log('进入错误')
  74. error.data = {}
  75. if (error && error.response) {
  76. switch (error.response.status) {
  77. case 400:
  78. error.data.msg = '错误请求'
  79. break
  80. case 500:
  81. error.data.msg = '服务器内部错误'
  82. break
  83. case 404:
  84. error.data.msg = '请求未找到'
  85. break
  86. default:
  87. error.data.msg = `连接错误${error.response.status}`
  88. break
  89. }
  90. Toast({
  91. message: error.data.msg || '服务器连接出错!',
  92. type: 'fail',
  93. duration: 5 * 1000
  94. })
  95. } else {
  96. error.data.msg = '连接到服务器失败!'
  97. Toast({
  98. message: error.data.msg,
  99. type: 'fail',
  100. duration: 5 * 1000
  101. })
  102. }
  103. return Promise.reject(error)
  104. })
  105. }
  106. /** RestFul api封装 **/
  107. // Get请求:注意这里params被解构了,后端获取参数的时候直接取字段名
  108. get<T = Result>(url: string, params?: object): Promise<T> {
  109. return this.instance.get(url, { params })
  110. }
  111. // Post请求
  112. post<T = Result>(url: string, data?: object): Promise<T> {
  113. return this.instance.post(url, data)
  114. }
  115. // Put请求
  116. put<T = Result>(url: string, data?: object): Promise<T> {
  117. return this.instance.put(url, data)
  118. }
  119. // DELETE请求
  120. delete<T = Result>(url: string): Promise<T> {
  121. return this.instance.delete(url)
  122. }
  123. }
  124. export default new Http(config)

【后端代码】

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

1. pom.xml引入相关jar包

  1. <!-- AES 密码解密 -->
  2. <dependency>
  3. <groupId>org.bouncycastle</groupId>
  4. <artifactId>bcprov-jdk15on</artifactId>
  5. <version>1.60</version>
  6. </dependency>
  7. <!-- Base64依赖 -->
  8. <dependency>
  9. <groupId>commons-codec</groupId>
  10. <artifactId>commons-codec</artifactId>
  11. <version>1.15</version>
  12. </dependency>

2. package.annotation新建2个注解EncryptDecrypt

  1. package com.finance.pettycharge.annotation;
  2. import java.lang.annotation.ElementType;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5. import java.lang.annotation.Target;
  6. /** 自定义解密方法:可以放在方法上,也可以放在参数上单独解密 **/
  7. @Retention(RetentionPolicy.RUNTIME)
  8. @Target({ElementType.METHOD, ElementType.PARAMETER})
  9. public @interface Decrypt {
  10. }
  1. package com.finance.pettycharge.annotation;
  2. import java.lang.annotation.ElementType;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5. import java.lang.annotation.Target;
  6. /** 自定义加密方法,仅能放在方法上 **/
  7. @Retention(RetentionPolicy.RUNTIME)
  8. @Target(ElementType.METHOD)
  9. public @interface Encrypt {
  10. }

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

  1. package com.package.utils;
  2. import org.apache.commons.codec.binary.Base64;
  3. import javax.crypto.Cipher;
  4. import javax.crypto.spec.IvParameterSpec;
  5. import javax.crypto.spec.SecretKeySpec;
  6. import java.nio.charset.Charset;
  7. import java.nio.charset.StandardCharsets;
  8. /**
  9. * @author YSK
  10. * @date 2020/8/24 13:13
  11. */
  12. public class SecretUtils {
  13. /***
  14. * key和iv值可以随机生成
  15. */
  16. private static String KEY = "yourkeycodexxxx";
  17. private static String IV = "yourivcodexxxx";
  18. /***
  19. * 加密
  20. * @param data 要加密的数据
  21. * @return encrypt
  22. */
  23. public static String encrypt(String data){
  24. return encrypt(data, KEY, IV);
  25. }
  26. /***
  27. * param data 需要解密的数据
  28. * 调用desEncrypt()方法
  29. */
  30. public static String desEncrypt(String data){
  31. return desEncrypt(data, KEY, IV);
  32. }
  33. /**
  34. * 加密方法
  35. * @param data 要加密的数据
  36. * @param key 加密key
  37. * @param iv 加密iv
  38. * @return 加密的结果
  39. */
  40. private static String encrypt(String data, String key, String iv){
  41. try {
  42. //"算法/模式/补码方式"NoPadding PkcsPadding
  43. Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
  44. int blockSize = cipher.getBlockSize();
  45. byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8);
  46. int plaintextLength = dataBytes.length;
  47. if (plaintextLength % blockSize != 0) {
  48. plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize));
  49. }
  50. byte[] plaintext = new byte[plaintextLength];
  51. System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);
  52. SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
  53. IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
  54. cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
  55. byte[] encrypted = cipher.doFinal(plaintext);
  56. return new Base64().encodeToString(encrypted);
  57. } catch (Exception e) {
  58. e.printStackTrace();
  59. return null;
  60. }
  61. }
  62. /**
  63. * 解密方法
  64. * @param data 要解密的数据
  65. * @param key 解密key
  66. * @param iv 解密iv
  67. * @return 解密的结果
  68. */
  69. private static String desEncrypt(String data, String key, String iv){
  70. try {
  71. byte[] encrypted1 = new Base64().decode(data);
  72. Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
  73. SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
  74. IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
  75. cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
  76. byte[] original = cipher.doFinal(encrypted1);
  77. return new String(original, StandardCharsets.UTF_8).trim();
  78. } catch (Exception e) {
  79. e.printStackTrace();
  80. return null;
  81. }
  82. }
  83. }

4. package.utils下新建DecryptRequest.java

  1. package com.package.utils;
  2. import com.finance.pettycharge.annotation.Decrypt;
  3. import org.springframework.core.MethodParameter;
  4. import org.springframework.http.HttpHeaders;
  5. import org.springframework.http.HttpInputMessage;
  6. import org.springframework.http.converter.HttpMessageConverter;
  7. import org.springframework.web.bind.annotation.ControllerAdvice;
  8. import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;
  9. import java.io.ByteArrayInputStream;
  10. import java.io.IOException;
  11. import java.io.InputStream;
  12. import java.lang.reflect.Type;
  13. import java.nio.charset.StandardCharsets;
  14. /** 解密请求数据 **/
  15. @ControllerAdvice
  16. public class DecryptRequest extends RequestBodyAdviceAdapter {
  17. /**
  18. * 该方法用于判断当前请求,是否要执行beforeBodyRead方法
  19. *
  20. * @param methodParameter handler方法的参数对象
  21. * @param targetType handler方法的参数类型
  22. * @param converterType 将会使用到的Http消息转换器类类型
  23. * @return 返回true则会执行beforeBodyRead
  24. */
  25. @Override
  26. public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
  27. return methodParameter.hasMethodAnnotation(Decrypt.class) || methodParameter.hasParameterAnnotation(Decrypt.class);
  28. }
  29. @Override
  30. public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
  31. byte[] body = new byte[inputMessage.getBody().available()];
  32. inputMessage.getBody().read(body);
  33. try {
  34. String str = new String(body);
  35. String newStr = SecretUtils.desEncrypt(str); // 解密加密串
  36. // str->inputstream
  37. /** 注意编码格式一定要指定UTF-8 不然会出现前后端解密错误 **/
  38. InputStream newInputStream = new ByteArrayInputStream(newStr.getBytes(StandardCharsets.UTF_8));
  39. return new HttpInputMessage() {
  40. @Override
  41. public InputStream getBody() throws IOException {
  42. return newInputStream; // 返回解密串
  43. }
  44. @Override
  45. public HttpHeaders getHeaders() {
  46. return inputMessage.getHeaders();
  47. }
  48. };
  49. }catch (Exception e){
  50. e.printStackTrace();
  51. }
  52. return super.beforeBodyRead(inputMessage, parameter, targetType, converterType);
  53. }
  54. }

5. package.utils下新建EncryptResponse.java

  1. package com.package.utils;
  2. import com.alibaba.fastjson2.JSON;
  3. import com.finance.pettycharge.annotation.Encrypt;
  4. import com.finance.pettycharge.viewobject.ResultVO;
  5. import org.springframework.core.MethodParameter;
  6. import org.springframework.http.MediaType;
  7. import org.springframework.http.converter.HttpMessageConverter;
  8. import org.springframework.http.server.ServerHttpRequest;
  9. import org.springframework.http.server.ServerHttpResponse;
  10. import org.springframework.web.bind.annotation.ControllerAdvice;
  11. import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
  12. /** 加密返回数据 **/
  13. @ControllerAdvice
  14. public class EncryptResponse implements ResponseBodyAdvice<ResultVO> {
  15. @Override
  16. public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
  17. return returnType.hasMethodAnnotation(Encrypt.class);
  18. }
  19. @Override
  20. public ResultVO beforeBodyWrite(ResultVO body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
  21. try{
  22. // if(body.getMsg()!=null){
  23. // body.setMsg(SecretUtils.encrypt(body.getMsg()));
  24. // }
  25. if(body.getData()!=null){ // 加密传输数据,注意先转JSON再转String格式
  26. body.setData(SecretUtils.encrypt(JSON.toJSONString(body.getData())));
  27. }
  28. }catch (Exception e){
  29. e.printStackTrace();
  30. }
  31. return body;
  32. }
  33. }

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

  1. // 查询
  2. @GetMapping("/list")
  3. @Encrypt
  4. public ResultVO projectList(@RequestHeader(name = "user") String userId, String queryType) {
  5. try {
  6. List<Project> list = projectService.getProjectList(userId, queryType);
  7. return ResultVOUtil.success("查询projectList列表成功!", list);
  8. } catch (Exception e) {
  9. return ResultVOUtil.error(e.getMessage());
  10. }
  11. }
  1. // 更新项目状态
  2. @PutMapping("/updatestatus")
  3. @Decrypt
  4. public ResultVO updateStatus(@RequestBody JSONObject data) {
  5. if (projectService.updateProjectStatus(data.getInteger("id"), data.getString("type")) == 1) {
  6. return ResultVOUtil.success("项目状态修改成功!", null);
  7. }
  8. return ResultVOUtil.error("项目状态修改失败");
  9. }

【小结】

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

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

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

遇到的bug

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

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

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/589293
推荐阅读
相关标签
  

闽ICP备14008679号