api接口加密

1.为什么需要api接口加密呢?

1.防止爬虫

2.防止数据被串改

3.确保数据安全

2.如何实现接口加密呢?

3.我们可以使用哪些加密算法来加密呢?

AES

密码学中的高级加密标准(Advanced Encryption Standard,AES),又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。

SM4

SM4.0(原名SMS4.0)是中华人民共和国政府采用的一种分组密码标准,由国家密码管理局于2012年3月21日发布。相关标准为“GM/T 0002-2012《SM4分组密码算法》(原SMS4分组密码算法)”。在商用密码体系中,SM4主要用于数据加密,其算法公开,分组长度与密钥长度均为128bit,加密算法与密钥扩展算法都采用32轮非线性迭代结构,S盒为固定的8比特输入8比特输出。SM4.0中的指令长度被提升到大于64K(即64×1024)的水平,这是SM 3.0规格(渲染指令长度允许大于512)的128倍。

4.具体实现
4.1后端

在这之前我们先了解一个类RequestBodyAdviceAdapter,主要通过该类实现~

RequestBodyAdviceAdapter

官网解释:RequestBodyAdviceAdapter (Spring Framework 6.2.1 API)

RequestBodyAdviceAdapter‌是Spring MVC中用于增强请求体处理的一个工具类,它实现了RequestBodyAdvice接口。RequestBodyAdviceAdapter提供了三个主要方法:beforeBodyReadafterBodyReadhandleEmptyBody

  1. beforeBodyRead‌:这个方法在请求体被读取之前调用,主要用于预处理请求体。默认实现是直接返回传入的HttpInputMessage对象。

  2. afterBodyRead‌:在请求体被读取并转换为对象之后调用,用于对读取到的对象进行进一步处理。默认实现是直接返回转换后的对象。

  3. handleEmptyBody‌:当请求体为空时调用,用于处理这种情况。默认实现是直接返回null。

代码实现

这里我们使用类似Aop的思想实现,定义对应的注解!

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface ApiDecrypt { //解密
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface ApiEncrypt {//加密
}
​
@Slf4j
@ControllerAdvice
public class DecryptRequestAdvice extends RequestBodyAdviceAdapter {
​
    private static final String ENCODING = "UTF-8";
​
    @Resource
    private ApiEncryptService apiEncryptService;
​
    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return methodParameter.hasMethodAnnotation(ApiDecrypt.class) || methodParameter.hasParameterAnnotation(ApiDecrypt.class)
                || methodParameter.getContainingClass().isAnnotationPresent(ApiDecrypt.class);
    }
​
    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
                                           Type targetType, Class<? extends HttpMessageConverter<?>> converterType)  {
        try {
            String bodyStr = IOUtils.toString(inputMessage.getBody(), ENCODING);
            ApiEncryptForm apiEncryptForm = JSONObject.parseObject(bodyStr, ApiEncryptForm.class);
            if (StrUtil.isEmpty(apiEncryptForm.getEncryptData())) {
                return inputMessage;
            }
            String decrypt = apiEncryptService.decrypt(apiEncryptForm.getEncryptData());
            return new DecryptHttpInputMessage(inputMessage.getHeaders(), IOUtils.toInputStream(decrypt, ENCODING));
        } catch (Exception e) {
            return inputMessage;
        }
​
    }
​
    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }
​
    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }
​
    static class DecryptHttpInputMessage implements HttpInputMessage {
        private final HttpHeaders headers;
​
        private final InputStream body;
​
        public DecryptHttpInputMessage(HttpHeaders headers, InputStream body) {
            this.headers = headers;
            this.body = body;
        }
​
        @Override
        public InputStream getBody() {
            return body;
        }
​
        @Override
        public HttpHeaders getHeaders() {
            return headers;
        }
    }
​
}
@Slf4j
@ControllerAdvice
public class EncryptResponseAdvice implements ResponseBodyAdvice<ResponseDTO> {
​
    @Resource
    private ApiEncryptService apiEncryptService;
​
    @Resource
    private ObjectMapper objectMapper;
​
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return returnType.hasMethodAnnotation(ApiEncrypt.class) || returnType.getContainingClass().isAnnotationPresent(ApiEncrypt.class);
    }
​
    @Override
    public ResponseDTO beforeBodyWrite(ResponseDTO body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body.getData() == null) {
            return body;
        }
​
        String encrypt = null;
        try {
            encrypt = apiEncryptService.encrypt(objectMapper.writeValueAsString(body.getData()));
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
        body.setData(encrypt);
        body.setDataType(DataTypeEnum.ENCRYPT.getValue());
        return body;
    }
}

