优雅的处理API 接口敏感数据加密解密

本文介绍了如何通过自定义注解@Decrypt和@Encrypt实现API接口的敏感数据加密解密。文章展示了如何定义注解、配置文件、AES加密工具、统一返回数据对象以及如何在请求和响应中进行预处理。此外,还提供了一个实际应用示例,展示了如何在SpringMVC中利用ResponseBodyAdvice和RequestBodyAdvice进行加解密操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

优雅的处理API 接口敏感数据加密解密

前言

最近遇到了很多网站爬取和反爬取的问题,所以在此分享一种简单的 api 接口加密策略。

我们与客户端的接口交互过程中,为了更高的安全性,防止数据被其他人抓取,可能需要对部分接口进行加密(请求参数加密,服务端解密)、返回信息加密(服务端加密,客户端解密),但也不是所有的接口都这样,有些接口可能不需要,所以我们可以使用注解的方式来轻松达到此要求。

完成代码已发布至 github:https://github.com/wiltonicp/vihacker-cloud

关注我

一、定义注解

自定义注解 @Decrypt@Encrypt

使用方法:

在接口上面添加 @Encrypt 注解就对这个接口返回的数据进行加密

在接口上面添加 @Decrypt 注解就对这个接口参数进行解密

加密注解

package cc.vihackerframework.core.encrypt.annotation;

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

/**
 * Created by Ranger on 2021/9/14
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Encrypt {
}

解密注解

package cc.vihackerframework.core.encrypt.annotation;

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

/**
 * Created by Ranger on 2021/9/14
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.PARAMETER})
public @interface Decrypt {

}

二、定义配置文件

用于在配置文件配置加解密密钥,定义一个 ViHackerEncryptProperties 类来读取用户配置的 key:

package cc.vihackerframework.core.encrypt.properties;


import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * Created by Ranger on 2021/9/14
 */
@Getter
@Setter
@ConfigurationProperties(ViHackerEncryptProperties.PREFIX)
public class ViHackerEncryptProperties {

    public static final String PREFIX = "vihacker.encrypt";

    /**
     * 解密密钥
     */
    public String key;
}

三、AES 算法加密工具

加密这块有多种方案可以选择,对称加密、非对称加密,其中对称加密又可以使用 AES、DES、3DES 等不同算法,这里我们使用 Java 自带的 Cipher 来实现对称加密,使用 AES 算法

package cc.vihackerframework.core.encrypt.util;

import cc.vihackerframework.core.exception.Asserts;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Random;

/**
 * AES加、解密算法工具类
 * Created by Ranger on 2021/09/14
 */
@Slf4j
public class AesUtil {

    /**
     * 加密算法AES
     */
    private static final String KEY_ALGORITHM = "AES";

    /**
     * key的长度,Wrong key size: must be equal to 128, 192 or 256
     * 传入时需要16、24、36
     */
    private static final Integer KEY_LENGTH = 16 * 8;

    /**
     * 算法名称/加密模式/数据填充方式
     * 默认:AES/ECB/PKCS5Padding
     */
    private static final String ALGORITHMS = "AES/ECB/PKCS5Padding";

    /**
     * 后端AES的key,由静态代码块赋值
     */
    public static String key;

    /**
     * 不能在代码中创建
     * JceSecurity.getVerificationResult 会将其put进 private static final Map<Provider,Object>中,导致内存缓便被耗尽
     */
    private static final BouncyCastleProvider PROVIDER = new BouncyCastleProvider();

    static {
        key = getKey();
    }

    /**
     * 获取key
     */
    public static String getKey() {
        StringBuilder uid = new StringBuilder();
        //产生16位的强随机数
        Random rd = new SecureRandom();
        for (int i = 0; i < KEY_LENGTH / 8; i++) {
            //产生0-2的3位随机数
            int type = rd.nextInt(3);
            switch (type) {
                case 0:
                    //0-9的随机数
                    uid.append(rd.nextInt(10));
                    break;
                case 1:
                    //ASCII在65-90之间为大写,获取大写随机
                    uid.append((char) (rd.nextInt(25) + 65));
                    break;
                case 2:
                    //ASCII在97-122之间为小写,获取小写随机
                    uid.append((char) (rd.nextInt(25) + 97));
                    break;
                default:
                    break;
            }
        }
        return uid.toString();
    }

