技术标签: spring boot 解决方案 java 后端
在项目开发工程中,有的项目可能对参数安全要求比较高,在整个http数据传输的过程中都需要对请求参数、响应参数进行加密,也就是说整个请求响应的过程都是加密处理的,不在浏览器上暴露请求参数、响应参数的真实数据。
补充:
也可以用于单点登录,在请求参数中添加时间戳,后台解析请求参数对时间戳进行校验,比如当前时间和请求参数中的时间戳相差多少秒、分钟才能进行放行,返回token。这样做的好处在于请求端每次加密之后的密文都是变化的,也能够避免携带相同的报文可以重复的登录。
1.引入依赖, 创建SpringBoot工程
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.22</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.8.7</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
/**
* @description: : 请求参数解密
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DecryptionAnnotation {
}
/**
* @description: 响应参数加密
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EncryptionAnnotation {
}
DecryptRequestBodyAdvice:请求参数解密,针对post请求
package com.llp.crypto.advice;
import cn.hutool.json.JSONUtil;
import com.llp.crypto.annotation.DecryptionAnnotation;
import com.llp.crypto.utils.AESUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
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.bind.annotation.PostMapping;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
/**
* @description: 请求参数解密,针对post请求
*/
@Slf4j
@ControllerAdvice
public class DecryptRequestBodyAdvice implements RequestBodyAdvice {
/**
* 方法上有DecryptionAnnotation注解的,进入此拦截器
*
* @param methodParameter 方法参数对象
* @param targetType 参数的类型
* @param converterType 消息转换器
* @return true,进入,false,跳过
*/
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.hasMethodAnnotation(DecryptionAnnotation.class);
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
try {
return new MyHttpInputMessage(inputMessage, parameter);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 转换之后,执行此方法,解密,赋值
*
* @param body spring解析完的参数
* @param inputMessage 输入参数
* @param parameter 参数对象
* @param targetType 参数类型
* @param converterType 消息转换类型
* @return 真实的参数
*/
@SneakyThrows
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
log.info("解密后的请求报文:{}", body);
return body;
}
/**
* 如果body为空,转为空对象
*
* @param body spring解析完的参数
* @param inputMessage 输入参数
* @param parameter 参数对象
* @param targetType 参数类型
* @param converterType 消息转换类型
* @return 真实的参数
*/
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
class MyHttpInputMessage implements HttpInputMessage {
private HttpHeaders headers;
private InputStream body;
private MethodParameter parameter;
public MyHttpInputMessage(HttpInputMessage inputMessage, MethodParameter parameter) throws Exception {
this.headers = inputMessage.getHeaders();
//只对post请求进行加密
if (parameter.hasMethodAnnotation(PostMapping.class)) {
/*
*请求报文示例:
* {
* "requestData":"JF7kvl9Wd/vgdmAS8JijsQ=="
* }
*/
String decrypt = AESUtil.decrypt(easpData(IOUtils.toString(inputMessage.getBody(), "UTF-8")));
log.info("解密后的请求参数:{}", decrypt);
this.body = IOUtils.toInputStream(decrypt, "UTF-8");
} else {
this.body = inputMessage.getBody();
}
}
@Override
public InputStream getBody() throws IOException {
return body;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
}
public String easpData(String requestData) {
if (requestData != null && !requestData.equals("")) {
String start = "requestData";
if (requestData.contains(start)) {
return JSONUtil.parseObj(requestData).getStr(start);
} else {
throw new RuntimeException("参数【requestData】缺失异常!");
}
}
return "";
}
}
GetDeleteDecryptAspect:针对get、delete请求参数进行解密
@Aspect
//值越小优先级越高
@Order(-1)
@Component
@Slf4j
public class GetDeleteDecryptAspect {
/**
* 对get、delete方法进行解密
* @param point
* @return
* @throws Throwable
*/
@Around("@annotation(com.llp.crypto.annotation.DecryptionAnnotation) && " + "(@annotation(org.springframework.web.bind.annotation.GetMapping) || @annotation(org.springframework.web.bind.annotation.DeleteMapping))")
public Object aroundMethod(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
// 获取到请求的参数列表
Object[] args = point.getArgs();
// 判断方法请求参数是否需要解密
if (method.isAnnotationPresent(DecryptionAnnotation.class)) {
try {
this.decrypt(args, point);
log.info("返回解密结果=" + args);
} catch (Exception e) {
e.printStackTrace();
log.error("对方法method :【" + method.getName() + "】入参数据进行解密出现异常:" + e.getMessage());
}
}
// 执行将解密的结果交给控制器进行处理,并返回处理结果
return point.proceed(args);
}
/**
* 前端对请求参数进行加密,最终将这个加密的字符串已 localhost:48080?data=xxx这样的方式进行传递
* 后端后去到 data的数据进行解密最终得到解密后的数据
* @param args
* @param point
* @throws Exception
*/
// 解密方法
@SuppressWarnings("unchecked")
public void decrypt(Object[] args, ProceedingJoinPoint point) throws Exception {
ServletRequestAttributes sc = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = sc.getRequest();
String data = request.getParameter("data");
log.info("data: " + data);
// 将密文解密为JSON字符串
Class<?> aClass = args[0].getClass();
log.info("数据类型:{}",aClass.getClass());
if (StringUtils.isNotEmpty(data)) {
// 将JSON字符串转换为Map集合,并替换原本的参数
args[0] = JSONUtil.toBean(AESUtil.decrypt(data), args[0].getClass());
}
}
}
EncryptResponseBodyAdvice:响应参数解密,针对统一返回结果类的装配
/**
* @description: 响应加密
*/
@Slf4j
@ControllerAdvice
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
return methodParameter.hasMethodAnnotation(EncryptionAnnotation.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
log.info("对方法method :【" + methodParameter.getMethod().getName() + "】返回数据进行加密");
// 只针对回参类型为CommonResult的对象,进行加密
if (body instanceof CommonResult) {
CommonResult commonResult = (CommonResult) body;
Object data = commonResult.getData();
if (Objects.nonNull(data)) {
// 将响应结果转换为json格式
String result = JSONUtil.toJsonStr(data);
log.info("返回结果:{}", result);
try {
String encrypt = AESUtil.encrypt(result);
commonResult.setData(encrypt);
log.info("返回结果加密=" + commonResult);
} catch (Exception e) {
log.error("对方法method :【" + methodParameter.getMethod().getName() + "】返回数据进行解密出现异常:" + e.getMessage());
}
return commonResult;
}
}
return body;
}
}
@Data
public class CommonResult<T> {
private String code;
private String msg;
private T data;
public CommonResult() {
}
public CommonResult(T data) {
this.data = data;
}
/**
* 表示成功的Result,不携带返回数据
*
* @return
*/
public static CommonResult success() {
CommonResult result = new CommonResult();
result.setCode("200");
result.setMsg("success");
return result;
}
/**
* 便是成功的Result,携带返回数据
* 如果需要在static方法使用泛型,需要在static后指定泛型表示 static<T>
*
* @param data
* @return
*/
public static <T> CommonResult<T> success(T data) {
CommonResult<T> result = new CommonResult<>(data);
result.setCode("200");
result.setMsg("success");
return result;
}
/**
* 失败不携带数据
* 将错误的code、msg作为形参,灵活传入
*
* @param code
* @param msg
* @return
*/
public static CommonResult error(String code, String msg) {
CommonResult result = new CommonResult();
result.setCode(code);
result.setMsg(msg);
return result;
}
/**
* 失败携带数据
* 将错误的code、msg、data作为形参,灵活传入
* @param code
* @param msg
* @param data
* @param <T>
* @return
*/
public static <T> CommonResult<T> error(String code, String msg, T data) {
CommonResult<T> result = new CommonResult<>(data);
result.setCode(code);
result.setMsg(msg);
return result;
}
}
public class AESUtil {
// 加解密方式
private static final String AES_ALGORITHM = "AES/ECB/PKCS5Padding";
// 与前端统一好KEY
private static final String KEY = "abcdsxyzhkj12345";
// 获取 cipher
private static Cipher getCipher(byte[] key, int model) throws Exception {
SecretKeySpec secretKeySpec = new SecretKeySpec(KEY.getBytes(), "AES");
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(model, secretKeySpec);
return cipher;
}
// AES加密
public static String encrypt(String data) throws Exception {
Cipher cipher = getCipher(KEY.getBytes(), Cipher.ENCRYPT_MODE);
return Base64.getEncoder().encodeToString(cipher.doFinal(data.getBytes("UTF-8")));
}
// AES解密
public static String decrypt(String data) throws Exception {
Cipher cipher = getCipher(KEY.getBytes(), Cipher.DECRYPT_MODE);
return new String(cipher.doFinal(Base64.getDecoder().decode(data.getBytes("UTF-8"))),"UTF-8");
}
public static byte[] decryptUrl(String url) throws Exception {
Cipher cipher = getCipher(KEY.getBytes(), Cipher.DECRYPT_MODE);
return cipher.doFinal(Base64.getDecoder().decode(url.replaceAll(" +", "+")));
}
// AES解密MySQL AES_ENCRYPT函数加密密文
public static String aesDecryptMySQL(String key, String content){
try {
SecretKey secretKey = generateMySQLAESKey(key,"ASCII");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] cleartext = Hex.decodeHex(content.toCharArray());
byte[] ciphertextBytes = cipher.doFinal(cleartext);
return new String(ciphertextBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
//加密
public static String aesEncryptMySQL(String key2, String content) {
try {
SecretKey key = generateMySQLAESKey(key2,"ASCII");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] cleartext = content.getBytes("UTF-8");
byte[] ciphertextBytes = cipher.doFinal(cleartext);
return new String(Hex.encodeHex(ciphertextBytes));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static SecretKeySpec generateMySQLAESKey(final String key, final String encoding) {
try {
final byte[] finalKey = new byte[16];
int i = 0;
for(byte b : key.getBytes(encoding)) {
finalKey[i++%16] ^= b;
}
return new SecretKeySpec(finalKey, "AES");
} catch(UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
@Test
public void decodeTest() {
try {
String a = "{\"username\":\"admin\",\"deptId\":\"1250500000\",\"userId\":\"1\",\"phone\":\"15195928695\"}";
String encrypt = AESUtil.encrypt(a);
System.out.println("加密后的字符串: "+encrypt);
System.out.println("解密后的字符串:" +AESUtil.decrypt(encrypt));
String str = "5tAayXF5ZcPC9yoNvBIT0fw2Li2uoxUhGyMq4JKUvCttOFnU7iKovyB9pm/ZV+2qU8h2htdk5s6ht9kCpTGG9WZAGTdMUgIJkD/Tf6IQ3gw=";
String decrypt = AESUtil.decrypt(IOUtils.toString(str.getBytes(), "UTF-8"));
System.out.println(decrypt);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 请求流支持多次获取
*/
public class InputStreamHttpServletRequestWrapper extends HttpServletRequestWrapper {
/**
* 用于缓存输入流
*/
private ByteArrayOutputStream cachedBytes;
public InputStreamHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public ServletInputStream getInputStream() throws IOException {
if (cachedBytes == null) {
// 首次获取流时,将流放入 缓存输入流 中
cacheInputStream();
}
// 从 缓存输入流 中获取流并返回
return new CachedServletInputStream(cachedBytes.toByteArray());
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
/**
* 首次获取流时,将流放入 缓存输入流 中
*/
private void cacheInputStream() throws IOException {
// 缓存输入流以便多次读取。为了方便, 我使用 org.apache.commons IOUtils
cachedBytes = new ByteArrayOutputStream();
IOUtils.copy(super.getInputStream(), cachedBytes);
}
/**
* 读取缓存的请求正文的输入流
* <p>
* 用于根据 缓存输入流 创建一个可返回的
*/
public static class CachedServletInputStream extends ServletInputStream {
private final ByteArrayInputStream input;
public CachedServletInputStream(byte[] buf) {
// 从缓存的请求正文创建一个新的输入流
input = new ByteArrayInputStream(buf);
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public int read() throws IOException {
return input.read();
}
}
}
1.测试类
@Slf4j
@RestController
@Api(tags = "测试加密解密")
public class TestController {
/**
* 请求示例:
* {
* "requestData":"5tAayXF5ZcPC9yoNvBIT0fw2Li2uoxUhGyMq4JKUvCttOFnU7iKovyB9pm/ZV+2qU8h2htdk5s6ht9kCpTGG9WZAGTdMUgIJkD/Tf6IQ3gw="
* }
*
* @return
*/
@PostMapping(value = "/postEncrypt")
@ApiOperation("测试post加密")
@EncryptionAnnotation
@DecryptionAnnotation
public CommonResult<String> postEncrypt(@RequestBody UserReqVO userReqVO) {
System.out.println("userReqVO: ============>" + userReqVO);
return CommonResult.success("成功");
}
@GetMapping(value = "/getEncrypt")
@ApiOperation("测试get加密")
@DecryptionAnnotation // requestBody 自动解密
public CommonResult<UserReqVO> getEncrypt(String data) {
log.info("解密后的数据:{}",data);
UserReqVO userReqVO = JSONUtil.toBean(data, UserReqVO.class);
//UserReqVO(username=admin, deptId=1250500000, userId=1, phone=15195928695)
log.info("用户信息:{}",userReqVO);
return CommonResult.success(userReqVO);
}
}
@ApiModel(description = "用户请求vo")
@Data
public class UserReqVO {
@ApiModelProperty(value = "用户名", required = true)
private String username;
@ApiModelProperty(value = "部门id",required = true)
private Long deptId;
@ApiModelProperty(value = "用户id",required = true)
private Long userId;
@ApiModelProperty(value = "电话号码",required = true)
private String phone;
}
测试结果
文章浏览阅读1k次。通过使用ajax方法跨域请求是浏览器所不允许的,浏览器出于安全考虑是禁止的。警告信息如下:不过jQuery对跨域问题也有解决方案,使用jsonp的方式解决,方法如下:$.ajax({ async:false, url: 'http://www.mysite.com/demo.do', // 跨域URL ty..._nginx不停的xhr
文章浏览阅读2k次。关于在 Oracle 中配置 extproc 以访问 ST_Geometry,也就是我们所说的 使用空间SQL 的方法,官方文档链接如下。http://desktop.arcgis.com/zh-cn/arcmap/latest/manage-data/gdbs-in-oracle/configure-oracle-extproc.htm其实简单总结一下,主要就分为以下几个步骤。..._extproc
文章浏览阅读1.5w次。linux下没有上面的两个函数,需要使用函数 mbstowcs和wcstombsmbstowcs将多字节编码转换为宽字节编码wcstombs将宽字节编码转换为多字节编码这两个函数,转换过程中受到系统编码类型的影响,需要通过设置来设定转换前和转换后的编码类型。通过函数setlocale进行系统编码的设置。linux下输入命名locale -a查看系统支持的编码_linux c++ gbk->utf8
文章浏览阅读750次。今天准备从生产库向测试库进行数据导入,结果在imp导入的时候遇到“ IMP-00009:导出文件异常结束” 错误,google一下,发现可能有如下原因导致imp的数据太大,没有写buffer和commit两个数据库字符集不同从低版本exp的dmp文件,向高版本imp导出的dmp文件出错传输dmp文件时,文件损坏解决办法:imp时指定..._imp-00009导出文件异常结束
文章浏览阅读143次。当下是一个大数据的时代,各个行业都离不开数据的支持。因此,网络爬虫就应运而生。网络爬虫当下最为火热的是Python,Python开发爬虫相对简单,而且功能库相当完善,力压众多开发语言。本次教程我们爬取前程无忧的招聘信息来分析Python程序员需要掌握那些编程技术。首先在谷歌浏览器打开前程无忧的首页,按F12打开浏览器的开发者工具。浏览器开发者工具是用于捕捉网站的请求信息,通过分析请求信息可以了解请..._初级python程序员能力要求
文章浏览阅读7.6k次,点赞2次,收藏6次。@Service标注的bean,类名:ABDemoService查看源码后发现,原来是经过一个特殊处理:当类的名字是以两个或以上的大写字母开头的话,bean的名字会与类名保持一致public class AnnotationBeanNameGenerator implements BeanNameGenerator { private static final String C..._@service beanname
文章浏览阅读6.9w次,点赞73次,收藏463次。1.前序创建#include<stdio.h>#include<string.h>#include<stdlib.h>#include<malloc.h>#include<iostream>#include<stack>#include<queue>using namespace std;typed_二叉树的建立
文章浏览阅读7.1k次。在Asp.net上使用Excel导出功能,如果文件名出现中文,便会以乱码视之。 解决方法: fileName = HttpUtility.UrlEncode(fileName, System.Text.Encoding.UTF8);_asp.net utf8 导出中文字符乱码
文章浏览阅读2.1k次,点赞4次,收藏23次。第一次实验 词法分析实验报告设计思想词法分析的主要任务是根据文法的词汇表以及对应约定的编码进行一定的识别,找出文件中所有的合法的单词,并给出一定的信息作为最后的结果,用于后续语法分析程序的使用;本实验针对 PL/0 语言 的文法、词汇表编写一个词法分析程序,对于每个单词根据词汇表输出: (单词种类, 单词的值) 二元对。词汇表:种别编码单词符号助记符0beginb..._对pl/0作以下修改扩充。增加单词
文章浏览阅读773次。我在使用adb.exe时遇到了麻烦.我想使用与bash相同的adb.exe shell提示符,所以我决定更改默认的bash二进制文件(当然二进制文件是交叉编译的,一切都很完美)更改bash二进制文件遵循以下顺序> adb remount> adb push bash / system / bin /> adb shell> cd / system / bin> chm..._adb shell mv 权限
文章浏览阅读6.8k次,点赞12次,收藏125次。1. 单目相机标定引言相机标定已经研究多年,标定的算法可以分为基于摄影测量的标定和自标定。其中,应用最为广泛的还是张正友标定法。这是一种简单灵活、高鲁棒性、低成本的相机标定算法。仅需要一台相机和一块平面标定板构建相机标定系统,在标定过程中,相机拍摄多个角度下(至少两个角度,推荐10~20个角度)的标定板图像(相机和标定板都可以移动),即可对相机的内外参数进行标定。下面介绍张氏标定法(以下也这么称呼)的原理。原理相机模型和单应矩阵相机标定,就是对相机的内外参数进行计算的过程,从而得到物体到图像的投影_相机-投影仪标定
文章浏览阅读2.2k次。文章目录Wayland 架构Wayland 渲染Wayland的 硬件支持简 述: 翻译一篇关于和 wayland 有关的技术文章, 其英文标题为Wayland Architecture .Wayland 架构若是想要更好的理解 Wayland 架构及其与 X (X11 or X Window System) 结构;一种很好的方法是将事件从输入设备就开始跟踪, 查看期间所有的屏幕上出现的变化。这就是我们现在对 X 的理解。 内核是从一个输入设备中获取一个事件,并通过 evdev 输入_wayland