spring数据加解密方法
问题描述
原有前端数据传输是通过明文传输的,现处于安全性考虑,需要对敏感信息加密(将加密后的数据存储到一个新的加密字段,后端通过解密这个字段获取数据)
解决方案
通过注解实现
思路
在需要进行解密的方法,然后通过切面前置处理(进行数据解密)
demo 实现
定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Decrypt {
}
定义请求 model
@Data
public class Person {
private String name;
private String address;
private Integer age;
/**
* 加密传输数据的字段
*/
private String encryptData;
}
定义controller
@Slf4j
@RestController
public class DecryptController {
/**
* 测试 http://localhost:8080/annotation
* 全参
* {
* "encryptData":"eyJhZGRyZXNzIjoi5rW35bqV5Lik5LiH6YeMIiwiYWdlIjoxOCwibmFtZSI6IuWlleaZqCJ9"
* }
*
* 缺 age 字段
* {
* "encryptData":"eyJhZGRyZXNzIjoi5rW35bqV5Lik5LiH6YeMIiwibmFtZSI6IuWlleaZqCJ9"
* }
*/
@RequestMapping("/annotation")
@Decrypt
public String annotationDecrypt(@RequestBody @Valid Person person){
log.info("通过注解解密数据");
return JSON.toJSONString(person);
}
@RequestMapping("/filter")
public String filterDecrypt(@RequestBody @Valid Person person){
log.info("通过过滤器和拦截器解密数据");
return JSON.toJSONString(person);
}
}
定义 Aspect
@Aspect
@Slf4j
@Component
public class DecryptAspect {
@Pointcut("@annotation(com.yichen.decryptdemo.annotation.Decrypt)")
public void decryptPointCut() {
}
@Before("decryptPointCut()")
public void decrypt(JoinPoint joinPoint){
// base64 测试
// 这里我单纯的模拟,用 Base64 加解密,一般都是通过对称加密或非对称加密实现
// 这里简化了,我直接转换了,一般需要做判断,我的实现是定义一个接口,接口有定义一个获取加密字段的方法。
Object[] args = joinPoint.getArgs();
log.info("请求入参 {}",JSON.toJSONString(args));
try {
Person person = (Person)args[0];
String decryptString = new String(Base64.getDecoder().decode(person.getEncryptData().getBytes(StandardCharsets.UTF_8)),"UTF-8");
JSONObject jsonObject = JSON.parseObject(decryptString);
// 这里简化了,可以用反射。
person.setName(String.valueOf(jsonObject.get("name")));
person.setAddress(String.valueOf(jsonObject.get("address")));
person.setAge(Integer.valueOf(String.valueOf(jsonObject.get("age"))));
}
catch (Exception e){
log.error("解密出错 {}",e.getMessage(),e);
}
}
}
请求测试
这里我下掉了annotationDecrypt()
方法中的注解 @Valid
缺陷
该实现存在一个问题,即原来入参如果使用了如@NotEmpty()
等字段校验注解时,会直接被校验住并抛出错误,被全局异常处理器拦截然后返给前端。这里即使给我们自定的切面最高优先级@Order(Ordered.HIGHEST_PRECEDENCE)
也是不起作用的,因为@Valid
的处理时机先于我们自定义的Aspect
无age参数请求
{
“encryptData”:“eyJhZGRyZXNzIjoi5rW35bqV5Lik5LiH6YeMIiwibmFtZSI6IuWlleaZqCJ9”
}
全参请求
{
“encryptData”:“eyJhZGRyZXNzIjoi5rW35bqV5Lik5LiH6YeMIiwiYWdlIjoxOCwibmFtZSI6IuWlleaZqCJ9”
}
这种情况下,只能无奈的把校验下掉,手动进行参数判断,略痛苦。
通过过滤器和拦截器实现
前置条件
把
DecryptAspect
中@Component
注了
思路
通过自定义filter
包装setvletRequest
,通过interceptor
进行数据解密
不单独使用interceptor的原因是因为,interceptor只能进行读取请求数据(request.getInputStream()),但是无法反写。
demo 实现
filter定义
@Slf4j
public class DecryptFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 为什么使用自定义类包装 request,因为 request 一旦读取后,无法在此读取,也不能添加入参,这里包装后重写 getInputStream() 方法即可克服以上功能
DecryptServletRequestWrapper requestWrapper = null;
try {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
requestWrapper = new DecryptServletRequestWrapper(httpServletRequest);
}
catch (Exception e){
log.error("DecryptServletRequestWrapper error {}",e.getMessage(),e);
}
chain.doFilter((Objects.isNull(requestWrapper) ? request : requestWrapper),response);
}
}
interceptor定义
@Slf4j
public class DecryptInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {
if (request instanceof DecryptServletRequestWrapper){
DecryptServletRequestWrapper requestWrapper = (DecryptServletRequestWrapper) request;
Map<String, Object> requestBody = requestWrapper.getRequestBody();
// 这里可以处理解密逻辑,我这里直接赋值了,就不解密了
requestBody.put("name","yichen");
requestBody.put("address","海底两万里");
// 对比结果
// requestBody.put("age",18);
DecryptServletRequestWrapper.printMap(requestBody);
}
return true;
}
}
request wrapper
@Slf4j
public class DecryptServletRequestWrapper extends HttpServletRequestWrapper {
private Map<String,Object> requestBody;
/**
* Constructs a request object wrapping the given request.
*
* @param request The request to wrap
* @throws IllegalArgumentException if the request is null
*/
public DecryptServletRequestWrapper(HttpServletRequest request) {
super(request);
// 这里手动赋值,可以是 解密数据
requestBody = new HashMap<>(16);
JSONObject params = getJsonParam(request);
requestBody.putAll(params);
printMap(requestBody);
}
@Override
public ServletInputStream getInputStream() throws IOException {
ByteArrayInputStream inputStream = new ByteArrayInputStream(JSON.toJSONString(requestBody).getBytes(StandardCharsets.UTF_8));
return new ServletInputStream() {
@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 inputStream.read();
}
};
}
public Map<String, Object> getRequestBody() {
return requestBody;
}
public void setRequestBody(Map<String, Object> requestBody) {
this.requestBody = requestBody;
}
/**
* 获取 入参
* @param request 请求
* @return 入参结果集
*/
public static JSONObject getJsonParam(HttpServletRequest request) {
JSONObject jsonParam = null;
try {
// 获取输入流
BufferedReader streamReader = new BufferedReader(new InputStreamReader(request.getInputStream(), "UTF-8"));
// 写入数据到Stringbuilder
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = streamReader.readLine()) != null) {
sb.append(line);
}
jsonParam = JSONObject.parseObject(sb.toString());
} catch (Exception e) {
log.error(e.getMessage());
}
log.info("getJsonParam => {}", JSON.toJSONString(jsonParam));
return jsonParam;
}
public static void printMap(Map<String,Object> params){
params.forEach((key, value) -> log.info("key => {}, value => {}", key, value));
}
}
解密配置
@Configuration
public class DecryptConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
DecryptInterceptor interceptor = new DecryptInterceptor();
registry.addInterceptor(interceptor);
}
@Bean
public FilterRegistrationBean<?> decryptDataRegistrationBean(){
DecryptFilter filter = new DecryptFilter();
FilterRegistrationBean<DecryptFilter> bean = new FilterRegistrationBean<>();
bean.setName("decrypt-data");
bean.setFilter(filter);
bean.addUrlPatterns("/*");
bean.setOrder(Ordered.LOWEST_PRECEDENCE);
return bean;
}
}
测试
这里我通过在DecryptInterceptor
手动模拟解密来比对
传全参
@Slf4j
public class DecryptInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {
if (request instanceof DecryptServletRequestWrapper){
DecryptServletRequestWrapper requestWrapper = (DecryptServletRequestWrapper) request;
Map<String, Object> requestBody = requestWrapper.getRequestBody();
// 这里可以处理解密逻辑,我这里直接赋值了,就不解密了
requestBody.put("name","yichen");
requestBody.put("address","海底两万里");
// 对比结果
requestBody.put("age",18);
DecryptServletRequestWrapper.printMap(requestBody);
}
return true;
}
}
少传age字段
@Slf4j
public class DecryptInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {
if (request instanceof DecryptServletRequestWrapper){
DecryptServletRequestWrapper requestWrapper = (DecryptServletRequestWrapper) request;
Map<String, Object> requestBody = requestWrapper.getRequestBody();
// 这里可以处理解密逻辑,我这里直接赋值了,就不解密了
requestBody.put("name","yichen");
requestBody.put("address","海底两万里");
// 对比结果
// requestBody.put("age",18);
DecryptServletRequestWrapper.printMap(requestBody);
}
return true;
}
}
结论
这种方式实现可以完全适配原有的代码逻辑,字段校验也不存在冲突。
参考
springboot HandlerIntercepter拦截器实现修改request body数据
心得
代码demo
1、加密方式最好统一,别不同端不同的加密方式。。。。
2、请求入参最好规范化,别大多数用了泛型适配如 RequestData,而某几个接口突然入参一个 @RequestParam("mobile")String mobile
,这很难受。
3、尽量代码复用,像这种加密,有一种偷懒的方式是在每个要加密的接口前置处理(一段代码到处拷贝。改起来也有点痛苦)