    /**
     * 加密
     *
     * @param content    加密的字符串
     * @param encryptKey key值
     */
    public static String encrypt(String content, String encryptKey) throws Exception {
        if(StringUtils.isBlank(encryptKey)){
            Asserts.fail("加密的 key 不能为空");
        }
        //设置Cipher对象
        Cipher cipher = Cipher.getInstance(ALGORITHMS, PROVIDER);
        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), KEY_ALGORITHM));

        //调用doFinal
        // 转base64
        return Base64.encodeBase64String(cipher.doFinal(content.getBytes(StandardCharsets.UTF_8)));

    }

    /**
     * 解密
     *
     * @param encryptStr 解密的字符串
     * @param decryptKey 解密的key值
     */
    public static String decrypt(String encryptStr, String decryptKey) throws Exception {
        if(StringUtils.isBlank(decryptKey)){
            Asserts.fail("解密的 key 不能为空");
        }
        //base64格式的key字符串转byte
        byte[] decodeBase64 = Base64.decodeBase64(encryptStr);

        //设置Cipher对象
        Cipher cipher = Cipher.getInstance(ALGORITHMS,PROVIDER);
        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(), KEY_ALGORITHM));

        //调用doFinal解密
        return new String(cipher.doFinal(decodeBase64));
    }
}

四、定义统一返回数据对象

用于api 接口统一返回数据模型

package cc.vihackerframework.core.api;

import lombok.Data;

import java.io.Serializable;

/**
 * 通用api 返回对象
 *
 * @author Ranger
 * @since 2021/1/15
 * @email wilton.icp@gmail.com
 */
@Data
public class ViHackerApiResult<T> implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 状态码
     */
    private long code;
    /**
     * 提示信息
     */
    private String message;
    /**
     * 时间戳
     */
    private long time;
    /**
     * 数据封装
     */
    private T data;

    /**
     * 数据是否加密
     */
    private Boolean encrypt = Boolean.FALSE;

    protected ViHackerApiResult() {
    }

    protected ViHackerApiResult(long code, String message) {
        this.code = code;
        this.message = message;
        this.time = System.currentTimeMillis();
    }

    protected ViHackerApiResult(long code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
        this.time = System.currentTimeMillis();
    }

    /**
     * 成功
     *
     * @param <T>
     * @return
     */
    public static <T> ViHackerApiResult<T> success() {
        return new ViHackerApiResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage());
    }

    /**
     * 成功
     *
     * @param <T>
     * @return
     */
    public static <T> ViHackerApiResult<T> success(String message) {
        return new ViHackerApiResult<T>(ResultCode.SUCCESS.getCode(), message);
    }

    /**
     * 成功返回结果
     *
     * @param data 获取的数据
     */
    public static <T> ViHackerApiResult<T> data(T data) {
        return new ViHackerApiResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
    }

    /**
     * 成功返回结果
     *
     * @param data    获取的数据
     * @param message 提示信息
     */
    public static <T> ViHackerApiResult<T> success(T data, String message) {
        return new ViHackerApiResult<T>(ResultCode.SUCCESS.getCode(), message, data);
    }

    /**
     * 失败返回结果
     *
     * @param errorCode 错误码
     */
    public static <T> ViHackerApiResult<T> failed(IErrorCode errorCode) {
        return new ViHackerApiResult<T>(errorCode.getCode(), errorCode.getMessage(), null);
    }

    /**
     * 失败返回结果
     *
     * @param errorCode 错误码
     * @param message   错误信息
     */
    public static <T> ViHackerApiResult<T> failed(IErrorCode errorCode, String message) {
        return new ViHackerApiResult<T>(errorCode.getCode(), message);
    }

    /**
     * 失败返回结果
     *
     * @param message 提示信息
     */
    public static <T> ViHackerApiResult<T> failed(String message) {
        return new ViHackerApiResult<T>(ResultCode.FAILED.getCode(), message);
    }

    /**
     * 失败返回结果
     */
    public static <T> ViHackerApiResult<T> failed() {
        return failed(ResultCode.FAILED);
    }

    /**
     * 参数验证失败返回结果
     */
    public static <T> ViHackerApiResult<T> validateFailed() {
        return failed(ResultCode.VALIDATE_FAILED);
    }

    /**
     * 参数验证失败返回结果
     *
     * @param message 提示信息
     */
    public static <T> ViHackerApiResult<T> validateFailed(String message) {
        return new ViHackerApiResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message);
    }

    /**
     * 未登录返回结果
     */
    public static <T> ViHackerApiResult<T> unauthorized(T data) {
        return new ViHackerApiResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data);
    }

    /**
     * 未授权返回结果
     */
    public static <T> ViHackerApiResult<T> forbidden(T data) {
        return new ViHackerApiResult<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data);
    }

    /**
     * 自定义返回结果
     */
    public static <T> ViHackerApiResult<T> customize(long code, String message) {
        return new ViHackerApiResult<T>(code, message);
    }
}

