Java:SpringBoot使用AES对JSON数据加密和解密

目录

    • 1、加密解密原理
    • 2、项目示例
      • 2.1、项目结构
      • 2.2、常规业务代码
      • 2.3、加密的实现
    • 2.4、接口测试
    • 2.5、总结

1、加密解密原理

客户端和服务端都可以加密和解密,使用base64进行网络传输

加密方

字符串 -> AES加密 -> base64

解密方

base64 -> AES解密 -> 字符串

2、项目示例

2.1、项目结构

$ tree -I target -I test
.
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           └── demo
        │               ├── Application.java
        │               ├── annotation
        │               │   └── SecretData.java
        │               ├── config
        │               │   ├── CrossConfig.java
        │               │   ├── DecryptRequestBodyAdvice.java
        │               │   ├── EncryptResponseBodyAdvice.java
        │               │   ├── SecretConfig.java
        │               │   └── WebMvcConfig.java
        │               ├── controller
        │               │   ├── IndexController.java
        │               │   └── UserController.java
        │               ├── request
        │               │   └── JsonRequest.java
        │               ├── response
        │               │   ├── JsonResult.java
        │               │   └── JsonResultVO.java
        │               ├── service
        │               │   ├── SecretDataService.java
        │               │   └── impl
        │               │       └── SecretDataServiceImpl.java
        │               └── utils
        │                   └── CipherUtil.java
        └── resources
            ├── application.yml
            ├── static
            │   ├── axios.min.js
            │   └── crypto-js.min.js
            └── templates
                └── index.html

2.2、常规业务代码

pom.xml



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.7.7
         
    

    com.example
    demo
    0.0.1-SNAPSHOT
    demo
    Demo project for Spring Boot

    
        1.8
        3.5.2
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        

        
            commons-codec
            commons-codec
            1.15
        

        
            org.springframework.boot
            spring-boot-starter-thymeleaf
        

        
            org.projectlombok
            lombok
            true
        

        
            org.springframework.boot
            spring-boot-devtools
            runtime
            true
        

        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        

        
            junit
            junit
            test
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
                
                    
                        
                            org.projectlombok
                            lombok
                        
                    
                
            
        
    



Application.java

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

WebMvcConfig.java

package com.example.demo.config;


import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

    // 设置静态资源映射
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/");
    }
}

CrossConfig.java

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
 * 处理跨域问题
 */
@Configuration
public class CrossConfig {
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOrigin("*");
        config.setAllowCredentials(false);
        config.addAllowedMethod("*");
        config.addAllowedHeader("*");
        config.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
        configSource.registerCorsConfiguration("/**", config);
        return new CorsFilter(configSource);
    }
}

JsonRequest.java

package com.example.demo.request;

import lombok.Data;

/**
 * 统一的请求体数据
 */
@Data
public class JsonRequest {
    /**
     * 未加密数据
     */
    private Object data;
    /**
     * 加密数据
     */
    private String encryptData;
}


JsonResult.java

package com.example.demo.response;

import lombok.Data;

/**
 * 统一的返回体数据 不加密
 */
@Data
public class JsonResult {
    private String message;

    private T data;

    private Integer code;

    public static  JsonResult success(T data){
        JsonResult jsonResult = new JsonResult<>();
        jsonResult.setCode(0);
        jsonResult.setData(data);
        jsonResult.setMessage("success");
        return jsonResult;
    }
}

JsonResultVO.java

package com.example.demo.response;

import lombok.Data;

/**
 * 统一的返回体数据 加密
 */
@Data
public class JsonResultVO {
    private String message;

    private String encryptData;

    private Integer code;
}

IndexController.java

package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController {
    @GetMapping("/")
    public String index(){
        return "index";
    }
}

UserController.java

package com.example.demo.controller;

import com.example.demo.annotation.SecretData;
import com.example.demo.response.JsonResult;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

/**
 * 对该Controller中的所有方法进行加解密处理
 */
@RestController
@SecretData
public class UserController {

    @GetMapping("/user/getUser")
    public JsonResult getUser() {
        Map user = new HashMap<>();
        user.put("name", "Tom");
        user.put("age", "18");

        return JsonResult.success(user);
    }