实现类

/**
 * AES 加密和解密
 * 1、AES加密算法支持三种密钥长度:128位、192位和256位,这里选择128位
 * 2、AES 要求秘钥为 128bit,转化字节为 16个字节;
 * 3、js前端使用 UCS-2 或者 UTF-16 编码,字母、数字、特殊符号等 占用1个字节;
 * 4、所以:秘钥Key 组成为:字母、数字、特殊符号 一共16个即可
 */
@Slf4j
public class ApiEncryptServiceAesImpl implements ApiEncryptService {
​
    private static final String CHARSET = "UTF-8";
​
    private static final String AES_KEY = "1024lab__1024lab";
​
    static {
        Security.addProvider(new BouncyCastleProvider());
    }
​
    @Override
    public String encrypt(String data) {
        try {
            //  AES 加密 并转为 base64
            AES aes = new AES(hexToBytes(stringToHex(AES_KEY)));
            return aes.encryptBase64(data);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return StringConst.EMPTY;
        }
    }
​
    @Override
    public String decrypt(String data) {
        try {
            // 第一步: Base64 解码
            byte[] base64Decode = Base64.getDecoder().decode(data);
            // 第二步: AES 解密
            AES aes = new AES(hexToBytes(stringToHex(AES_KEY)));
            byte[] decryptedBytes = aes.decrypt(base64Decode);
            return new String(decryptedBytes, CHARSET);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return StringConst.EMPTY;
        }
    }
​
    /**
     * 16 进制串转字节数组
     *
     * @param hex 16进制字符串
     * @return byte数组
     */
    public static byte[] hexToBytes(String hex) {
        int length = hex.length();
        byte[] result;
        if (length % 2 == 1) {
            length++;
            result = new byte[(length / 2)];
            hex = "0" + hex;
        } else {
            result = new byte[(length / 2)];
        }
        int j = 0;
        for (int i = 0; i < length; i += 2) {
            result[j] = hexToByte(hex.substring(i, i + 2));
            j++;
        }
        return result;
    }
​
    public static String stringToHex(String input) {
        char[] chars = input.toCharArray();
        StringBuilder hex = new StringBuilder();
        for (char c : chars) {
            hex.append(Integer.toHexString((int) c));
        }
        return hex.toString();
    }
​
    /**
     * 16 进制字符转字节
     *
     * @param hex 16进制字符 0x00到0xFF
     * @return byte
     */
    private static byte hexToByte(String hex) {
        return (byte) Integer.parseInt(hex, 16);
    }
​
    public static void main(String[] args) {
//        System.out.println(new ApiEncryptServiceAesImpl().encrypt("{\"name\":\"chen\",\"age\":18}"));
        System.out.println(new ApiEncryptServiceAesImpl().decrypt("MWFjZTI3MDA0ZDcxY2RlM2U4YTY5NDU3MjE3MmE0YzlhOTI0OTVhOTBiM2VmYzYwZjgxZTlmMjRmNDVkMWNlMg=="));
    }
}
​
​
@Slf4j
@Service
public class ApiEncryptServiceSmImpl implements ApiEncryptService {
​
    private static final String CHARSET = "UTF-8";
    private static final String SM4_KEY = "1024lab__1024lab";
​
    static {
        Security.addProvider(new BouncyCastleProvider());
    }
​
​
    @Override
    public String encrypt(String data) {
        try {
​
            // 第一步: SM4 加密
            SM4 sm4 = new SM4(hexToBytes(stringToHex(SM4_KEY)));
            String encryptHex = sm4.encryptHex(data);
​
            // 第二步: Base64 编码
            return new String(Base64.getEncoder().encode(encryptHex.getBytes(CHARSET)), CHARSET);
​
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return StringConst.EMPTY;
        }
    }
​
​
    @Override
    public String decrypt(String data) {
        try {
​
            // 第一步: Base64 解码
            byte[] base64Decode = Base64.getDecoder().decode(data);
​
            // 第二步: SM4 解密
            SM4 sm4 = new SM4(hexToBytes(stringToHex(SM4_KEY)));
            return sm4.decryptStr(new String(base64Decode));
​
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return StringConst.EMPTY;
        }
    }
​
​
    public static String stringToHex(String input) {
        char[] chars = input.toCharArray();
        StringBuilder hex = new StringBuilder();
        for (char c : chars) {
            hex.append(Integer.toHexString((int) c));
        }
        return hex.toString();
    }
​
​
    /**
     * 16 进制串转字节数组
     *
     * @param hex 16进制字符串
     * @return byte数组
     */
    public static byte[] hexToBytes(String hex) {
        int length = hex.length();
        byte[] result;
        if (length % 2 == 1) {
            length++;
            result = new byte[(length / 2)];
            hex = "0" + hex;
        } else {
            result = new byte[(length / 2)];
        }
        int j = 0;
        for (int i = 0; i < length; i += 2) {
            result[j] = hexToByte(hex.substring(i, i + 2));
            j++;
        }
        return result;
    }
​
    /**
     * 16 进制字符转字节
     *
     * @param hex 16进制字符 0x00到0xFF
     * @return byte
     */
    private static byte hexToByte(String hex) {
        return (byte) Integer.parseInt(hex, 16);
    }
​
}
4.2前端

