User-Profile-Image
hankin
  • 5
  • Java
  • Kotlin
  • Spring
  • Web
  • SQL
  • MegaData
  • More
  • Experience
  • Enamiĝu al vi
  • 分类
    • Zuul
    • Zookeeper
    • XML
    • WebSocket
    • Web Notes
    • Web
    • Vue
    • Thymeleaf
    • SQL Server
    • SQL Notes
    • SQL
    • SpringSecurity
    • SpringMVC
    • SpringJPA
    • SpringCloud
    • SpringBoot
    • Spring Notes
    • Spring
    • Servlet
    • Ribbon
    • Redis
    • RabbitMQ
    • Python
    • PostgreSQL
    • OAuth2
    • NOSQL
    • Netty
    • MySQL
    • MyBatis
    • More
    • MinIO
    • MegaData
    • Maven
    • LoadBalancer
    • Kotlin Notes
    • Kotlin
    • Kafka
    • jQuery
    • JavaScript
    • Java Notes
    • Java
    • Hystrix
    • Git
    • Gateway
    • Freemarker
    • Feign
    • Eureka
    • ElasticSearch
    • Docker
    • Consul
    • Ajax
    • ActiveMQ
  • 页面
    • 归档
    • 摘要
    • 杂图
    • 问题随笔
  • 友链
    • Spring Cloud Alibaba
    • Spring Cloud Alibaba - 指南
    • Spring Cloud
    • Nacos
    • Docker
    • ElasticSearch
    • Kotlin中文版
    • Kotlin易百
    • KotlinWeb3
    • KotlinNhooo
    • 前端开源搜索
    • Ktorm ORM
    • Ktorm-KSP
    • Ebean ORM
    • Maven
    • 江南一点雨
    • 江南国际站
    • 设计模式
    • 熊猫大佬
    • java学习
    • kotlin函数查询
    • Istio 服务网格
    • istio
    • Ktor 异步 Web 框架
    • PostGis
    • kuangstudy
    • 源码地图
    • it教程吧
    • Arthas-JVM调优
    • Electron
    • bugstack虫洞栈
    • github大佬宝典
    • Sa-Token
    • 前端技术胖
    • bennyhuo-Kt大佬
    • Rickiyang博客
    • 李大辉大佬博客
    • KOIN
    • SQLDelight
    • Exposed-Kt-ORM
    • Javalin—Web 框架
    • http4k—HTTP包
    • 爱威尔大佬
    • 小土豆
    • 小胖哥安全框架
    • 负雪明烛刷题
    • Kotlin-FP-Arrow
    • Lua参考手册
    • 美团文章
    • Java 全栈知识体系
    • 尼恩架构师学习
    • 现代 JavaScript 教程
    • GO相关文档
    • Go学习导航
    • GoCN社区
    • GO极客兔兔-案例
    • 讯飞星火GPT
    • Hollis博客
    • PostgreSQL德哥
    • 优质博客推荐
    • 半兽人大佬
    • 系列教程
    • PostgreSQL文章
    • 云原生资料库
    • 并发博客大佬
Help?

Please contact us on our email for need any support

Support
    首页   ›   Spring   ›   正文
Spring

Spring—@ControllerAdvice等异常处理方式或统一处理数据

2020-03-15 01:52:39
1302  0 2
参考目录 隐藏
1) 全局异常处理
2) 全局数据绑定
3) 全局数据预处理
4) ResponseBodyAdvice
5) RequestBodyAdvice
6) BasicErrorController
7) 接口参数加密处理

阅读完需:约 22 分钟

@ControllerAdvice ,很多初学者可能都没有听说过这个注解,实际上,这是一个非常有用的注解,顾名思义,这是一个增强的 Controller。使用这个 Controller ,可以实现三个方面的功能:

  1. 全局异常处理
  2. 全局数据绑定
  3. 全局数据预处理

灵活使用这三个功能,可以帮助我们简化很多工作,需要注意的是,这是 SpringMVC 提供的功能,在 Spring Boot 中可以直接使用,下面分别来看。

全局异常处理

使用 @ControllerAdvice 实现全局异常处理,只需要定义类,添加该注解即可定义方式如下:

@ControllerAdvice
public class MyGlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    public ModelAndView customException(Exception e) {
        ModelAndView mv = new ModelAndView();
        mv.addObject("message", e.getMessage());
        mv.setViewName("myerror");
        return mv;
    }
}

