一个注解实现SpringBoot接口请求数据和返回数据加密,提高系统安全性!

发布于:2024-05-09 ⋅ 阅读:(32) ⋅ 点赞:(0)

1、前言

  起因是公司给人开发的内网系统登录接口采用明文传输,本来用着没啥问题(其实不太好!),毕竟只是内网使用。后面需要外网访问,需要提高安全等级,禁止密码明文传输。于是就有了这篇文章。本文采用:对称加密
有问题联系Qq:1101165230

1.1、前端必看

  需要说明的是,加密传输是需要前端配合的,毕竟请求接口的是前端。所以前端也需要知道加密模式、填充方式与偏移量等基础知识。同时需要商定是对称加密还是非对称加密。

  前端加密快速上手

1.2、后端必看

点开链接:按照目录浏览

  后端加密快速上手

2、后端注解实现

  好的,你已经了解了上面的基础知识,接下来你准备好为你项目编写一个牛x的功能了吗?

2.1、实现流程

实现流程描述

2.2、开始实现

开始前我们现在application.yml 中增加一个配置

# 配置是否开启AOP参数加密解密,不配置默认为true
MySecret:
  isSecret: true 

2.2.1、 pom

	<!-- AOP切面依赖 -->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-aop</artifactId>
	</dependency>
	
	<!-- lombok工具 -->
	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<optional>true</optional>
	</dependency>
	
	<!-- json操作类 -->
	<dependency>
		<groupId>com.alibaba</groupId>
		<artifactId>fastjson</artifactId>
		<version>1.2.52.sec06</version>
	</dependency>

	 <!-- String工具包 -->
	<dependency>
		<groupId>org.apache.commons</groupId>
		 <artifactId>commons-lang3</artifactId>
	</dependency>

2.2.2、 注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Secret {

    Class value();

    // 参数类中传递加密数据的属性名,默认encryptStr
    String encryptStrName() default "encryptStr";
}

2.2.3、 加密工具类

  注意AES_KEY需要更改,切记和前端商量

import com.alibaba.fastjson.JSON;
import com.example.undertwo.entity.vo.UserVO;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Base64.Decoder;
import java.util.Base64.Encoder;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author by Guoshun
 * @version 1.0.0
 * @description 支持AES、DES、RSA加密、数字签名以及生成对称密钥和非对称密钥对
 * @date 2024/5/8 15:04
 */
public class CryptoUtils {

    public static final String AES_KEY = "IQcoqkg==";
    private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
    private static final Encoder BASE64_ENCODER = Base64.getEncoder();
    private static final Decoder BASE64_DECODER = Base64.getDecoder();
    private static final Map<Algorithm, KeyFactory> KEY_FACTORY_CACHE = new ConcurrentHashMap<>();
    private static final Map<Algorithm, Cipher> CIPHER_CACHE = new HashMap<>();

    /**
     * 对称加密
     *
     * @param secretKey 密钥
     * @param iv        加密向量,只有CBC模式才支持,如果是CBC则必传
     * @param plainText 明文
     * @param algorithm 对称加密算法,如AES、DES
     * @return
     * @throws Exception
     */
    public static String encryptSymmetrically(String secretKey, String iv, String plainText, Algorithm algorithm) throws Exception {
        SecretKey key = decodeSymmetricKey(secretKey, algorithm);
        IvParameterSpec ivParameterSpec = StringUtils.isBlank(iv) ? null : decodeIv(iv);
        byte[] plainTextInBytes = plainText.getBytes(DEFAULT_CHARSET);
        byte[] ciphertextInBytes = transform(algorithm, Cipher.ENCRYPT_MODE, key, ivParameterSpec, plainTextInBytes);

        return BASE64_ENCODER.encodeToString(ciphertextInBytes);
    }