axios.js

import axios from 'axios'
import { decryptData, encryptData } from './encrypt.js';
​
const requests = axios.create({
  //共同的接口前缀
  baseURL: 'http://localhost:8881',
  //超时时间
  timeout: 2000
});
​
//请求拦截器:再发请求之前,请求拦截器可以检查到,可以在请求发出去之前做一些事情
requests.interceptors.request.use(config => {
  return config
}), err => {
  return Promise.reject(err)
}
​
//响应拦截器
requests.interceptors.response.use(response => {
  // 如果是加密数据
  if (response.data.dataType === 10) {
    response.data.encryptData = response.data.data;
    let decryptStr = decryptData(response.data.data);
    if (decryptStr) {
      response.data.data = JSON.parse(decryptStr);
    }
  }
  return response.data
}, err => {
  return Promise.reject(err)
})
​
// ================================= 加密 =================================
/**
 * 加密请求参数的post请求
 */
export const postEncryptRequest = (url, data) => {
  return requests({
    data: { encryptData: encryptData(data) },
    url,
    method: 'post',
  });
};
​
​
/**
 * post请求
 */
export const postRequest = (url, data) => {
  return requests({
    data,
    url,
    method: 'post',
  });
};
​
/**
 * get请求  /api?encryptData=xxx
 */
export const getEncryptRequest = (url, data) => {
  return requests({
    params: { encryptData: encryptData(data) },
    url,
    method: 'get',
  });
};

前端加密

import CryptoJS from 'crypto-js';
import CryptoSM from 'sm-crypto';
​
function object2string(data) {
  if (typeof data === 'object') {
    return JSON.stringify(data);
  }
​
  let str = JSON.stringify(data);
  if (str.startsWith("'") || str.startsWith('"')) {
    str = str.substring(1);
  }
  if (str.endsWith("'") || str.endsWith('"')) {
    str = str.substring(0, str.length - 1);
  }
  return str;
}
/**
 * 字符串转为数字
 */
function stringToHex(str) {
  let hex = '';
  for (let i = 0; i < str.length; i++) {
    hex += str.charCodeAt(i).toString(16).padStart(2, '0');
  }
  return hex;
}
​
/*
 * -------------------- ※ AES 加密、解密 begin ※ --------------------
 *
 * 1、AES加密算法支持三种密钥长度:128位、192位和256位,这里选择128位
 * 2、AES 要求秘钥为 128bit,转化字节为 16个字节;
 * 3、js前端使用 UCS-2 或者 UTF-16 编码,字母、数字、特殊符号等 占用1个字节;
 * 4、所以:秘钥Key 组成为:字母、数字、特殊符号 一共16个即可
 *
 * -------------------- ※ AES 加密、解密 end ※ --------------------
 */
const AES_KEY = '1024lab__1024lab';
const AES = {
  encryptData: function (data) {
    // AES 加密 并转为 base64
    let utf8Data = CryptoJS.enc.Utf8.parse(object2string(data));
    const key = CryptoJS.enc.Utf8.parse(AES_KEY);
    const encrypted = CryptoJS.AES.encrypt(utf8Data, key, {
      mode: CryptoJS.mode.ECB,
      padding: CryptoJS.pad.Pkcs7,
    });
    return CryptoJS.enc.Base64.stringify(encrypted.ciphertext);
  },
​
  decryptData: function (data) {
    //  第一步:Base64 解码
    let words = CryptoJS.enc.Base64.parse(data);
​
    // 第二步:AES 解密
    const key = CryptoJS.enc.Utf8.parse(AES_KEY);
    return CryptoJS.AES.decrypt({ ciphertext: words }, key, {
      mode: CryptoJS.mode.ECB,
      padding: CryptoJS.pad.Pkcs7,
    }).toString(CryptoJS.enc.Utf8);
  },
};
​
/*
 * -------------------- ※ 国密SM4算法 加密、解密 begin ※ --------------------
 *
 * 1、国密SM4 要求秘钥为 128bit,转化字节为 16个字节;
 * 2、js前端使用 UCS-2 或者 UTF-16 编码,字母、数字、特殊符号等 占用1个字节;
 * 3、java中 每个 字母数字 也是占用1个字节;
 * 4、所以:前端和后端的 秘钥Key 组成为:字母、数字、特殊符号 一共16个即可
 *
 * -------------------- ※ 国密SM4算法 加密、解密 end ※ --------------------
 */