在该类中,可以定义多个方法,不同的方法处理不同的异常,例如专门处理空指针的方法、专门处理数组越界的方法…,也可以直接向上面代码一样,在一个方法中处理所有的异常信息。

@ExceptionHandler 注解用来指明异常的处理类型,即如果这里指定为 NullpointerException,则数组越界异常就不会进到这个方法中来。

例子:

编写FileUploadController.java文件主要内容是文件的上传

SimpleDateFormat s=new SimpleDateFormat("/yyyy/MM/dd/");//创建日期来分类 
@RequestMapping(value="/uploads")
    public String uploads(MultipartFile[] files , HttpServletRequest request )  {
        String format=s.format(new Date());
        String r=request.getServletContext().getRealPath("/img")+format;//获取根目录
        File folder=new File(r);//创建文件
        if(!folder.exists()){
            folder.mkdirs();
        }
        for (MultipartFile file:files){
            String oldname=file.getOriginalFilename();//获取旧的名字
            String newname= UUID.randomUUID().toString()+oldname.substring(oldname.lastIndexOf("."));
            //替换新的名字

            try {
                file.transferTo(new File(folder,newname));
                String url=request.getScheme()+"://"+ request.getServerName()+":"+request.getServerPort()+"/img"+format+newname;
                System.out.println(url); //返回上传图片的路径
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return "YES";
    }

这是一个多文件的上传代码

接下来编写自己要定义的异常处理类MyCustomException.java

@ControllerAdvice //这是一个增强的 Controller
public class MyCustomException {

//    @ExceptionHandler(MaxUploadSizeExceededException.class)
//    public  void myex(MaxUploadSizeExceededException e, HttpServletResponse response) throws IOException {
//        response.setContentType("text/html;charset=utf-8");
//        PrintWriter out=response.getWriter();
//        out.write("文件太大了");
//        out.flush();
//        out.close();
//    }

    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public ModelAndView myexs(MaxUploadSizeExceededException e) throws IOException {
        ModelAndView modelAndView=new ModelAndView("xianshi");
        modelAndView.addObject("error","文件太大了超出范围");
        return modelAndView;

    }
}

注释的是针对于普通的html来显示信息的,下面的是针对与Thymeleaf模板来显示的

目录中的ajax.html,index.html,index2.html都是一样的不过是提交的方式不同,这里我们用多文件的提交

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/uploads" method="post" enctype="multipart/form-data">
    <input type="file" name="files" multiple>
    <input type="submit" value="提交">
</form>
</body>
</html>

下面是Thymeleaf模板,用来显示错误的提示

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org/">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div th:text="error"></div>
</body>
</html>

运行结果:

设置上传文件的大小为1MB

因为这样我们的异常是上传的文件大小不能超过1MB,所以报错了,提示的信息是我们自定义的

全局数据绑定

全局数据绑定功能可以用来做一些初始化的数据操作,我们可以将一些公共的数据定义在添加了 @ControllerAdvice 注解的类中,这样,在每一个 Controller 的接口中,就都能够访问导致这些数据。

使用步骤,首先定义全局数据,如下:

@ControllerAdvice
public class MyGlobalExceptionHandler {
    @ModelAttribute(name = "md")
    public Map<String,Object> mydata() {
        HashMap<String, Object> map = new HashMap<>();
        map.put("age", 99);
        map.put("gender", "男");
        return map;
    }
}

使用 @ModelAttribute 注解标记该方法的返回数据是一个全局数据,默认情况下,这个全局数据的 key 就是返回的变量名,value 就是方法返回值,当然开发者可以通过 @ModelAttribute 注解的 name 属性去重新指定 key。

定义完成后,在任何一个Controller 的接口中,都可以获取到这里定义的数据:

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(Model model) {
        Map<String, Object> map = model.asMap();
        System.out.println(map);
        int i = 1 / 0;
        return "hello controller advice";
    }
}

例子:

先编写GloabalData

@ControllerAdvice
public class GloabalData {

    @ModelAttribute(value = "info")
    public Map<String,Object> mapdata(){
        Map<String,Object> map=new HashMap<>();
        map.put("name","123");
        map.put("oop","456");
        return map;
    }
}

定义一个全局数据

再编写一个HelloController进行访问

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(Model model){
        Map<String,Object> map=model.asMap();
        Set<String> keySet=map.keySet();
        for (String key:keySet){
            System.out.println(key+":"+map.get(key));
        }
        return "hello";
    }
}