    /**
     * 对称解密
     *
     * @param secretKey  密钥
     * @param iv         加密向量,只有CBC模式才支持,如果是CBC则必传
     * @param ciphertext 密文
     * @param algorithm  对称加密算法,如AES、DES
     * @return
     * @throws Exception
     */
    public static String decryptSymmetrically(String secretKey, String iv, String ciphertext, Algorithm algorithm) throws Exception {
        SecretKey key = decodeSymmetricKey(secretKey, algorithm);
        IvParameterSpec ivParameterSpec = StringUtils.isBlank(iv) ? null : decodeIv(iv);
        byte[] ciphertextInBytes = BASE64_DECODER.decode(ciphertext);
        byte[] plainTextInBytes = transform(algorithm, Cipher.DECRYPT_MODE, key, ivParameterSpec, ciphertextInBytes);
        return new String(plainTextInBytes, DEFAULT_CHARSET);
    }

    /**
     * 将密钥进行Base64位解码,重新生成SecretKey实例
     *
     * @param secretKey 密钥
     * @param algorithm 算法
     * @return
     */
    private static SecretKey decodeSymmetricKey(String secretKey, Algorithm algorithm) {
        byte[] key = BASE64_DECODER.decode(secretKey);
        return new SecretKeySpec(key, algorithm.getName());
    }

    private static IvParameterSpec decodeIv(String iv) {
        byte[] ivInBytes = BASE64_DECODER.decode(iv);
        return new IvParameterSpec(ivInBytes);
    }

    private static byte[] transform(Algorithm algorithm, int mode, Key key, IvParameterSpec iv, byte[] msg) throws Exception {
        Cipher cipher = CIPHER_CACHE.get(algorithm);
        // double check,减少上下文切换
        if (cipher == null) {
            synchronized (CryptoUtils.class) {
                if ((cipher = CIPHER_CACHE.get(algorithm)) == null) {
                    cipher = determineWhichCipherToUse(algorithm);
                    CIPHER_CACHE.put(algorithm, cipher);
                }
                cipher.init(mode, key, iv);
                return cipher.doFinal(msg);
            }
        }

        synchronized (CryptoUtils.class) {
            cipher.init(mode, key, iv);
            return cipher.doFinal(msg);
        }
    }

    private static Cipher determineWhichCipherToUse(Algorithm algorithm) throws NoSuchAlgorithmException, NoSuchPaddingException {
        Cipher cipher;
        String transformation = algorithm.getTransformation();
        // 官方推荐的transformation使用algorithm/mode/padding组合,SunJCE使用ECB作为默认模式,使用PKCS5Padding作为默认填充
        if (StringUtils.isNotEmpty(transformation)) {
            cipher = Cipher.getInstance(transformation);
        } else {
            cipher = Cipher.getInstance(algorithm.getName());
        }

        return cipher;
    }

    /**
     * 算法分为加密算法和签名算法,更多算法实现见:<br/>
     * <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#impl">jdk8中的标准算法</a>
     */
    public static class Algorithm {

        @Getter
        private String name;
        @Getter
        private String transformation;
        @Getter
        private int keySize;

        public Algorithm(String name, int keySize) {
            this(name, null, keySize);
        }

        public Algorithm(String name, String transformation, int keySize) {
            this.name = name;
            this.transformation = transformation;
            this.keySize = keySize;
        }

        public interface Encryption {
            Algorithm AES_ECB_PKCS5 = new Algorithm("AES", "AES/ECB/PKCS5Padding", 128);
            Algorithm AES_CBC_PKCS5 = new Algorithm("AES", "AES/CBC/PKCS5Padding", 128);
            Algorithm DES_ECB_PKCS5 = new Algorithm("DES", "DES/ECB/PKCS5Padding", 56);
            Algorithm DES_CBC_PKCS5 = new Algorithm("DES", "DES/CBC/PKCS5Padding", 56);
            Algorithm RSA_ECB_PKCS1 = new Algorithm("RSA", "RSA/ECB/PKCS1Padding", 1024);
            Algorithm DSA = new Algorithm("DSA", 1024);
        }

        public interface Signing {
            Algorithm SHA1WithDSA = new Algorithm("SHA1withDSA", 1024);
            Algorithm SHA1WithRSA = new Algorithm("SHA1WithRSA", 2048);
            Algorithm SHA256WithRSA = new Algorithm("SHA256WithRSA", 2048);
        }

    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class AsymmetricKeyPair {

        private String publicKey;
        private String privateKey;
    }

