因为刚刚有朋友问了这个问题,宋哥就抽空写了一篇文章跟大家聊聊这个话题。加密和解密本身并不难。问题是什么时候处理?定义一个过滤器来拦截请求和响应分别进行处理也是一种方式。这种方式虽然比较粗糙,但是比较灵活,因为可以得到第一手的请求参数。和响应数据。但是SpringMVC为我们提供了ResponseBodyAdvice和RequestBodyAdvice。使用这两个工具可以对请求和响应进行预处理,非常方便。所以今天的文章有两个目的:分享参数/响应加解密的思路。分享一下ResponseBodyAdvice和RequestBodyAdvice的用法。好了,废话不多说了,一起来看看吧。1.开发加解密starter为了让我们开发的工具更加通用,也为了回顾一下自定义的SpringBootStarter,这里我们将这个工具做成stater,以后在SpringBoot项目中可以直接引用.首先我们创建一个SpringBoot工程,引入spring-boot-starter-web依赖:org.springframework.bootspring-boot-starter-webprovided2.4.3因为我们的工具是针对Web项目开发的,以后肯定要在Web环境下使用,所以scope设置为provided的时候在此处添加依赖项。添加依赖后,我们先定义一个加密工具类,用于备份。加密有多种选择,对称加密和非对称加密。其中对称加密可以使用AES、DES、3DES等不同的算法,这里我们使用Java自带的Cipher实现对称加密,使用AES算法:publicclassAESUtils{privatestaticfinalStringAES_ALGORITHM="AES/ECB/PKCS5Padding";//获取cipherprivatestaticCiphergetCipher(byte[]key,intmodel)throwsException{SecretKeySpecsecretKeySpec=newSecretKeySpec(key,"AES");Ciphercipher=Cipher.getInstance(AES_ALGORITHM);cipher.init(model,secretKeySpec);returncipher;}//AES加密publicstaticStringencrypt(byte)[]data,byte[]key)throwsException{Ciphercipher=getCipher(key,Cipher.ENCRYPT_MODE);returnBase64.getEncoder().encodeToString(cipher.doFinal(data));}//AES解密publicstaticbyte[]decrypt(byte[]data,byte[]key)throwsException{Ciphercipher=getCipher(key,Cipher.DECRYPT_MODE);returncipher.doFinal(Base64.getDecoder().decode(data));}}这个工具类比较简单,不需要解释。需要注意的是,加密后的数据可能是不可读的,所以我们一般需要对加密后的数据使用Base64算法进行编码,得到可读的字符串。也就是说,上述AES加密方式的返回值是Base64编码的字符串,AES解密方式的参数也是Base64编码的字符串。该字符串首先被解码,然后被解密。接下来我们封装一个响应工具类,用于备份。如果你经常看松哥的视频,你已经很熟悉了:publicclassRespBean{privateIntegerstatus;privateStringmsg;privateObjectobj;publicstaticRespBeanbuild(){returnnewRespBean();}publicstaticRespBeanok(Stringmsg){returnnewRespBean(200,msg),null);}publicstaticRespBeanok(Stringmsg),Objectobj){returnnewRespBean(200,msg,obj);}publicstaticRespBeanerror(Stringmsg){returnnewRespBean(500,msg,null);}publicstaticRespBeanerror(Stringmsg,Objectobj){returnnewRespBean(500,msg,obj);}privateRespBean(){}privateRespBean(Integerstatus,Stringmsg,Objectobj){this.status=status;this.msg=msg;this.obj=obj;}publicIntegergetStatus(){returnstatus;}publicRespBeansetStatus(Integerstatus){this.status=status;returnthis;}publicStringgetMsg(){returnmsg;}publicRespBeansetMsg(Stringmsg){this.msg=msg;returnthis;}publicObjectgetObj(){returnobj;}publicRespBeansetObj(Objectobj){this.obj=obj;returnthis;}}接下来我们定义两个注解@Decrypt和@Encrypt:@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD,元素Type.PARAMETER})public@interfaceDecrypt{}@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public@interfaceEncrypt{}这两个注解是两个标签。在后面的使用过程中,在哪个接口方法上加上@Encrypt注解,就会加密返回哪个接口的数据,在哪个接口/参数上加上@Decrypt注解,解密哪个接口/参数。这个定义也比较简单。没什么可说的。需要注意的是,@Decrypt比@Encrypt多了一个使用场景,@Decrypt可以用在参数上。考虑到用户可能会自己配置加密密钥,让我们定义一个EncryptProperties类来读取用户配置的密钥:@ConfigurationProperties(prefix="spring.encrypt")){returnkey;}publicvoidsetKey(Stringkey){this.key=key;}}这里我设置的默认key是www.itboyhub.com,key是一个16位的字符串,宋哥的网址正好满足.以后如果用户想自己配置密钥,只需要在application.properties中配置spring.encrypt.key=xxx即可。一切准备工作做好后,就到了正式加解密的时候了。因为松哥这篇文章的一个很重要的目的就是给大家分享一下ResponseBodyAdvice和RequestBodyAdvice的用法。RequestBodyAdvice做解密的时候没有问题,而ResponseBodyAdvice做加密的时候有一些局限性,但是影响不大,前面说了,如果你想非常灵活的控制一切,那么你应该自定义过滤器。这里我先用这两个工具来实现。还有一点需要注意的是ResponseBodyAdvice只有在使用@ResponseBody注解时才会生效,而RequestBodyAdvice只有在使用@RequestBody注解时才会生效。有用。但是一般来说,接口加解密的场景只有在前后端分离的情况下才有可能。先来看接口加密:@EnableConfigurationProperties(EncryptProperties.class)@ControllerAdvicepublicclassEncryptResponseimplementsResponseBodyAdvice{privateObjectMapperom=newObjectMapper();@AutowiredEncryptPropertiesencryptProperties;@Overridepublicbooleansupports(MethodParameterreturnType,Class>converterType){returnreturnType.hasMethodAnnotation(Encrypt.类);}@OverridepublicRespBeanbeforeBodyWrite(RespBeanbody,MethodParameterreturnType,MediaTypeselectedContentType,Class>selectedConverterType,ServerHttpRequestrequest,ServerHttpResponseresponse){byte[]keyBytes=encryptProperties.getKey().getBytes();try{g(body.getMs)()!=null){body.setMsg(AESUtils.encrypt(body.getMsg().getBytes(),keyBytes));}if(body.getObj()!=null){body.setObj(AESUtils.encrypt(om.writeValueAsBytes(body.getObj()),keyBytes));}}catch(Exceptione){e.printStackTrace();}returnbody;}}我们自定义EncryptResponse类实现了ResponseBodyAdvice接口。泛型表示接口的返回类型。这里有两个方法要实现:supports:该方法用于判断什么样的接口需要加密。参数returnType表示返回类型。我们这里的判断逻辑是方法是否包含@Encrypt注解。如果是,说明接口需要加密。如果不是,则说明该接口不需要加密。之后会转成json返回。我们这里的处理方法很简单。RespBean中的status就是状态码,所以不需要加密。其他两个字段可以重新加密并重新设置值。另请注意,自定义ResponseBodyAdvice需要使用@ControllerAdvice注解进行标记。再来看接口解密:@EnableConfigurationProperties(EncryptProperties.class)@ControllerAdvicepublicclassDecryptRequestextendsRequestBodyAdviceAdapter{@AutowiredEncryptPropertiesencryptProperties;@Overridepublicbooleansupports(MethodParametermethodParameter,TypetargetType,Class>converterType){returnmethodParameter.hasMethodAnnotation(Decrypt.class)||methodParameter.hasParameterAnnotation(Decrypt.class);}@OverridepublicHttpInputMessagebeforeBodyRead(finalHttpInputMessageinputMessage,MethodParameterparameter,TypetargetType,Class>converterType)throwsIOException{字节[]body=newbyte[inputMessage.getBody().available()];inputMessage.getBody().read(body);try{byte[]decrypt=AESUtils.decrypt(body,encryptProperties.getKey().getBytes());finalByteArrayInputStreambais=newByteArrayInputStream(decrypt);返回newHttpInputMessage(){@OverridepublicInputStreamgetBody()throwsIOException{returnbais;}@OverridepublicHttpHeadersgetHeaders(){returninputMessage.getHeaders();}};}catch(Exceptione){e.printStackTrace();}returnsuper.beforeBodyRead(inputMessage,parameter,targetType,converterType);}}首先大家注意,DecryptRequest类我们并没有直接实现RequestBodyAdvice接口,而是继承自RequestBodyAdviceAdapter类,该类是RequestBodyAdvice接口的子类,实现了接口中的一些方法,这样我们在继承RequestBodyAdviceAdapter的时候,只需要实现一些根据我们实际需要的东西可以支持几个方法:该方法用于确定哪些接口需要处理接口解密。我们这里的判断逻辑是针对方法或参数上有@Decrypt注解的接口来处理解密问题。beforeBodyRead:该方法将在参数转换为具体对象之前执行。我们先从流中加载数据,然后对数据进行解密,解密完成后,再重构HttpInputMessage对象返回。接下来我们定义一个自动配置类,如下:@Configuration@ComponentScan("org.javaboy.encrypt.starter")publicclassEncryptAutoConfiguration{}这个没什么好说的,比较简单。最后在resources目录下定义META-INF,然后定义spring.factories文件如下:org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.javaboy.encrypt.starter.autoconfig.EncryptAutoConfiguration这样在项目启动的时候,它会自动加载这个配置类。至此,我们的starter已经开发完成。2.打包发布我们可以将项目安装到本地仓库,也可以发布到网上供其他人使用。2.1安装到本地仓库安装到本地仓库比较简单,直接mvninstall,或者在IDEA中,点击右边的Maven,然后双击install,如下:2.2发布到网上但不发布到网上我们可以使用JitPack来做。首先,我们在GitHub上创建一个仓库,并将我们的代码上传到其中。这个过程我就不用多说了。上传成功后,点击右侧的Createanewrelease按钮,发布一个正式版,如下:发布成功后,打开jitpack,输入仓库全路径,点击lookup按钮,找到后it,点击Getit按钮完成构建,如下:构建成功后,JitPack上会给出项目引用方法:注意引用的时候,把tag改成自己的具体版本号。至此,我们的工具已经发布成功!小伙伴们可以参考这个starter如下:>jitpack.iohttps://jitpack.io3.Application我们创建一个普通的SpringBoot项目,引入web依赖,然后引入我们刚才的起步依赖,如下:starter-webcom.github.lenveencrypt-spring-boot-starter<版本>0.0.3org.springframework.bootspring-boot-starter-testtestjitpack.iohttps://jitpack.io/repository>然后创建一个实体类用于备份:publicclassUser{privateLongid;privateStringusername;//省略getter/setter}创建两个测试接口:@RestControllerpublicclassHelloController{@GetMapping("/user")@EncryptpublicRespBeangetUser(){Useruser=newUser();user.setId((long)99);user.setUsername("javaboy");returnRespBean.ok("ok",user);}@PostMapping("/user")publicRespBeanaddUser(@RequestBody@DecryptUseruser){System.out.println("user="+user);returnRespBean.ok("ok",user);}}第一个接口使用了@Encrypt注解,所以接口的数据会被加密(如果你不要使用这个注解,不会被加密),第二个接口使用了@Decrypt,所以上传的参数会被解密。注意@Decrypt注解可以放在方法上,也可以放在参数上,然后启动项目进行测试。先测试get请求接口:可以看到返回的数据已经加密。再测试一下post请求,可以看到参数中的加密数据已经恢复。如果用户想修改加密密钥,可以在application.properties中添加如下配置:spring.encrypt.key=1234567890123456加密后的数据发送到前端,前端有一些js工具处理加密的数据。等松哥有空再说。说说js的加解密。4.总结好了,今天这篇文章主要想和大家聊一聊ResponseBodyAdvice和RequestBodyAdvice的用法,一些加密的思路,当然ResponseBodyAdvice和RequestBodyAdvice还有很多其他的使用场景,小伙伴们可以自己去探索~本文使用symmetryFor加密中的AES算法,也可以尝试改成非对称加密。本文转载自微信公众号“江南的一场小雨”,可通过以下二维码关注。转载本文请联系江南一点鱼公众号。