控制器是通过Model把数据传到view层

结果:

全局数据预处理

考虑我有两个实体类,Book 和 Author,分别定义如下:

public class Book {
    private String name;
    private Long price;
    //getter/setter
}
public class Author {
    private String name;
    private Integer age;
    //getter/setter
}

此时,如果我定义一个数据添加接口,如下:

@PostMapping("/book")
public void addBook(Book book, Author author) {
    System.out.println(book);
    System.out.println(author);
}

这个时候,添加操作就会有问题,因为两个实体类都有一个 name 属性,从前端传递时 ,无法区分。此时,通过 @ControllerAdvice 的全局数据预处理可以解决这个问题

解决步骤如下:

1.给接口中的变量取别名

@PostMapping("/book")
public void addBook(@ModelAttribute("b") Book book, @ModelAttribute("a") Author author) {
    System.out.println(book);
    System.out.println(author);
}

2.进行请求数据预处理
在 @ControllerAdvice 标记的类中添加如下代码:

@InitBinder("b")
public void b(WebDataBinder binder) {
    binder.setFieldDefaultPrefix("b.");
}
@InitBinder("a")
public void a(WebDataBinder binder) {
    binder.setFieldDefaultPrefix("a.");
}

@InitBinder(“b”) 注解表示该方法用来处理和Book和相关的参数,在方法中,给参数添加一个 b 前缀,即请求参数要有b前缀.

3.发送请求

请求发送时,通过给不同对象的参数添加不同的前缀,可以实现参数的区分.

@ControllerAdvice 的几个简单用法,这些点既可以在传统的 SSM 项目中使用,也可以在 Spring Boot + Spring Cloud 微服务中使用


ResponseBodyAdvice

ResponseBodyAdvice接口是在Controller执行return之后,在response返回给浏览器或者APP客户端之前,执行的对response的一些处理。可以实现对response数据的一些统一封装或者加密等操作。

该接口一共有两个方法:

@Override
    //判断是否要执行beforeBodyWrite方法,true为执行,false不执行
    public boolean supports(MethodParameter returnType, Class converterType) {
        return false;
    }

    @Override
    //对response处理的执行方法
    public Object beforeBodyWrite(Object body, MethodParameter returnType,
            MediaType selectedContentType, Class selectedConverterType,
            ServerHttpRequest request, ServerHttpResponse response) {
        return null;
    }

通过supports方法,我们可以选择哪些类,或者哪些方法要对response进行处理,其余的则不处理。

beforeBdoyWrite方法中,为对response处理的具体代码。

这个工程中的一个Controller类,返回参数为OutputObject(一个自定义的javaBean),我们要通过ResponseBodyAdvice,对该类的所有方法返回的OutputObject中的部分数据进行统一加密处理。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import com.alibaba.fastjson.JSON;
import com.cmos.edcreg.beans.common.OutputObject;
import com.cmos.edcreg.utils.StringUtil;
import com.cmos.edcreg.utils.des.DesSpecial;
import com.cmos.edcreg.utils.enums.ReturnInfoEnums;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * 对响应报文统一处理,对bean内容进行加密
 * @author qiaozhong
 */
@Component
//声明该类要处理的包路径
@ControllerAdvice("com.cmos.edcreg.web.controller")
public class ResponseAdvice implements ResponseBodyAdvice {
    
    private final Logger logger = LoggerFactory.getLogger(ResponseAdvice.class);
    
    /* 
     * 对response处理的具体方法
     * arg0为返回的报文体,arg0为org.json.jsonObject,使用alibaba.json方法转换时候报错了
     */
    @Override
    public Object beforeBodyWrite(Object arg0, MethodParameter arg1,
            MediaType arg2, Class arg3, ServerHttpRequest arg4,
            ServerHttpResponse arg5) {
        OutputObject out = new OutputObject();
        try {
            //arg0转换为OutputObject类型
            ObjectMapper objectMapper=new ObjectMapper();
            out = objectMapper.readValue(org.json.JSONObject.valueToString(arg0), OutputObject.class);
            //获取加密密钥
            String oldEncryptKey = out.getBean().get("oldEncryptKey");
            //获取加密字符串
            DesSpecial des = new DesSpecial();
            String encryptData = des.strEnc(JSON.toJSONString(out.getBean()), oldEncryptKey, null, null);
            //封装数据(清除原来数据,放入加密数据)
            out.getBean().clear();
            out.getBean().put("data", encryptData);
            return out;
        } catch (Exception e) {
            logger.error("返回报文处理出错", e);
            out.setReturnCode(ReturnInfoEnums.PROCESS_ERROR.getCode());
            out.setReturnMessage(ReturnInfoEnums.PROCESS_ERROR.getMessage());
            return out;
        }
    }