    /**
     * 加密
     * @param jsonStr
     * @return
     * @throws Exception
     */
    public static String encrypt(String jsonStr) throws Exception {
       return CryptoUtils.encryptSymmetrically(AES_KEY, null, jsonStr, Algorithm.Encryption.AES_ECB_PKCS5);
    }

    /**
     * 解密
     * @param jsonStr
     * @return
     * @throws Exception
     */
    public static String decrypt(String jsonStr) throws Exception {
       return CryptoUtils.decryptSymmetrically(AES_KEY, null, jsonStr, Algorithm.Encryption.AES_ECB_PKCS5);
    }

    public static void main(String[] args) throws Exception {
        UserVO userVO = new UserVO();
        userVO.setId(123);
        userVO.setName("阿萨");
        String encrypt = encrypt(JSON.toJSONString(userVO));
        System.out.println("加密后:" + encrypt);
        String decrypt = decrypt(encrypt);
        UserVO userVO1 = JSON.parseObject(decrypt, UserVO.class);
        System.out.println("解密后" + userVO1.toString());

    }

2.2.3、 定义切面(注意切点包名)

import com.alibaba.fastjson.JSON;
import com.example.undertwo.config.Secret;
import com.example.undertwo.entity.result.ResultVO;
import com.example.undertwo.utils.CryptoUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.lang.reflect.Type;

/**
 * @author by Guoshun
 * @version 1.0.0
 * @description 切面加密解密
 * @date 2024/5/9 14:14
 */
@Aspect
@Component
@Slf4j
public class SecretAOP {

    // 是否进行加密解密,通过配置文件注入(不配置默认为true)
    @Value("${MySecret.isSecret:true}")
    boolean isSecret;

    // 定义切点,使用了@Secret注解的类 或 使用了@Secret注解的方法
    @Pointcut("@within(com.example.undertwo.config.Secret) || @annotation(com.example.undertwo.config.Secret)")
    public void pointcut(){}

    // 环绕切面
    @Around("pointcut()")
    public ResultVO around(ProceedingJoinPoint point){
        ResultVO result = null;
        // 获取被代理方法参数
        Object[] args = point.getArgs();
        // 获取被代理对象
        Object target = point.getTarget();
        // 获取通知签名
        MethodSignature signature = (MethodSignature )point.getSignature();

        try {
            // 获取被代理方法
            Method pointMethod = target.getClass().getMethod(signature.getName(), signature.getParameterTypes());
            // 获取被代理方法上面的注解@Secret
            Secret secret = pointMethod.getAnnotation(Secret.class);
            // 被代理方法上没有,则说明@Secret注解在被代理类上
            if(secret==null){
                secret = target.getClass().getAnnotation(Secret.class);
            }

            if(secret!=null){
                // 获取注解上声明的加解密类
                Class clazz = secret.value();
                // 获取注解上声明的加密参数名
                String encryptStrName = secret.encryptStrName();

                for (int i = 0; i < args.length; i++) {
                    // 如果是clazz类型则说明使用了加密字符串encryptStr传递的加密参数
                    if(clazz.isInstance(args[i])){
                        Object cast = clazz.cast(args[i]);      //将args[i]转换为clazz表示的类对象
                        // 通过反射,执行getEncryptStr()方法,获取加密数据
                        Method method = clazz.getMethod(getMethedName(encryptStrName));
                        // 执行方法,获取加密数据
                        String encryptStr = (String) method.invoke(cast);
                        // 加密字符串是否为空
                        if(StringUtils.isNotBlank(encryptStr)){
                            // 解密
                            String json = CryptoUtils.decrypt(encryptStr);
                            // 转换vo
                            args[i] = JSON.parseObject(json, (Type) args[i].getClass());
                        }
                    }
                    // 其他类型,比如基本数据类型、包装类型就不使用加密解密了
                }
            }

            // 执行请求
            result = (ResultVO) point.proceed(args);

            // 判断配置是否需要返回加密
            if(isSecret){
                // 获取返回值json字符串
                String jsonString = JSON.toJSONString(result.getData());
                // 加密
                String s = CryptoUtils.encrypt(jsonString);
                result.setData(s);
            }

        } catch (NoSuchMethodException e) {
            log.error("@Secret注解指定的类没有字段:encryptStr,或encryptStrName参数字段不存在");
            e.printStackTrace();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return result;
    }

    // 转化方法名
    private String getMethedName(String name){
        String first = name.substring(0,1);
        String last = name.substring(1);
        first = StringUtils.upperCase(first);
        return "get" + first + last;
    }
}

2.2.4、 定义加密基类与各种入参VO

import lombok.Data;

/**
 * @author by Guoshun
 * @version 1.0.0
 * @description 基础的实体类
 * @date 2024/5/9 14:00
 */
@Data
public class BaseVO {
    private String encryptStr;
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author by Guoshun
 * @version 1.0.0
 * @description 部门类
 * @date 2024/5/9 14:16
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeptVO{

    private Integer id;

    private String deptName;

    // 自己实现的一个参数,用来给前端传递加密字符串
    private String encryptJson;

}
import com.example.undertwo.entity.BaseVO;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author by Guoshun
 * @version 1.0.0
 * @description TODO
 * @date 2024/5/9 14:05
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserVO extends BaseVO {

    private Integer id;

    private String name;

}

2.2.5、写两个Controller


import com.example.undertwo.config.Secret;
import com.example.undertwo.entity.result.ResultVO;
import com.example.undertwo.entity.vo.DeptVO;
import org.springframework.web.bind.annotation.*;


/**
 * @author by Guoshun
 * @version 1.0.0
 * @description 部门类
 * @date 2024/5/9 14:17
 */
@RestController
@RequestMapping("dept")
public class DeptController {

