1. 介绍 在我们日常的java开发中,免不了和其他系统的业务交互,或者微服务之间的接口调用
如果我们想保证数据传输的安全,对接口出参加密,入参解密。
但是不想写重复代码,我们可以提供一个通用starter,提供通用加密解密功能
基于 spring boot + mybatis plus + vue & element 实现的后台管理系统 + 用户小程序,支持 rbac 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/yunaiv/ruoyi-vue-pro 视频教程:https://doc.iocoder.cn/video/ 2. 前置知识 2.1 hutool-crypto加密解密工具 hutool-crypto提供了很多加密解密工具,包括对称加密,非对称加密,摘要加密等等,这不做详细介绍。
2.2 request流只能读取一次的问题 2.2.1 问题: 在接口调用链中,request的请求流只能调用一次,处理之后,如果之后还需要用到请求流获取数据,就会发现数据为空。
比如使用了filter或者aop在接口处理之前,获取了request中的数据,对参数进行了校验,那么之后就不能在获取request请求流了
2.2.2 解决办法 继承httpservletrequestwrapper,将请求中的流copy一份,复写getinputstream和getreader方法供外部使用。每次调用后的getinputstream方法都是从复制出来的二进制数组中进行获取,这个二进制数组在对象存在期间一致存在。
使用filter过滤器,在一开始,替换request为自己定义的可以多次读取流的request。
这样就实现了流的重复获取
inputstreamhttpservletrequestwrapper
package xyz.hlh.cryptotest.utils;import org.apache.commons.io.ioutils;import javax.servlet.readlistener;import javax.servlet.servletinputstream;import javax.servlet.http.httpservletrequest;import javax.servlet.http.httpservletrequestwrapper;import java.io.bufferedreader;import java.io.bytearrayinputstream;import java.io.bytearrayoutputstream;import java.io.ioexception;import java.io.inputstreamreader;/** * 请求流支持多次获取 */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); } /** * 读取缓存的请求正文的输入流 * * 用于根据 缓存输入流 创建一个可返回的 */ 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(); } }} httpservletrequestinputstreamfilter
package xyz.hlh.cryptotest.filter;import org.springframework.core.annotation.order;import org.springframework.stereotype.component;import xyz.hlh.cryptotest.utils.inputstreamhttpservletrequestwrapper;import javax.servlet.filter;import javax.servlet.filterchain;import javax.servlet.servletexception;import javax.servlet.servletrequest;import javax.servlet.servletresponse;import javax.servlet.http.httpservletrequest;import java.io.ioexception;import static org.springframework.core.ordered.highest_precedence;/** * @author hlh * @description: * 请求流转换为多次读取的请求流 过滤器 * @email 17703595860@163.com * @date : created in 2022/2/4 9:58 */@component@order(highest_precedence + 1) // 优先级最高public class httpservletrequestinputstreamfilter implements filter { @override public void dofilter(servletrequest request, servletresponse response, filterchain chain) throws ioexception, servletexception { // 转换为可以多次获取流的request httpservletrequest httpservletrequest = (httpservletrequest) request; inputstreamhttpservletrequestwrapper inputstreamhttpservletrequestwrapper = new inputstreamhttpservletrequestwrapper(httpservletrequest); // 放行 chain.dofilter(inputstreamhttpservletrequestwrapper, response); }} 2.3 springboot的参数校验validation 为了减少接口中,业务代码之前的大量冗余的参数校验代码
springboot-validation提供了优雅的参数校验,入参都是实体类,在实体类字段上加上对应注解,就可以在进入方法之前,进行参数校验,如果参数错误,会抛出错误bindexception,是不会进入方法的。
这种方法,必须要求在接口参数上加注解@validated或者是@valid
但是很多清空下,我们希望在代码中调用某个实体类的校验功能,所以需要如下工具类
paramexception
package xyz.hlh.cryptotest.exception;import lombok.getter;import java.util.list;/** * @author hlh * @description 自定义参数异常 * @email 17703595860@163.com * @date created in 2021/8/10 下午10:56 */@getterpublic class paramexception extends exception { private final list fieldlist; private final list msglist; public paramexception(list fieldlist, list msglist) { this.fieldlist = fieldlist; this.msglist = msglist; }} validationutils
package xyz.hlh.cryptotest.utils;import xyz.hlh.cryptotest.exception.customizeexception;import xyz.hlh.cryptotest.exception.paramexception;import javax.validation.constraintviolation;import javax.validation.validation;import javax.validation.validator;import java.util.linkedlist;import java.util.list;import java.util.set;/** * @author hlh * @description 验证工具类 * @email 17703595860@163.com * @date created in 2021/8/10 下午10:56 */public class validationutils { private static final validator validator = validation.builddefaultvalidatorfactory().getvalidator(); /** * 验证数据 * @param object 数据 */ public static void validate(object object) throws customizeexception { set validate = validator.validate(object); // 验证结果异常 throwparamexception(validate); } /** * 验证数据(分组) * @param object 数据 * @param groups 所在组 */ public static void validate(object object, class ... groups) throws customizeexception { set validate = validator.validate(object, groups); // 验证结果异常 throwparamexception(validate); } /** * 验证数据中的某个字段(分组) * @param object 数据 * @param propertyname 字段名称 */ public static void validate(object object, string propertyname) throws customizeexception { set validate = validator.validateproperty(object, propertyname); // 验证结果异常 throwparamexception(validate); } /** * 验证数据中的某个字段(分组) * @param object 数据 * @param propertyname 字段名称 * @param groups 所在组 */ public static void validate(object object, string propertyname, class ... groups) throws customizeexception { set validate = validator.validateproperty(object, propertyname, groups); // 验证结果异常 throwparamexception(validate); } /** * 验证结果异常 * @param validate 验证结果 */ private static void throwparamexception(set validate) throws customizeexception { if (validate.size() > 0) { list fieldlist = new linkedlist(); list msglist = new linkedlist(); for (constraintviolation next : validate) { fieldlist.add(next.getpropertypath().tostring()); msglist.add(next.getmessage()); } throw new paramexception(fieldlist, msglist); } }} 2.4 自定义starter 自定义starter步骤
创建工厂,编写功能代码
声明自动配置类,把需要对外提供的对象创建好,通过配置类统一向外暴露
在resource目录下准备一个名为spring/spring.factories的文件,以org.springframework.boot.autoconfigure.enableautoconfiguration为key,自动配置类为value列表,进行注册
2.5 requestbodyadvice和responsebodyadvice requestbodyadvice是对请求的json串进行处理, 一般使用环境是处理接口参数的自动解密
responsebodyadvice是对请求相应的jsoin传进行处理,一般用于相应结果的加密
基于 spring cloud alibaba + gateway + nacos + rocketmq + vue & element 实现的后台管理系统 + 用户小程序,支持 rbac 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/yunaiv/yudao-cloud 视频教程:https://doc.iocoder.cn/video/ 3. 功能介绍 接口相应数据的时候,返回的是加密之后的数据 接口入参的时候,接收的是解密之后的数据,但是在进入接口之前,会自动解密,取得对应的数据
4. 功能细节 加密解密使用对称加密的aes算法,使用hutool-crypto模块进行实现
所有的实体类提取一个公共父类,包含属性时间戳,用于加密数据返回之后的实效性,如果超过60分钟,那么其他接口将不进行处理。
如果接口加了加密注解encryptionannotation,并且返回统一的json数据result类,则自动对数据进行加密。如果是继承了统一父类requestbase的数据,自动注入时间戳,确保数据的时效性
如果接口加了解密注解decryptionannotation,并且参数使用requestbody注解标注,传入json使用统一格式requestdata类,并且内容是继承了包含时间长的父类requestbase,则自动解密,并且转为对应的数据类型
功能提供springboot的starter,实现开箱即用
5. 代码实现 https://gitee.com/springboot-hlh/spring-boot-csdn/tree/master/09-spring-boot-interface-crypto
5.1 项目结构 5.2 crypto-common 5.2.1 结构 5.3 crypto-spring-boot-starter 5.3.1 接口 5.3.2 重要代码 crypto.properties aes需要的参数配置
# 模式 cn.hutool.crypto.mode crypto.mode=cts # 补码方式 cn.hutool.crypto.mode crypto.padding=pkcs5padding # 秘钥 crypto.key=testkey123456789 # 盐 crypto.iv=testiv1234567890 spring.factories 自动配置文件
org.springframework.boot.autoconfigure.enableautoconfiguration= xyz.hlh.crypto.config.appconfig cryptconfig aes需要的配置参数
package xyz.hlh.crypto.config;import cn.hutool.crypto.mode;import cn.hutool.crypto.padding;import lombok.data;import lombok.equalsandhashcode;import lombok.getter;import org.springframework.boot.context.properties.configurationproperties;import org.springframework.context.annotation.configuration;import org.springframework.context.annotation.propertysource;import java.io.serializable;/** * @author hlh * @description: aes需要的配置参数 * @email 17703595860@163.com * @date : created in 2022/2/4 13:16 */@configuration@configurationproperties(prefix = crypto)@propertysource(classpath:crypto.properties)@data@equalsandhashcode@getterpublic class cryptconfig implements serializable { private mode mode; private padding padding; private string key; private string iv;} appconfig 自动配置类
package xyz.hlh.crypto.config;import cn.hutool.crypto.symmetric.aes;import org.springframework.context.annotation.bean;import org.springframework.context.annotation.configuration;import javax.annotation.resource;import java.nio.charset.standardcharsets;/** * @author hlh * @description: 自动配置类 * @email 17703595860@163.com * @date : created in 2022/2/4 13:12 */@configurationpublic class appconfig { @resource private cryptconfig cryptconfig; @bean public aes aes() { return new aes(cryptconfig.getmode(), cryptconfig.getpadding(), cryptconfig.getkey().getbytes(standardcharsets.utf_8), cryptconfig.getiv().getbytes(standardcharsets.utf_8)); }} decryptrequestbodyadvice 请求自动解密
package xyz.hlh.crypto.advice;import com.fasterxml.jackson.databind.objectmapper;import lombok.sneakythrows;import org.apache.commons.lang3.stringutils;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.web.bind.annotation.controlleradvice;import org.springframework.web.context.request.requestattributes;import org.springframework.web.context.request.requestcontextholder;import org.springframework.web.context.request.servletrequestattributes;import org.springframework.web.servlet.mvc.method.annotation.requestbodyadvice;import xyz.hlh.crypto.annotation.decryptionannotation;import xyz.hlh.crypto.common.exception.paramexception;import xyz.hlh.crypto.constant.cryptoconstant;import xyz.hlh.crypto.entity.requestbase;import xyz.hlh.crypto.entity.requestdata;import xyz.hlh.crypto.util.aesutil;import javax.servlet.servletinputstream;import javax.servlet.http.httpservletrequest;import java.io.ioexception;import java.lang.reflect.type;/** * @author hlh * @description: requestbody 自动解密 * @email 17703595860@163.com * @date : created in 2022/2/4 15:12 */@controlleradvicepublic class decryptrequestbodyadvice implements requestbodyadvice { @autowired private objectmapper objectmapper; /** * 方法上有decryptionannotation注解的,进入此拦截器 * @param methodparameter 方法参数对象 * @param targettype 参数的类型 * @param convertertype 消息转换器 * @return true,进入,false,跳过 */ @override public boolean supports(methodparameter methodparameter, type targettype, class convertertype) { return methodparameter.hasmethodannotation(decryptionannotation.class); } @override public httpinputmessage beforebodyread(httpinputmessage inputmessage, methodparameter parameter, type targettype, class convertertype) throws ioexception { return inputmessage; } /** * 转换之后,执行此方法,解密,赋值 * @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 convertertype) { // 获取request requestattributes requestattributes = requestcontextholder.getrequestattributes(); servletrequestattributes servletrequestattributes = (servletrequestattributes) requestattributes; if (servletrequestattributes == null) { throw new paramexception(request错误); } httpservletrequest request = servletrequestattributes.getrequest(); // 获取数据 servletinputstream inputstream = request.getinputstream(); requestdata requestdata = objectmapper.readvalue(inputstream, requestdata.class); if (requestdata == null || stringutils.isblank(requestdata.gettext())) { throw new paramexception(参数错误); } // 获取加密的数据 string text = requestdata.gettext(); // 放入解密之前的数据 request.setattribute(cryptoconstant.input_original_data, text); // 解密 string decrypttext = null; try { decrypttext = aesutil.decrypt(text); } catch (exception e) { throw new paramexception(解密失败); } if (stringutils.isblank(decrypttext)) { throw new paramexception(解密失败); } // 放入解密之后的数据 request.setattribute(cryptoconstant.input_decrypt_data, decrypttext); // 获取结果 object result = objectmapper.readvalue(decrypttext, body.getclass()); // 强制所有实体类必须继承requestbase类,设置时间戳 if (result instanceof requestbase) { // 获取时间戳 long currenttimemillis = ((requestbase) result).getcurrenttimemillis(); // 有效期 60秒 long effective = 60*1000; // 时间差 long expire = system.currenttimemillis() - currenttimemillis; // 是否在有效期内 if (math.abs(expire) > effective) { throw new paramexception(时间戳不合法); } // 返回解密之后的数据 return result; } else { throw new paramexception(string.format(请求参数类型:%s 未继承:%s, result.getclass().getname(), requestbase.class.getname())); } } /** * 如果body为空,转为空对象 * @param body spring解析完的参数 * @param inputmessage 输入参数 * @param parameter 参数对象 * @param targettype 参数类型 * @param convertertype 消息转换类型 * @return 真实的参数 */ @sneakythrows @override public object handleemptybody(object body, httpinputmessage inputmessage, methodparameter parameter, type targettype, class convertertype) { string typename = targettype.gettypename(); class bodyclass = class.forname(typename); return bodyclass.newinstance(); }} encryptresponsebodyadvice 相应自动加密
package xyz.hlh.crypto.advice;import cn.hutool.json.jsonutil;import com.fasterxml.jackson.databind.objectmapper;import lombok.sneakythrows;import org.apache.commons.lang3.stringutils;import org.springframework.beans.factory.annotation.autowired;import org.springframework.core.methodparameter;import org.springframework.http.mediatype;import org.springframework.http.responseentity;import org.springframework.http.converter.httpmessageconverter;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;import sun.reflect.generics.reflectiveobjects.parameterizedtypeimpl;import xyz.hlh.crypto.annotation.encryptionannotation;import xyz.hlh.crypto.common.entity.result;import xyz.hlh.crypto.common.exception.cryptoexception;import xyz.hlh.crypto.entity.requestbase;import xyz.hlh.crypto.util.aesutil;import java.lang.reflect.type;/** * @author hlh * @description: * @email 17703595860@163.com * @date : created in 2022/2/4 15:12 */@controlleradvicepublic class encryptresponsebodyadvice implements responsebodyadvice { @autowired private objectmapper objectmapper; @override public boolean supports(methodparameter returntype, class convertertype) { parameterizedtypeimpl genericparametertype = (parameterizedtypeimpl)returntype.getgenericparametertype(); // 如果直接是result,则返回 if (genericparametertype.getrawtype() == result.class && returntype.hasmethodannotation(encryptionannotation.class)) { return true; } if (genericparametertype.getrawtype() != responseentity.class) { return false; } // 如果是responseentity for (type type : genericparametertype.getactualtypearguments()) { if (((parameterizedtypeimpl) type).getrawtype() == result.class && returntype.hasmethodannotation(encryptionannotation.class)) { return true; } } return false; } @sneakythrows @override public result beforebodywrite(result body, methodparameter returntype, mediatype selectedcontenttype, class selectedconvertertype, serverhttprequest request, serverhttpresponse response) { // 加密 object data = body.getdata(); // 如果data为空,直接返回 if (data == null) { return body; } // 如果是实体,并且继承了request,则放入时间戳 if (data instanceof requestbase) { ((requestbase)data).setcurrenttimemillis(system.currenttimemillis()); } string datatext = jsonutil.tojsonstr(data); // 如果data为空,直接返回 if (stringutils.isblank(datatext)) { return body; } // 如果位数小于16,报错 if (datatext.length() < 16) { throw new cryptoexception(加密失败,数据小于16位); } string encrypttext = aesutil.encrypthex(datatext); return result.builder() .status(body.getstatus()) .data(encrypttext) .message(body.getmessage()) .build(); }} 5.4 crypto-test 5.4.1 结构 5.4.2 重要代码
application.yml 配置文件
spring: mvc: format: date-time: yyyy-mm-dd hhss date: yyyy-mm-dd # 日期格式化 jackson: date-format: yyyy-mm-dd hhss teacher 实体类
package xyz.hlh.crypto.entity;import lombok.allargsconstructor;import lombok.data;import lombok.equalsandhashcode;import lombok.noargsconstructor;import org.hibernate.validator.constraints.range;import javax.validation.constraints.notblank;import javax.validation.constraints.notnull;import java.io.serializable;import java.util.date;/** * @author hlh * @description: teacher实体类,使用springboot的validation校验 * @email 17703595860@163.com * @date : created in 2022/2/4 10:21 */@data@noargsconstructor@allargsconstructor@equalsandhashcode(callsuper = true)public class teacher extends requestbase implements serializable { @notblank(message = 姓名不能为空) private string name; @notnull(message = 年龄不能为空) @range(min = 0, max = 150, message = 年龄不合法) private integer age; @notnull(message = 生日不能为空) private date birthday;} testcontroller 测试controller
Anritsu联手dSPACE 开发支持5G的虚拟测试驱动器应用程序
德风科技宣布获得近2亿元A+轮融资,用于工业互联网的研发升级
英特尔正式放弃迷你计算机市场 但仍保留可穿戴设备搭载的Curie模块
流媒体电视功能解析
新品6月上市 汇威手机AICALL V9扬帆发布
SpringBoot接口加密解密,新姿势!
在低成本功率转换器上成功实现质量的应用示例
光纤光电复合滑环在雷达系统的应用
印第安发布新车Scout-Bobber,如此的炫酷拉风!售价7.75万左右
GaAs半导体元件市场规模2015年可达3.2亿美元
路由权
戴尔服务关注小企业数字化转型,联合中小商协成立创业服务平台
PM Factory Netherlands部署Mavenir的云原生分组网关,为其移动虚拟网络运营商解决方案组合提供支持
AMD 3D缓存首次杀入笔记本!海量145MB 性能飙升64%
泰克科技2021年新年展望:数智泰克,创赢未来
一种适用于宽范围输入的Boost Buck-Boost Fl
视频、音频和定位信息的数据采集传输器的设计
低压差稳压器(LDO)电路设计
中科院力学所搭建基于三维Helmholtz线圈磁控系统
晶体管图示仪使用方法及使用注意事项