    @PostMapping("/user/addUser")
    public Object addUser(@RequestBody Map data) {
        System.out.println(data);
        return data;
    }
}

2.3、加密的实现

application.yml

secret:
  key: 1234567890123456 # 密钥位数为16位
  enabled: true # 开启加解密功能

SecretData.java

package com.example.demo.annotation;


import org.springframework.web.bind.annotation.Mapping;

import java.lang.annotation.*;

/**
 * 只有添加有该注解的Controller类或是具体接口方法才进行数据的加密解密
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Mapping
@Documented
public @interface SecretData {

}

DecryptRequestBodyAdvice.java

package com.example.demo.config;

import com.example.demo.annotation.SecretData;
import com.example.demo.request.JsonRequest;
import com.example.demo.service.SecretDataService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
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.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;

import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;

/**
 * 对请求内容进行解密
 * 只有开启了加解密功能才会生效
 * 仅对使用了@RqestBody注解的生效
 * https://blog.csdn.net/xingbaozhen1210/article/details/98189562
 */
@ControllerAdvice
// @ConditionalOnProperty(name = "secret.enabled", havingValue = "true")
public class DecryptRequestBodyAdvice extends RequestBodyAdviceAdapter {

    @Resource
    private SecretDataService secretDataService;

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType,
                            Class> converterType) {
        return methodParameter.getMethod().isAnnotationPresent(SecretData.class)
                || methodParameter.getMethod().getDeclaringClass().isAnnotationPresent(SecretData.class);
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
                                           Class> converterType) throws IOException {
        System.out.println("beforeBodyRead");

        String body = inToString(inputMessage.getBody());
        System.out.println(body);

        ObjectMapper objectMapper = new ObjectMapper();
        JsonRequest jsonRequest = objectMapper.readValue(body, JsonRequest.class);

        // 默认取data数据,如果提交加密数据则解密
        String decryptData = null;
        if (jsonRequest.getEncryptData() != null) {
            decryptData = secretDataService.decrypt(jsonRequest.getEncryptData());
        } else{
            decryptData = objectMapper.writeValueAsString(jsonRequest.getData());
        }

        String data = decryptData;

        // 解密后的数据
        System.out.println(data);

        return new HttpInputMessage() {
            @Override
            public HttpHeaders getHeaders() {
                return inputMessage.getHeaders();
            }

            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream(data.getBytes());
            }
        };
    }

    /**
     * 读取输入流为字符串
     *
     * @param is
     * @return
     */
    private String inToString(InputStream is) {
        byte[] buf = new byte[10 * 1024];
        int length = -1;
        StringBuilder sb = new StringBuilder();
        try {
            while ((length = is.read(buf)) != -1) {
                sb.append(new String(buf, 0, length));
            }
            return sb.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

}

EncryptResponseBodyAdvice.java

package com.example.demo.config;

import com.example.demo.annotation.SecretData;
import com.example.demo.response.JsonResult;
import com.example.demo.response.JsonResultVO;
import com.example.demo.service.SecretDataService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
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 javax.annotation.Resource;

/**
 * 对响应内容加密
 */
@ControllerAdvice
@ConditionalOnProperty(name = "secret.enabled", havingValue = "true")
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice {

    @Resource
    private SecretDataService secretDataService;

    @Override
    public boolean supports(MethodParameter returnType, Class> converterType) {
        return returnType.getMethod().isAnnotationPresent(SecretData.class)
                || returnType.getMethod().getDeclaringClass().isAnnotationPresent(SecretData.class);
    }


    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        System.out.println("beforeBodyWrite");

        // 仅对JsonResult对象数据加密
        if (body instanceof JsonResult) {
            JsonResult jsonResult = (JsonResult) body;

            JsonResultVO jsonResultVO = new JsonResultVO();
            BeanUtils.copyProperties(jsonResult, jsonResultVO);

            String jsonStr = new ObjectMapper().writeValueAsString(jsonResult.getData());

            jsonResultVO.setEncryptData(secretDataService.encrypt(jsonStr));
            return jsonResultVO;
        } else {
            return body;
        }
    }
}


SecretConfig.java

package com.example.demo.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "secret")
public class SecretConfig {

    private Boolean enabled;

    private String key;

    public Boolean getEnabled() {
        return enabled;
    }