    /* 
     * 选择哪些类,或哪些方法需要走beforeBodyWrite
     * 从arg0中可以获取方法名和类名
     * arg0.getMethod().getDeclaringClass().getName()为获取方法名
     */
    @Override
    public boolean supports(MethodParameter arg0, Class arg1) {
        if("com.cmos.edcreg.web.controller.GdH5AppointmentActiveVideoNewController".equals(arg0.getMethod().getDeclaringClass().getName())){
            return true;
        }else{
            return false;
        }
    }
}

RequestBodyAdvice

在实际项目中 , 往往需要对请求参数做一些统一的操作 , 例如参数的过滤 , 字符的编码 , 第三方的解密等等 , Spring提供了RequestBodyAdvice一个全局的解决方案 , 免去了我们在Controller处理的繁琐

RequestBodyAdvice是对@RequestBody进行增强处理,也是对所有请求的拦截

提供的方法

该方法返回true时,才会进去下面的系列方法

boolean supports(MethodParameter methodParameter, Type targetType,
      Class<? extends HttpMessageConverter<?>> converterType);

body数据读取之前调用,一般在此方法中对body数据进行修改

HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
      Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException;

body读取之后操作,一般直接返回原实例

Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
      Type targetType, Class<? extends HttpMessageConverter<?>> converterType);

当body问empty时操作

Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter,
      Type targetType, Class<? extends HttpMessageConverter<?>> converterType);

案例

@Component
@ControllerAdvice({"com.xxxx.controller"})
@Slf4j
public class MyRequestBodyHandler implements RequestBodyAdvice {

    @Autowired
    private LoginServiceClient loginServiceClient;

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        return inputMessage;
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        String token = inputMessage.getHeaders().getFirst("token");
        //获取登录用户信息
        UserDTO loginUser = loginServiceClient.getLoginUserByToken();
        if (loginUser == null) {
            log.error("获取登录用户信息为空,toekn:{}", token);
        } else {
            SessionCache.put(loginUser);
        }
        return body;
    }

    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }
}

BasicErrorController 

在springboot项目中当我们访问一个不存在的url时经常会出现以下页面

在postman访问时则是以下情况

{
    "timestamp": "2020-05-25T12:29:35.418+0000",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/"
}

看起来, 如果程序抛出了未能被 @ControllerAdvice 处理的异常, SpringBoot 会把请求转向 /error 这个端点, 自然的, 我们会期望有这么一个 Controller 用户处理这个端点的请求, SpringBoot 有一个默认的 Controller 用于处理这类请求, 它就是 BasicErrorController

先写一个简单 Filter, 里面当请求携带指定参数时, 抛出自定义异常:

@Component
public class SimplePerRequestFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.info("SimplePerRequestFilter#doFilterInternal");

        if (Boolean.parseBoolean(request.getParameter("shouldThrowAnException"))) {
            throw new FilterException("FilterException@SimplePerRequestFilter");
        }

        filterChain.doFilter(request, response);
    }
}

继承 BasicErrorController, 将 error 方法体用我们自己的逻辑覆盖:

@RestController
public class CustomErrorController extends BasicErrorController {

    private static final TypeReference<Map<String, Object>> mapTypeReference = new TypeReference<Map<String, Object>>() {
    };

    public CustomErrorController(ErrorAttributes errorAttributes) {
        super(errorAttributes, new ErrorProperties());
    }

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        final Map<String, Object> body = getErrorAttributes(request, false);

        final HttpStatus status = getStatus(request);
        return
                new ResponseEntity<>(
                        JSON.parseObject(JSON.toJSONString(
                                ApiResponse.builder().httpStatus(status.value()).timestamp(LocalDateTime.now()).message((String) body.get("message")).build()),
                                mapTypeReference
                        ),
                        status
                );
    }

}