​
// 秘钥Key 组成为:字母、数字、特殊符号 一共16个即可
const SM4_KEY = '1024lab__1024lab';
​
const SM4 = {
  encryptData: function (data) {
    // 第一步:SM4 加密
    let encryptData = CryptoSM.sm4.encrypt(object2string(data), stringToHex(SM4_KEY));
    // 第二步: Base64 编码
    return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(encryptData));
  },
​
  decryptData: function (data) {
    // 第一步:Base64 解码
    let words = CryptoJS.enc.Base64.parse(data);
    let decode64Str = CryptoJS.enc.Utf8.stringify(words);
​
    // 第二步:SM4 解密
    return CryptoSM.sm4.decrypt(decode64Str, stringToHex(SM4_KEY));
  },
};
​
​
​
// -----------------------  对外暴露: 加密、解密 -----------------------
​
// 默认使用SM4算法
const EncryptObject = SM4;
// const EncryptObject = AES;
​
/**
 * 加密
 */
export const encryptData = function (data) {
  return !data ? null : EncryptObject.encryptData(data);
};
/**
 * 解密
 */
export const decryptData = function (data) {
  return !data ? null : EncryptObject.decryptData(data);
};

api.js

import { postRequest, postEncryptRequest, getEncryptRequest } from '@/utils/axios.js';
// 测试获取列表 双向加密
export const getList = (data) => {
  return postEncryptRequest(
    '/api/test/getList',
    data
  )
}
​
export const getListOfTetsGet = (data) => {
  return getEncryptRequest(
    '/api/testget/getList',
    data
  )
}
​
// 请求加密paramsAes
export const getListParamsAes = (data) => {
  return postEncryptRequest(
    '/api/paramsAes/getList',
    data
  )
}
​
// 响应加密  /resAes/getList
export const getListResAes = (data) => {
  return postRequest(
    '/api/resAes/getList',
    data
  )
}

vue界面

<template>
  <div>
    <el-button type="primary" @click="aes">发送加密请求</el-button>
    <el-button type="primary" @click="paramsAsc">请求体加密</el-button>
    <el-button type="primary" @click="reqAsc">返回加密</el-button>
​
    <div>
      <el-table :data="tableData" border style="width: 100%">
        <el-table-column prop="title" label="书标题">
        </el-table-column>
        <el-table-column prop="author" label="作者">
        </el-table-column>
        <el-table-column prop="price" label="价格">
        </el-table-column>
        <el-table-column fixed="right" label="操作" width="100">
          <template slot-scope="scope">
            <el-button @click="handleClick(scope.row)" type="text" size="small">查看</el-button>
            <el-button type="text" size="small">编辑</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>
​
<script>
  import { getList, getListParamsAes, getListResAes, getListOfTetsGet } from "@/api/test.js"
  export default {
    name: 'HelloWorld',
    props: {
      msg: String
    },
    data() {
      return {
        tableData: []
      }
    },
    mounted() {
      this.aes()
    },
    methods: {
      aes() {
        getList({ pageNum: 1, pageSize: 3 }).then(res => {
          if (res.code == 0) {
            this.tableData = res.data
          }
        })
      },
      paramsAsc() {
        getListParamsAes({ pageNum: 1, pageSize: 3 }).then(res => {
          console.log(res);
        })
      },
      reqAsc() {
        getListResAes({ pageNum: 1, pageSize: 3 }).then(res => {
          console.log(res);
        })
      }
    },
  }
</script>
5.测试结果

双向加密

 

请求体加密

 

响应体加密

6.声明

该案例参考于开源的smartadmin项目,该项目类似于若依,整体代码比较规范,推荐使用~

官网地址:SmartAdmin | 1024创新实验室

gitee地址:smart-admin(MIT协议-免费任意商用): 🔥SmartAdmin以「高质量代码」为核心,「简洁、高效、安全」的快速开发平台;基于SpringBoot2/3+Sa-Token+Mybatis-Plus和Vue3 +Ant Design Vue+UniApp (提供JavaScript和TypeScript双版本、Java8和java17双版本);满足三级等保、网络安全、数据安全等功能要求。并重磅开源千余家企业在使用的《高质量代码规范》等

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