    @GetMapping("getDeptName/{id}")
    public ResultVO getDeptName(@PathVariable("id") String id){
        return new ResultVO(0,"查询成功","财务部" + id);
    }

    // 注解在方法上,并传递了encryptStrName自己定义的加密字符串名称encryptJson
    @Secret(value = DeptVO.class,encryptStrName = "encryptJson")
    @PostMapping("addDept")
    public ResultVO addDept(@RequestBody DeptVO dept){
        return new ResultVO(0,"新增成功",dept);
    }

}

import com.example.undertwo.config.Secret;
import com.example.undertwo.entity.BaseVO;
import com.example.undertwo.entity.result.ResultVO;
import com.example.undertwo.entity.vo.UserVO;
import org.springframework.web.bind.annotation.*;

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


/**
 * @author by Guoshun
 * @version 1.0.0
 * @description  用户控制类
 * @date 2024/5/9 14:10
 */
@Secret(BaseVO.class)                             //接口参数和返回要进行加解密
@RestController
@RequestMapping("user")
public class UserController {

    //采用内部类的实例代码块方式初始化map
    HashMap<Integer, UserVO> userMap = new HashMap<Integer, UserVO>(){
        {
            put(1,new UserVO(1,"张三"));
            put(2,new UserVO(2,"李四"));
            put(3,new UserVO(3,"王五"));
        }
    };

    // 通过id查询用户
    @GetMapping("getUserName/{id}")
    public ResultVO getUserName(@PathVariable("id")  Integer id){
        return new ResultVO(0,"查询成功",userMap.get(id));
    }

    // 通过name查询用户id
    @GetMapping("getUserId")
    public ResultVO getUserId(@RequestParam  String name){
        Iterator<Map.Entry<Integer, UserVO>> iterator = userMap.entrySet().iterator();
        UserVO u = null;
        while (iterator.hasNext()){
            Map.Entry<Integer, UserVO> entry = iterator.next();
            if(entry.getValue().getName().equals(name)){
                u = entry.getValue();
                break;
            }
        }
        return new ResultVO(0,"查询成功",u);
    }

    // 新增用户
    @PostMapping("addUser")
    public ResultVO addUser(@RequestBody UserVO user){
        return new ResultVO(0,"新增成功",user);
    }

    // 更改用户
    @PostMapping("updateUser")
    public ResultVO updateUser(@RequestBody UserVO user) throws Throwable {
        if(user==null||user.getId()==null){
            throw new NullPointerException();
        }else{
            return new ResultVO(0,"修改成功",user);
        }
    }
}

3、参考文章

[1]:springboot 自定义注解使用AOP实现请求参数解密以及响应数据加密
[2]:前端CryptoJS和Java后端数据互相加解密(AES)