    public void setEnabled(Boolean enabled) {
        this.enabled = enabled;
    }

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

}

SecretDataService.java

package com.example.demo.service;

/**
 * 加密解密的接口
 */
public interface SecretDataService {
    /**
     * 数据加密
     *
     * @param data 待加密数据
     * @return String 加密结果
     */
    String encrypt(String data);

    /**
     * 数据解密
     *
     * @param data 待解密数据
     * @return String 解密后的数据
     */
    String decrypt(String data);
}

SecretDataServiceImpl.java

package com.example.demo.service.impl;


import com.example.demo.config.SecretConfig;
import com.example.demo.service.SecretDataService;
import com.example.demo.utils.CipherUtil;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * 具体的加解密实现
 */
@Service
public class SecretDataServiceImpl implements SecretDataService {
    @Resource
    private SecretConfig secretConfig;

    @Override
    public String decrypt(String data) {
        return CipherUtil.decrypt(secretConfig.getKey(), data);
    }

    @Override
    public String encrypt(String data) {
        return CipherUtil.encrypt(secretConfig.getKey(), data);
    }
}

CipherUtil.java

package com.example.demo.utils;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.util.Base64;

/**
 * 数据加密解密工具类
 * 加密后返回base64
 */
public class CipherUtil {
    /**
     * 定义加密算法
     */
    private static final String ALGORITHM = "AES/ECB/PKCS5Padding";

    /**
     * 解密
     *
     * @param secretKey
     * @param cipherText base64
     * @return
     */
    public static String decrypt(String secretKey, String cipherText) {
        // 将Base64编码的密文解码
        byte[] encrypted = Base64.getDecoder().decode(cipherText);

        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            SecretKeySpec key = new SecretKeySpec(secretKey.getBytes(), "AES");
            cipher.init(Cipher.DECRYPT_MODE, key);
            return new String(cipher.doFinal(encrypted));

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 加密
     *
     * @param secretKey
     * @param plainText base64
     * @return
     */
    public static String encrypt(String secretKey, String plainText) {
        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            SecretKeySpec key = new SecretKeySpec(secretKey.getBytes(), "AES");
            cipher.init(Cipher.ENCRYPT_MODE, key);
            return Base64.getEncoder().encodeToString(cipher.doFinal(plainText.getBytes(Charset.forName("UTF-8"))));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

浏览器中实现加密解密

templates/index.html



  
    
    接口数据加密解密
  
  
    
    
    
    
    

    

查看控制台

2.4、接口测试

  • 开发环境不加密更易于开发调试
  • 生产环境需要数据加密

前后端都可以通过参数 encryptData 判断对方提交/返回的数据是否为加密数据,如果是加密数据则进行解密操作

接口返回加密数据

GET http://127.0.0.1:8080/user/getUser

未加密的数据

{
    "message": "success",
    "data": {
        "name": "Tom",
        "age": "18"
    },
    "code": 0
}

返回数据

{
    "message": "success",
    "encryptData": "kun2Wvk2LNKICaXIeIExA7jKRyqOV0qCv5KQXFOzfpQ=",
    "code": 0
}

客户端提交参数

POST http://127.0.0.1:8080/user/addUser

提交数据

{
    "data": {
        "name": "Tom",
        "age": "18"
    }
}

提交加密数据

{
    "encryptData": "kun2Wvk2LNKICaXIeIExA7jKRyqOV0qCv5KQXFOzfpQ="
}

2.5、总结

服务端

  • 全局开关:通过控制secret.enabled=true全局开启返回数据加密
  • 全局局部:可以通过SecretData或者自定义PassSecretData来控制单个控制器或者单个接口的需要或不需要加密

客户端

  • 可以根据开发环境、测试环境、生产环境来控制是否开启加密
  • 需要注意,FormData传输文件的数据格式可以考虑不加密

相同点

  • 服务端和客户端都通过对方传输的encryptData来判断是否为加密数据
  • 服务端和客户端都可以根据自己的环境来决定是否开启数据加密

完整代码:https://github.com/mouday/spring-boot-demo/tree/master/SpringBoot-Secret

参考文章

  • 详解API接口如何安全的传输数据

本文来自网络,不代表协通编程立场,如若转载,请注明出处:https://net2asp.com/7842feb41b.html