原本的返回信息就会发送变化:

{
    "data": "{}",
    "httpStatus": 500,
    "message": "FilterException@SimplePerRequestFilter",
    "timestamp": "2020-05-25 21:17:27"
}

BasicErrorController 提供两种返回错误:

  1. 页面返回(重写 errorHtml  方法)
  2. json 返回(重写 error 方法)

BasicErrorController 中有两个标注了 @RequestMapping 的方法, 当请求头的 Accept 中包含 text/html 时, 会调用 public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response), 其他情况则会调用 public ResponseEntity> error(HttpServletRequest request), 通过断点调试可以看到, 这个 request 对象的 arrtibute 中封装了异常对象本身, HTTP 状态码, requestURI 这类非常有用的信息. 我们完全可以利用这些信息来定义自己的信息体.


这样就可以实现在不改动之前工程任何代码的情况下只处理额外 Filter 中抛出的异常了。需要注意的是,上述是通过 BasicErrorController 来接受了 Filter 抛出的异常信息,然后再通过 BasicErrorController 将异常信息进行包装并且返回。为什么要提一下这个呢?主要是为了和 SpringBoot 中基于 REST 请求层所提供的两个用于处理全局异常的注解区分,这两个注解分别是 @ControllerAdvice 和 @RestControllerAdvice,通过注解的名字其实就能看出,SpringBoot 中,可以通过这两个注解来实现对 @Controller 和 @RestController 标注的类进行全局拦截,因为是 Controller 层面的 AOP 拦截,所以对于 Filter 中抛出的异常,通过 @ControllerAdvice 和 @RestControllerAdvice 两个注解定义的全局异常处理器是没法处理的。

小结:

在 SpringBoot 中如何保证 Filter 中抛出的异常能和业务一样以指定类型的对象返回,并对 SpringBoot 中提供的基于 Controller 层异常捕获处理进行简单介绍。两者处理异常的思路是不同的:

  • BasicErrorController:接受来自 /error 的异常请求处理,Filter 中抛出的异常先 forward 到 /error,然后处理。
  • @RestControllerAdvice:通过对于所有 @Controller 注解所标注的类进行 AOP 拦截,能够根据异常类型匹配具体的 ExceptionHandler 进行处理。

接口参数加密处理

加密解密本身并不是难事,问题是在何时去处理?定义一个过滤器,将请求和响应分别拦截下来进行处理也是一个办法,这种方式虽然粗暴,但是灵活,因为可以拿到一手的请求参数和响应数据。不过 SpringMVC 中给我们提供了 ResponseBodyAdvice 和 RequestBodyAdvice,利用这两个工具可以对请求和响应进行预处理,非常方便。

开发加解密

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

依赖添加完成后,我们先来定义一个加密工具类备用,加密这块有多种方案可以选择,对称加密、非对称加密,其中对称加密又可以使用 AES、DES、3DES 等不同算法,这里我们使用 Java 自带的 Cipher 来实现对称加密,使用 AES 算法:

Java—加密扩展(JCE)框架 之 Cipher-加密与解密

AESUtils

public class AESUtils {

    private static final String AES_ALGORITHM = "AES/ECB/PKCS5Padding";

    // 获取 cipher
    private static Cipher getCipher(byte[] key, int model) throws Exception {
        SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
        cipher.init(model, secretKeySpec);
        return cipher;
    }

    // AES加密
    public static String encrypt(byte[] data, byte[] key) throws Exception {
        Cipher cipher = getCipher(key, Cipher.ENCRYPT_MODE);
        return Base64.getEncoder().encodeToString(cipher.doFinal(data));
    }

    // AES解密
    public static byte[] decrypt(byte[] data, byte[] key) throws Exception {
        Cipher cipher = getCipher(key, Cipher.DECRYPT_MODE);
        return cipher.doFinal(Base64.getDecoder().decode(data));
    }
}

这个工具类比较简单,不需要多解释。需要说明的是,加密后的数据可能不具备可读性,因此我们一般需要对加密后的数据再使用 Base64 算法进行编码,获取可读字符串。换言之,上面的 AES 加密方法的返回值是一个 Base64 编码之后的字符串,AES 解密方法的参数也是一个 Base64 编码之后的字符串,先对该字符串进行解码,然后再解密。