五、请求和响应进行预处理

我们有多种方式可以对数据进行拦截,从而去实现加解密,而在SpringMVC 中给我们提供了 ResponseBodyAdvice 和 RequestBodyAdvice,利用这两个工具可以对请求和响应进行预处理,非常方便,所以我们在这利用它们来实现对数据的加密和解密。

接口加密

package cc.vihackerframework.core.encrypt.response;

import cc.vihackerframework.core.api.ViHackerApiResult;
import cc.vihackerframework.core.encrypt.annotation.Encrypt;
import cc.vihackerframework.core.encrypt.properties.ViHackerEncryptProperties;
import cc.vihackerframework.core.encrypt.util.AesUtil;
import cc.vihackerframework.core.exception.Asserts;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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;

/**
 * 接口加密
 * Created by Ranger on 2021/9/15
 */
@Slf4j
@ControllerAdvice
public class ViHackerEncryptResponse implements ResponseBodyAdvice<ViHackerApiResult> {

    private ObjectMapper om = new ObjectMapper();

    @Resource
    private ViHackerEncryptProperties encryptProperties;

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

    @Override
    public ViHackerApiResult beforeBodyWrite(ViHackerApiResult body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        String key = encryptProperties.getKey();
        if(StringUtils.isBlank(key)){
            Asserts.fail("加密的 key 不能为空,请添加如下配置:\nvihacker:\n" +
                    "  encrypt:\n" +
                    "    key:");
        }
        try {
            if (body.getData() != null) {
                body.setData(AesUtil.encrypt(om.writeValueAsBytes(body.getData()).toString(), key));
                body.setEncrypt(Boolean.TRUE);
            }
        } catch (Exception e) {
            log.error("接口返回加密失败:{}",e.getMessage());
        }
        return body;
    }
}

我们在这实现了 ResponseBodyAdvice 接口,实现了两个方法。

1、supports:用于判断在什么情况下我们去实现加密的流程,我们这里就是判断它是否使用了 @Encrypt 注解。

2、beforeBodyWrite:这个方法一般会在响应之前去执行,所以我们在这对返回的数据进行二次处理,对数据进行加密。

解密接口

package cc.vihackerframework.core.encrypt.response;

import cc.vihackerframework.core.encrypt.annotation.Decrypt;
import cc.vihackerframework.core.encrypt.properties.ViHackerEncryptProperties;
import cc.vihackerframework.core.encrypt.util.AesUtil;
import cc.vihackerframework.core.exception.Asserts;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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;

/**
 * 解密接口
 * Created by Ranger on 2021/9/15
 */
@Slf4j
@ControllerAdvice
public class ViHackerDecryptRequest extends RequestBodyAdviceAdapter {

    @Resource
    private ViHackerEncryptProperties encryptProperties;

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

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        if(StringUtils.isBlank(encryptProperties.getKey())){
            Asserts.fail("解密的 key 不能为空,请添加如下配置:\nvihacker:\n" +
                    "  encrypt:\n" +
                    "    key:");
        }
        byte[] body = new byte[inputMessage.getBody().available()];
        String bodyStr = new String(body);
        try {
            String decryptStr = AesUtil.decrypt(bodyStr, encryptProperties.getKey());
            byte[] decrypt = decryptStr.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) {
            log.error("参数解密失败:{}",e.getMessage());
            e.printStackTrace();
        }
        return super.beforeBodyRead(inputMessage, parameter, targetType, converterType);
    }
}

我们在这继承了 RequestBodyAdvice 接口的子类 RequestBodyAdviceAdapter,实现了两个方法。

1、supports:用于判断在什么情况下我们去实现解密的流程,我们这里就是判断它是否使用了 @Decrypt 注解。

2、beforeBodyRead:这个方法一般会在参数转换成具体的对象之前执行,所以我们在这对参数进行,对数据进行加密。

至此,就已经完成了所有的流程。

六、使用测试

application.yml配置

vihacker:
  encrypt:
    key: Av0qiVlXXtlpu19W

controller

package cc.vihackerframework.demo.controller;

import cc.vihackerframework.core.api.ViHackerApiResult;
import cc.vihackerframework.core.encrypt.annotation.Encrypt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

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

/**
 * Created by Ranger on 2021/9/15
 */
@RestController
public class DemoController {

    @Encrypt
    @GetMapping("/")
    public ViHackerApiResult test(){
        Map<String,Object> map = new HashMap<>();
        map.put("username",1881818181);
        map.put("password","admin");
        return ViHackerApiResult.data(map);
    }
}

调用情况:

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值