赞
踩
由于项目需求,需要用到前后端数据的加解密操作。在网上查找了了相关资料,但在实际应用中遇到了一些问题,不能完全满足我的要求。
以此为基础(前后端接口AES+RSA混合加解密详解(vue+SpringBoot)附完整源码)进行了定制化的改造,以提高代码的效率和易用性
请参考前言中引入的链接,复制
后端加密工具类:AESUtils、RSAUtils、MD5Util、ApiSecurityUtils
替换Base64,使用jdk内置的
AESUtils、ApiSecurityUtils
Base64.getEncoder().encodeToString
Base64.getDecoder().decode
RSAUtils,之所以这个工具类使用以下方法,因为它会报Illegal base64 character a
Base64.getMimeEncoder().encodeToString
Base64.getMimeDecoder().decode
前端加密工具类
import lombok.Data; import org.springframework.stereotype.Component; /** * @author Mr.Jie * @version v1.0 * @description 用于返回前端解密返回体的aeskey和返回体 * @date 2024/8/11 下午4:12 **/ @Data @Component public class ApiEncryptResult { private String aesKeyByRsa; private String data; private String frontPublicKey; }
import org.springframework.web.bind.annotation.Mapping; import java.lang.annotation.*; /** * @author Mr.Jie * @version v1.0 * @description 加解密算法 * @date 2024/8/12 上午9:19 **/ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Mapping @Documented public @interface SecurityParameter { /** * 入参是否解密,默认解密 */ boolean isDecode() default true; /** * 输出参数是否加密,默认加密 **/ boolean isOutEncode() default true; }
/** * @author Mr.Jie * @version v1.0 * @description 解密数据失败异常 * @date 2024/8/12 上午9:43 **/ public class DecryptBodyFailException extends RuntimeException { private String code; public DecryptBodyFailException() { super("Decrypting data failed. (解密数据失败)"); } public DecryptBodyFailException(String message, String errorCode) { super(message); code = errorCode; } public DecryptBodyFailException(String message) { super(message); } public String getCode() { return code; } }
import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; import java.io.IOException; import java.io.InputStream; /** * @author Mr.Jie * @version v1.0 * @description 解密信息输入流 * @date 2024/8/12 上午9:43 **/ @NoArgsConstructor @AllArgsConstructor public class DecryptHttpInputMessage implements HttpInputMessage { private InputStream body; private HttpHeaders headers; @Override public InputStream getBody() throws IOException { return body; } @Override public HttpHeaders getHeaders() { return headers; } }
import cn.hutool.core.convert.Convert; import com.alibaba.fastjson.JSONObject; import com.longsec.encrypt.exception.DecryptBodyFailException; import com.longsec.encrypt.utils.ApiSecurityUtils; import com.longsec.encrypt.utils.MD5Util; import com.longsec.common.redis.RedisUtils; import com.longsec.common.utils.StringUtils; import com.longsec.encrypt.annotation.SecurityParameter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; import org.springframework.http.HttpInputMessage; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; /** * @author Mr.Jie * @version v1.0 * @description 接口数据解密 核心的方法就是 supports,该方法返回的boolean值, * 决定了是要执行 beforeBodyRead 方法。而我们主要的逻辑就是在beforeBodyRead方法中,对客户端的请求体进行解密。 * RequestBodyAdvice这个接口定义了一系列的方法,它可以在请求体数据被HttpMessageConverter转换前后,执行一些逻辑代码。通常用来做解密 * 本类只对控制器参数中含有<strong>{@link org.springframework.web.bind.annotation.RequestBody}</strong> * @date 2024/8/12 上午9:43 **/ @Slf4j @RestControllerAdvice @Component public class DecodeRequestBodyAdvice implements RequestBodyAdvice { @Autowired private RedisUtils redisUtils; protected static String jsPublicKey = ""; @Override public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { //如果等于false则不执行 return methodParameter.getMethod().isAnnotationPresent(SecurityParameter.class); } @Override public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException { // 定义是否解密 boolean encode = false; SecurityParameter serializedField = parameter.getMethodAnnotation(SecurityParameter.class); //入参是否需要解密 if (serializedField != null) { encode = serializedField.isDecode(); } log.info("对方法method :【" + parameter.getMethod().getName() + "】数据进行解密!"); inputMessage.getBody(); String body; try { body = IOUtils.toString(inputMessage.getBody(), StandardCharsets.UTF_8); } catch (Exception e) { throw new DecryptBodyFailException("无法获取请求正文数据,请检查发送数据体或请求方法是否符合规范。"); } if (StringUtils.isEmpty(body)) { throw new DecryptBodyFailException("请求正文为NULL或为空字符串,因此解密失败。"); } //解密 if (encode) { try { // 获取当前的ServletRequestAttributes ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes != null) { // 获取原始的HttpServletRequest对象 HttpServletRequest request = attributes.getRequest(); // 从请求头中获取X-Access-Token String token = request.getHeader("X-Access-Token"); if (StringUtils.isEmpty(token)) { throw new DecryptBodyFailException("无法获取请求头中的token"); } // 使用token进行进一步操作 JSONObject jsonBody = JSONObject.parseObject(body); if (null != jsonBody) { String dataEncrypt = jsonBody.getString("data"); String aeskey = jsonBody.getString("aeskey"); jsPublicKey = jsonBody.getString("frontPublicKey"); String md5Token = MD5Util.md5(token); String privateKey = Convert.toStr(redisUtils.getCacheObject(md5Token + "privateKey")); body = ApiSecurityUtils.decrypt(aeskey, dataEncrypt, privateKey); } } InputStream inputStream = IOUtils.toInputStream(body, StandardCharsets.UTF_8); return new DecryptHttpInputMessage(inputStream, inputMessage.getHeaders()); } catch (DecryptBodyFailException e) { throw new DecryptBodyFailException(e.getMessage(), e.getCode()); } catch (Exception e) { throw new RuntimeException(e); } } InputStream inputStream = IOUtils.toInputStream(body, StandardCharsets.UTF_8); return new DecryptHttpInputMessage(inputStream, inputMessage.getHeaders()); } @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; } //这个方法为获取前端给后端用于加密aeskey的rsa公钥 public static String getJsPublicKey() { return jsPublicKey; } }
import com.alibaba.fastjson.JSON; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.longsec.encrypt.annotation.SecurityParameter; import com.longsec.encrypt.utils.ApiEncryptResult; import com.longsec.encrypt.utils.ApiSecurityUtils; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.Order; import org.springframework.http.MediaType; 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; /** * @author Mr.Jie * @version v1.0 * @description 响应数据的加密处理 * 本类只对控制器参数中含有<strong>{@link org.springframework.web.bind.annotation.ResponseBody}</strong> * 或者控制类上含有<strong>{@link org.springframework.web.bind.annotation.RestController}</strong> * @date 2024/8/12 上午9:43 **/ @Order(1) @ControllerAdvice @Slf4j public class EncryptResponseBodyAdvice implements ResponseBodyAdvice { private final ObjectMapper objectMapper; public EncryptResponseBodyAdvice(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Override public boolean supports(MethodParameter methodParameter, Class aClass) { //这里设置成false 它就不会再走这个类了 return methodParameter.getMethod().isAnnotationPresent(SecurityParameter.class); } @Override public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { if (body == null) { return null; } serverHttpResponse.getHeaders().setContentType(MediaType.TEXT_PLAIN); String formatStringBody = null; try { formatStringBody = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(body); } catch (JsonProcessingException e) { e.printStackTrace(); } log.info("开始对返回值进行加密操作!"); // 定义是否解密 boolean encode = false; //获取注解配置的包含和去除字段 SecurityParameter serializedField = methodParameter.getMethodAnnotation(SecurityParameter.class); //入参是否需要解密 if (serializedField != null) { encode = serializedField.isOutEncode(); } log.info("对方法method :【" + methodParameter.getMethod().getName() + "】数据进行加密!"); if (encode) { String jsPublicKey = DecodeRequestBodyAdvice.getJsPublicKey(); if (StringUtils.isNotBlank(jsPublicKey)) { ApiEncryptResult apiEncryptRes; try { apiEncryptRes = ApiSecurityUtils.encrypt(JSON.toJSONString(formatStringBody), jsPublicKey); } catch (Exception e) { throw new RuntimeException(e); } return apiEncryptRes; } } return body; } }
import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Map; /** * @author Mr.Jie * @version v1.0 * @description 获取RSA公钥接口 * @date 2024/8/11 下午4:04 **/ @CrossOrigin @RestController @RequestMapping("api/encrypt") @RequiredArgsConstructor public class EncryptApi { private final RedisUtils redisUtil; @GetMapping("/getPublicKey") public Object getPublicKey() throws Exception { //获取当前登陆账号对应的token,这行代码就不贴了。 String token = "42608991"; String publicKey = ""; if (StringUtils.isNotBlank(token)) { Map<String, String> stringStringMap = RSAUtils.genKeyPair(); publicKey = stringStringMap.get("publicKey"); String privateKey = stringStringMap.get("privateKey"); String md5Token = MD5Util.md5(token); //这个地方的存放时间根据你的token存放时间走 redisUtil.setCacheObject(md5Token + "publicKey", publicKey); redisUtil.setCacheObject(md5Token + "privateKey", privateKey); } return R.ok(publicKey); } }
import com.alibaba.fastjson.JSONObject; import com.l.api.param.ApiParam; import com.l.common.annotation.Log; import com.l.common.core.controller.BaseController; import com.l.common.enums.BusinessType; import com.l.common.encrypt.annotation.SecurityParameter; import com.l.tools.HttpClientUtil; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; /** * @author Mr.Jie * @version v1.0 * @description 统一对外接口 * @date 2024/8/1 下午3:59 **/ @Api(tags = "统一对外接口") @CrossOrigin @Controller @RequiredArgsConstructor @RequestMapping("/api/uniformInterface") public class UniformInterfaceApi extends BaseController { private final HttpClientUtil httpClientUtil; @PostMapping("/invoke") @ResponseBody @Log(title = "统一接口", businessType = BusinessType.API) @ApiOperation(value = "统一接口", produces = "application/json") @SecurityParameter public Object invoke(@RequestBody ApiParam dto) { try { JSONObject param = JSONObject.parseObject(dto.getJsonParam()); Object obj = httpClientUtil.getDubboService(dto.getServiceKey(), param, true); return success(obj); } catch (Exception e) { return error("服务调用异常:" + e.getMessage()); } } }
import Axios from 'axios' import {getRsaKeys, rsaEncrypt, rsaDecrypt, aesDecrypt, aesEncrypt, get16RandomNum} from '../utii/encrypt' /** * axios实例 * @type {AxiosInstance} */ const instance = Axios.create({ headers: { x_requested_with: 'XMLHttpRequest' } }) let frontPrivateKey /** * axios请求过滤器 */ instance.interceptors.request.use( async config => { if (sessionStorage.getItem('X-Access-Token')) { // 判断是否存在token,如果存在的话,则每个http header都加上token config.headers['X-Access-Token'] = sessionStorage.getItem('X-Access-Token') } if (config.headers['isEncrypt']) { // config.headers['Content-Type'] = 'application/json;charset=utf-8' if (config.method === 'post' || config.method === 'put') { const {privateKey, publicKey} = await getRsaKeys() let afterPublicKey = sessionStorage.getItem('afterPublicKey') frontPrivateKey = privateKey //每次请求生成aeskey let aesKey = get16RandomNum() //用登陆后后端生成并返回给前端的的RSA密钥对的公钥将AES16位密钥进行加密 let aesKeyByRsa = rsaEncrypt(aesKey, afterPublicKey) //使用AES16位的密钥将请求报文加密(使用的是加密前的aes密钥) if (config.data) { let data = aesEncrypt(aesKey, JSON.stringify(config.data)) config.data = { data: data, aeskey: aesKeyByRsa, frontPublicKey: publicKey } } if (config.params) { let data = aesEncrypt(aesKey, JSON.stringify(config.params)) config.params = { params: data, aeskey: aesKeyByRsa, frontPublicKey: publicKey } } } } return config }, err => { return Promise.reject(err) } ) /** * axios响应过滤器 */ instance.interceptors.response.use( response => { //后端返回的通过rsa加密后的aes密钥 let aesKeyByRsa = response.data.aesKeyByRsa if (aesKeyByRsa) { //通过rsa的私钥对后端返回的加密的aeskey进行解密 let aesKey = rsaDecrypt(aesKeyByRsa, frontPrivateKey) //使用解密后的aeskey对加密的返回报文进行解密 var result = response.data.data; result = JSON.parse(JSON.parse(aesDecrypt(aesKey, result))) return result } else { return response.data } } ) export default instance
export const saveStudent = (data) => {
return axios.request({
url: api.invoke,
method: 'post',
headers: {
//需要加密的请求在头部塞入标识
isEncrypt: 1
},
data
})
}
请求加密数据
返回加密数据
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。