接下来我们定义两个注解 @Decrypt 和 @Encrypt:

/**
 * 解密
 */
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER,AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class Decrypt


/**
 * 加密
 */
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
annotation class Encrypt

这两个注解就是两个标记,在以后使用的过程中,哪个接口方法添加了 @Encrypt 注解就对哪个接口的数据加密返回,哪个接口/参数添加了 @Decrypt 注解就对哪个接口/参数进行解密。这个定义也比较简单,没啥好说的,需要注意的是 @Decrypt 比 @Encrypt 多了一个使用场景就是 @Decrypt 可以用在参数上。

考虑到用户可能会自己配置加密的 key,因此我们再来定义一个 EncryptProperties 类来读取用户配置的 key:

@ConfigurationProperties(prefix = "spring.encrypt")
@Component
class EncryptProperties {
    var key = DEFAULT_KEY
    companion object {
        private const val DEFAULT_KEY = "passwordPassword"
    }
}

这里我设置了默认的 key 是 passwordPassword,key 是 16 位字符串,如果用户想自己配置 key,只需要在 application.properties 中配置 spring.encrypt.key=xxx 即可。

所有准备工作做完了,接下来就该正式加解密了。

这里一个很重要的目的是想试试 ResponseBodyAdvice 和 RequestBodyAdvice 的用法,RequestBodyAdvice 在做解密的时候倒是没啥问题,而 ResponseBodyAdvice 在做加密的时候则会有一些局限,不过影响不大,还是我前面说的,如果想非常灵活的掌控一切,那还是自定义过滤器吧。

还有一点需要注意,ResponseBodyAdvice 在你使用了 @ResponseBody 注解的时候才会生效,RequestBodyAdvice 在你使用了 @RequestBody 注解的时候才会生效,换言之,前后端都是 JSON 交互的时候,这两个才有用。不过一般来说接口加解密的场景也都是前后端分离的时候才可能有的事。

先来看接口加密:

EncryptResponse

@EnableConfigurationProperties(EncryptProperties.class)
@ControllerAdvice
public class EncryptResponse implements ResponseBodyAdvice<Data> {
    private ObjectMapper om = new ObjectMapper();

    @Autowired
    EncryptProperties encryptProperties;
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return returnType.hasMethodAnnotation(Encrypt.class);
    }


    @Override
    public Data beforeBodyWrite(Data body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        byte[] keyBytes = encryptProperties.getKey().getBytes();
        try {
            if (body.get("data")!=null) {
                body.include("data",AESUtils.encrypt(om.writeValueAsString(body.get("data")).getBytes(),keyBytes));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return body;
    }
}

我们自定义 EncryptResponse 类实现 ResponseBodyAdvice接口,泛型表示接口的返回类型,这里一共要实现两个方法:

  1. supports:这个方法用来判断什么样的接口需要加密,参数 returnType 表示返回类型,我们这里的判断逻辑就是方法是否含有 @Encrypt 注解,如果有,表示该接口需要加密处理,如果没有,表示该接口不需要加密处理。
  2. beforeBodyWrite:这个方法会在数据响应之前执行,也就是我们先对响应数据进行二次处理,处理完成后,才会转成 json 返回。我们这里的处理方式很简单,RespBean 中的 status 是状态码就不用加密了,另外两个字段重新加密后重新设置值即可。
  3. 另外需要注意,自定义的 ResponseBodyAdvice 需要用 @ControllerAdvice 注解来标记。

再来看接口解密:

DecryptRequest

@EnableConfigurationProperties(EncryptProperties.class)
@ControllerAdvice
public class DecryptRequest extends RequestBodyAdviceAdapter {
    @Autowired
    EncryptProperties encryptProperties;

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return methodParameter.hasMethodAnnotation(Decrypt.class) || methodParameter.hasParameterAnnotation(Decrypt.class);
    }

    @Override
    public HttpInputMessage beforeBodyRead(final HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        byte[] body = new byte[inputMessage.getBody().available()];
        inputMessage.getBody().read(body);
        try {
            byte[] decrypt = AESUtils.decrypt(body, encryptProperties.getKey().getBytes());
            final ByteArrayInputStream bais = new ByteArrayInputStream(decrypt);
            return new HttpInputMessage() {
                @Override
                public InputStream getBody() throws IOException {
                    return bais;
                }

                @Override
                public HttpHeaders getHeaders() {
                    return inputMessage.getHeaders();
                }
            };
        } catch (Exception e) {
            e.printStackTrace();
        }
        return super.beforeBodyRead(inputMessage, parameter, targetType, converterType);
    }
}
  1. 首先大家注意,DecryptRequest 类我们没有直接实现 RequestBodyAdvice 接口,而是继承自 RequestBodyAdviceAdapter 类,该类是 RequestBodyAdvice 接口的子类,并且实现了接口中的一些方法,这样当我们继承自 RequestBodyAdviceAdapter 时,就只需要根据自己实际需求实现某几个方法即可。
  2. supports:该方法用来判断哪些接口需要处理接口解密,我们这里的判断逻辑是方法上或者参数上含有 @Decrypt 注解的接口,处理解密问题。
  3. beforeBodyRead:这个方法会在参数转换成具体的对象之前执行,我们先从流中加载到数据,然后对数据进行解密,解密完成后再重新构造 HttpInputMessage 对象返回。

如果是打成jar包,变成一个依赖包,需要增加自动配置的类

EncryptAutoConfiguration

@Configuration
@ComponentScan("com.enmalvi.casestudyweb.CipherAES")
public class EncryptAutoConfiguration {

}

最后,resources 目录下定义 META-INF,然后再定义 spring.factories 文件,内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.enmalvi.casestudyweb.CipherAES.autoconfig.EncryptAutoConfiguration

这样当项目启动时,就会自动加载该配置类。

测试加密解密

首先定义一个实例类

User

public class User {
    private Long id;
    private String name;
    private Integer age;
    private Date createTime;


    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }
}

创建两个测试接口:

@RestController
public class ControllerDemo {

    @GetMapping("/user")
    @Encrypt
    public Data getUser() {
        User user = new User();
        user.setId((long) 99);
        return new MultipartData().plugin().code(10000).message("操作成功").data(user).attach();
    }

    @PostMapping("/user")
    public Data addUser(@RequestBody @Decrypt User user) {
        System.out.println("user = " + user);
        return new MultipartData().plugin().code(10000).message("操作成功").data(user).attach();
    }

}

第一个接口使用了 @Encrypt 注解,所以会对该接口的数据进行加密(如果不使用该注解就不加密),第二个接口使用了 @Decrypt 所以会对上传的参数进行解密,注意 @Decrypt 注解既可以放在方法上也可以放在参数上。

接下来启动项目进行测试

调用接口:http://127.0.0.1:8080/user

返回结果

{
	"code": 10000,
	"message": "操作成功",
	"data": "/gdtqgrOYMwUoQVj0D2B2szjsb6SxRmlJYJ0Lvb4jE4XTU/YAiXxMX99XIhRLM87"
}

再次调用解密接口:http://127.0.0.1:8080/user

返回结果

{
	"code": 10000,
	"message": "操作成功",
	"data": {
		"id": 99,
		"name": null,
		"createTime": null
	}
}

加密数据到了前端,前端也有一些 js 工具来处理加密数据。

如本文“对您有用”,欢迎随意打赏作者,让我们坚持创作!

2 打赏
Enamiĝu al vi
不要为明天忧虑.因为明天自有明天的忧虑.一天的难处一天当就够了。
543文章 68评论 294点赞 594006浏览

随机文章
Java—绑定线程到指定CPU上(线程问题思考)
12个月前
Spring Data Redis 使用(SSM版)
5年前
Kotlin-协程(专)—Flow 篇(四十一)
4年前
SpringBoot—默认的json解析方案
5年前
LangChain+RAG—构建知识库(一)
1年前
博客统计
  • 日志总数:543 篇
  • 评论数目:68 条
  • 建站日期:2020-03-06
  • 运行天数:1927 天
  • 标签总数:23 个
  • 最后更新:2024-12-20
Copyright © 2025 网站备案号: 浙ICP备20017730号 身体没有灵魂是死的,信心没有行为也是死的。
主页
页面
  • 归档
  • 摘要
  • 杂图
  • 问题随笔
博主
Enamiĝu al vi
Enamiĝu al vi 管理员
To be, or not to be
543 文章 68 评论 594006 浏览
测试
测试
看板娘
赞赏作者

请通过微信、支付宝 APP 扫一扫

感谢您对作者的支持!

 支付宝 微信支付