Spring Boot 实战:RSA+AES 接口全链路加解密(防篡改 / 防重放)

Spring Boot 实战:RSA+AES 接口全链路加解密(防篡改 / 防重放)

😄生命不息,写作不止

🔥 继续踏上学习之路,学之分享笔记

👊 总有一天我也能像各位大佬一样

🏆 博客首页 @怒放吧德德 To记录领地 @一个有梦有戏的人

🌝分享学习心得,欢迎指正,大家一起学习成长!

转发请携带作者信息 @怒放吧德德(掘金) @一个有梦有戏的人(CSDN)

前言

本项目演示了如何在 Spring Boot 应用中实现安全的 API 接口加密传输机制,采用 RSA + AES-GCM 混合加密方案,确保敏感数据在传输过程中的机密性和完整性。基于 Spring Boot 的 API 接口加密传输解决方案,实现前后端数据的端到端加密通信。

1 方案背景:为什么HTTPS之外还要做接口加密?

很多开发者会有疑问:我已经用了HTTPS,为什么还要做接口级加解密?

核心答案是:HTTPS只解决了传输链路的安全,无法覆盖「终端被Hook、App被逆向、API网关日志泄露、内网流量被侧录」等场景的风险

HTTPS是"防路上被偷看",而接口级加密是"防终点被看穿",两者解决的问题完全不同,具体对比如下:

风险场景 HTTPS 接口级加密
传输链路被监听 ✅ 防护 ✅ 防护
终端被Hook/抓包工具解密 ❌ 无法防护 ✅ 防护
App被逆向、API逻辑被破解 ❌ 无法防护 ✅ 防护
网关/代理日志明文泄露 ❌ 无法防护 ✅ 防护
内网不可信链路流量被侧录 ❌ 无法防护 ✅ 防护

2 整体方案设计

核心特性:

  • 混合加密方案:RSA-OAEP 加密 AES 密钥,AES-GCM 加密业务数据
  • 防重放攻击:基于时间戳 + Nonce 的双重防护机制
  • 数据完整性校验:AES-GCM 模式自带认证标签,防止数据篡改
  • 注解式开发:通过 @DecryptRequestBody 和 @EncryptResponseBody 注解实现透明加解密
  • 前后端兼容:与 Web Crypto API 完全兼容,支持浏览器原生加密

2.1 核心设计目标

核心目标 落地方案
密钥传输安全 RSA-OAEP 非对称加密(兼容WebCrypto,规避老旧填充方式的安全风险)
业务数据加解密 AES-256-GCM 对称加密(自带完整性校验,CPU指令集加速,支持AAD防篡改)
防请求篡改 GCM-AAD 附加认证数据(timestamp+nonce参与认证,篡改直接解密失败)
防请求重放 timestamp时间窗口校验 + nonce唯一值幂等控制(Redis实现)
业务零侵入 自定义注解 + 参数解析器 + 响应增强,Controller无需感知加解密逻辑
跨端兼容性 前后端算法100%对齐,支持Web/小程序/App多端对接
生产可运维 支持密钥平滑轮换、统一异常处理、全链路日志追踪

2.2 全链路时序流程

  1. 客户端提前请求服务端,获取RSA公钥
  2. 客户端生成随机AES-256密钥(一请求一密钥,杜绝密钥泄露风险)
  3. 客户端用AES-GCM加密业务请求数据,将timestamp+nonce作为AAD附加认证数据
  4. 客户端用服务端RSA公钥,通过RSA-OAEP加密本次的AES密钥
  5. 客户端封装加密后的AES密钥、业务数据、timestamp、nonce为统一请求体,发送至服务端
  6. 服务端校验timestamp时间窗口(默认5分钟),过期直接拒绝;校验nonce是否已使用,已使用则判定为重放请求
  7. 服务端用RSA私钥解密,拿到本次请求的AES密钥
  8. 服务端用AES密钥+相同AAD解密业务数据,反序列化为对应DTO对象
  9. 业务处理完成后,服务端用本次请求的AES密钥加密响应数据,返回给客户端
  10. 客户端用本地保存的AES密钥解密响应数据,拿到业务结果

2.3 项目工程结构

代码见 gitee: Trial5-api-security-demo

plain 复制代码
src/main/java/com/storm/security/
├── annotation/          // 自定义注解
│   ├── DecryptRequestBody.java    // 请求解密注解
│   └── EncryptResponseBody.java   // 响应加密注解
├── config/              // 配置类
│   ├── EncryptionConfig.java      // 密钥、工具类Bean配置
│   └── WebMvcConfig.java          // 参数解析器、响应增强注册
├── constant/            // 常量类
│   └── EncryptionConstant.java    // 算法、超时时间等常量
├── controller/          // 业务接口
│   ├── KeyController.java         // 公钥获取接口
│   └── UserController.java        // 业务测试接口
├── dto/                 // 数据传输对象
│   ├── EncryptedRequest.java      // 统一加密请求体
│   ├── EncryptedResponse.java     // 统一加密响应体
│   └── UserDTO.java               // 业务DTO示例
├── encryption/          // 加解密核心工具
│   ├── RsaUtil.java               // RSA-OAEP加解密工具
│   ├── AesUtil.java               // AES-GCM加解密工具
│   └── KeyPairManager.java        // RSA密钥对管理(支持轮换)
├── exception/           // 异常处理
│   ├── EncryptionException.java   // 加解密自定义异常
│   └── GlobalExceptionHandler.java// 全局异常统一处理
├── resolver/            // 参数解析器
│   └── DecryptRequestBodyResolver.java // 自动解密参数解析器
├── advice/              // 响应增强
│   └── EncryptResponseBodyAdvice.java  // 自动加密响应增强
└── service/             // 核心调度
    └── EncryptionService.java     // 加解密核心调度服务

3 核心技术选型说明

3.1 RSA算法:必须统一RSA-OAEP,拒绝PKCS1Padding

大量教程都会踩的核心坑:Java默认的RSA/ECB/PKCS1Padding与前端WebCrypto API默认的RSA-OAEP完全不兼容,且PKCS1Padding属于老旧填充方式,存在明文攻击风险,已被NIST不推荐使用。

✅ 生产级正确做法:前后端统一使用RSA/ECB/OAEPWithSHA-256AndMGF1Padding,完全兼容WebCrypto,安全性更高。

3.2 AES算法:必须选GCM模式,拒绝CBC/CTR

AES模式 是否推荐 核心问题
AES-CBC ❌ 不推荐 仅提供加密,无完整性校验,存在Padding Oracle攻击风险
AES-CTR ❌ 不推荐 流加密模式,无完整性校验,无法防篡改
AES-GCM ✅ 生产推荐 同时实现「加密+完整性认证+防篡改」,CPU有指令集加速,性能优异,天然支持AAD附加认证数据

GCM的AAD特性是防篡改的核心:我们将timestamp和nonce作为AAD数据,不参与加密但参与认证,一旦这两个字段被篡改,解密会直接抛出AEADBadTagException,从根本上杜绝请求被篡改的风险。

4 完整代码实现

采用 SpringBoot3.x + Java17 完成

4.1 核心依赖(pom.xml)

plain 复制代码
<!-- Spring Boot Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data Redis(防重放nonce存储) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Lombok(简化代码) -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<!-- Jackson JSON处理 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

4.2 常量类(统一管理魔法值)

plain 复制代码
public class EncryptionConstant {
    // RSA算法配置
    public static final String RSA_ALGORITHM = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
    public static final int RSA_KEY_SIZE = 2048;
    // AES算法配置
    public static final String AES_ALGORITHM = "AES/GCM/NoPadding";
    public static final int AES_KEY_SIZE = 256;
    public static final int GCM_IV_LENGTH = 12;
    public static final int GCM_TAG_LENGTH = 128;
    // 防重放配置
    public static final long REQUEST_EXPIRE_TIME = 5 * 60 * 1000L;
    public static final long NONCE_EXPIRE_TIME = 5;
    public static final TimeUnit NONCE_EXPIRE_UNIT = TimeUnit.MINUTES;
    public static final String NONCE_REDIS_KEY_PREFIX = "security:nonce:";
    // 字符集
    public static final Charset CHARSET = StandardCharsets.UTF_8;
}

4.3 自定义异常类

plain 复制代码
public class EncryptionException extends RuntimeException {
    public EncryptionException(String message) {
        super(message);
    }

    public EncryptionException(String message, Throwable cause) {
        super(message, cause);
    }
}

4.4 RSA-OAEP加解密工具类

plain 复制代码
@Slf4j
public class RsaUtil {

    public String encrypt(String data, Key publicKey) {
        try {
            Cipher cipher = Cipher.getInstance(EncryptionConstant.RSA_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            byte[] encryptedData = cipher.doFinal(data.getBytes(EncryptionConstant.CHARSET));
            return Base64.getEncoder().encodeToString(encryptedData);
        } catch (Exception e) {
            log.error("RSA加密失败", e);
            throw new EncryptionException("RSA加密失败", e);
        }
    }

    public String decrypt(String encryptedData, Key privateKey) {
        try {
            Cipher cipher = Cipher.getInstance(EncryptionConstant.RSA_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            byte[] decodedData = Base64.getDecoder().decode(encryptedData);
            byte[] decryptedData = cipher.doFinal(decodedData);
            return new String(decryptedData, EncryptionConstant.CHARSET);
        } catch (Exception e) {
            log.error("RSA解密失败", e);
            throw new EncryptionException("RSA解密失败,密钥不匹配或数据被篡改", e);
        }
    }
}

4.5 AES-GCM加解密工具类

java 复制代码
@Slf4j
public class AesUtil {

    public String generateAesKey() {
        byte[] key = new byte[EncryptionConstant.AES_KEY_SIZE / 8];
        new SecureRandom().nextBytes(key);
        return Base64.getEncoder().encodeToString(key);
    }

    public String encrypt(String data, String base64Key, String aad) {
        try {
            byte[] keyBytes = Base64.getDecoder().decode(base64Key);
            if (keyBytes.length != EncryptionConstant.AES_KEY_SIZE / 8) {
                throw new EncryptionException("AES密钥长度必须为256位");
            }
            SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
            byte[] iv = new byte[EncryptionConstant.GCM_IV_LENGTH];
            new SecureRandom().nextBytes(iv);

            Cipher cipher = Cipher.getInstance(EncryptionConstant.AES_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, keySpec, new GCMParameterSpec(EncryptionConstant.GCM_TAG_LENGTH, iv));
            cipher.updateAAD(aad.getBytes(EncryptionConstant.CHARSET));
            byte[] encryptedData = cipher.doFinal(data.getBytes(EncryptionConstant.CHARSET));

            byte[] result = new byte[iv.length + encryptedData.length];
            System.arraycopy(iv, 0, result, 0, iv.length);
            System.arraycopy(encryptedData, 0, result, iv.length, encryptedData.length);
            return Base64.getEncoder().encodeToString(result);
        } catch (EncryptionException e) {
            throw e;
        } catch (Exception e) {
            log.error("AES加密失败", e);
            throw new EncryptionException("AES加密失败", e);
        }
    }

    public String decrypt(String encryptedData, String base64Key, String aad) {
        try {
            byte[] keyBytes = Base64.getDecoder().decode(base64Key);
            if (keyBytes.length != EncryptionConstant.AES_KEY_SIZE / 8) {
                throw new EncryptionException("AES密钥长度必须为256位");
            }
            SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");

            byte[] decodedData = Base64.getDecoder().decode(encryptedData);
            byte[] iv = new byte[EncryptionConstant.GCM_IV_LENGTH];
            byte[] cipherBody = new byte[decodedData.length - EncryptionConstant.GCM_IV_LENGTH];
            System.arraycopy(decodedData, 0, iv, 0, iv.length);
            System.arraycopy(decodedData, iv.length, cipherBody, 0, cipherBody.length);

            Cipher cipher = Cipher.getInstance(EncryptionConstant.AES_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, keySpec, new GCMParameterSpec(EncryptionConstant.GCM_TAG_LENGTH, iv));
            cipher.updateAAD(aad.getBytes(EncryptionConstant.CHARSET));
            byte[] decryptedData = cipher.doFinal(cipherBody);
            return new String(decryptedData, EncryptionConstant.CHARSET);
        } catch (EncryptionException e) {
            throw e;
        } catch (Exception e) {
            log.error("AES解密失败", e);
            throw new EncryptionException("AES解密失败,密钥不匹配、数据被篡改或AAD不一致", e);
        }
            }
}

4.6 RSA密钥对管理(支持平滑轮换)

java 复制代码
@Getter
public class KeyPairManager {
    private final RSAPublicKey publicKey;
    private final RSAPrivateKey privateKey;
    private final String publicKeyBase64;

    // 生产环境建议从配置中心/KMS加载,支持热更新与平滑轮换
    public KeyPairManager() throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(EncryptionConstant.RSA_KEY_SIZE);
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        this.publicKey = (RSAPublicKey) keyPair.getPublic();
        this.privateKey = (RSAPrivateKey) keyPair.getPrivate();
        this.publicKeyBase64 = Base64.getEncoder().encodeToString(publicKey.getEncoded());
    }
}

4.7 配置类

java 复制代码
@Configuration
public class EncryptionConfig {

    @Bean
    public KeyPairManager keyPairManager() throws Exception {
        return new KeyPairManager();
    }

    @Bean
    public RsaUtil rsaUtil() {
        return new RsaUtil();
    }

    @Bean
    public AesUtil aesUtil() {
        return new AesUtil();
    }
}

4.8 统一请求与响应DTO

java 复制代码
// 加密请求体
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EncryptedRequest {
    private String encryptedKey;
    private String encryptedData;
    private Long timestamp;
    private String nonce;
}

// 加密响应体
package com.example.security.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EncryptedResponse {
    private String encryptedData;
    private int code;
    private String msg;
}

// 业务DTO示例
package com.example.security.dto;

import lombok.Data;

@Data
public class UserDTO {
    private String username;
    private String password;
    private String phone;
    private String email;
}

4.9 自定义注解

java 复制代码
// 请求解密注解
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptRequestBody {
}

// 响应加密注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptResponseBody {
}

4.10 核心加解密调度服务

java 复制代码
@Slf4j
@Service
@RequiredArgsConstructor
public class EncryptionService {
    private final KeyPairManager keyPairManager;
    private final RsaUtil rsaUtil;
    private final AesUtil aesUtil;
    private final ObjectMapper objectMapper;
    private final StringRedisTemplate redisTemplate;

    private static final ThreadLocal<String> AES_KEY_HOLDER = new ThreadLocal<>();
    private static final ThreadLocal<String> AAD_HOLDER = new ThreadLocal<>();

    // 请求解密
    public <T> T decrypt(EncryptedRequest request, Class<T> targetClass) {
        try {
            // 时间窗口校验
            long currentTime = System.currentTimeMillis();
            if (Math.abs(currentTime - request.getTimestamp()) > EncryptionConstant.REQUEST_EXPIRE_TIME) {
                throw new EncryptionException("请求已过期,请重新发起");
            }
            // nonce幂等校验
            String nonceKey = EncryptionConstant.NONCE_REDIS_KEY_PREFIX + request.getNonce();
            Boolean isFirstRequest = redisTemplate.opsForValue()
            .setIfAbsent(nonceKey, "1", EncryptionConstant.NONCE_EXPIRE_TIME, EncryptionConstant.NONCE_EXPIRE_UNIT);
            if (Boolean.FALSE.equals(isFirstRequest)) {
                throw new EncryptionException("重复请求,禁止访问");
            }
            // RSA解密获取AES密钥
            String aesKey = rsaUtil.decrypt(request.getEncryptedKey(), keyPairManager.getPrivateKey());
            // 构建AAD,与前端完全一致
            String aad = request.getTimestamp() + ":" + request.getNonce();
            // AES解密业务数据
            String jsonData = aesUtil.decrypt(request.getEncryptedData(), aesKey, aad);
            // 存入ThreadLocal用于响应加密
            AES_KEY_HOLDER.set(aesKey);
            AAD_HOLDER.set(aad);
            // 反序列化为目标对象
            return objectMapper.readValue(jsonData, targetClass);
        } catch (EncryptionException e) {
            throw e;
        } catch (Exception e) {
            log.error("请求解密失败", e);
            throw new EncryptionException("请求解密失败", e);
        }
    }

    // 响应加密
    public String encrypt(Object data) {
        try {
            String aesKey = AES_KEY_HOLDER.get();
            String aad = AAD_HOLDER.get();
            if (aesKey == null || aad == null) {
                throw new EncryptionException("响应加密失败,未获取到请求密钥信息");
            }
            String jsonData = objectMapper.writeValueAsString(data);
            return aesUtil.encrypt(jsonData, aesKey, aad);
        } catch (EncryptionException e) {
            throw e;
        } catch (Exception e) {
            log.error("响应加密失败", e);
            throw new EncryptionException("响应加密失败", e);
        } finally {
            // 清除ThreadLocal,避免内存泄漏
            AES_KEY_HOLDER.remove();
            AAD_HOLDER.remove();
        }
    }
}

4.11 自动解密参数解析器

java 复制代码
@RequiredArgsConstructor
public class DecryptRequestBodyResolver implements HandlerMethodArgumentResolver {
    private final EncryptionService encryptionService;
    private final ObjectMapper objectMapper;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(DecryptRequestBody.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        EncryptedRequest encryptedRequest = objectMapper.readValue(request.getInputStream(), EncryptedRequest.class);
        return encryptionService.decrypt(encryptedRequest, parameter.getParameterType());
    }
}

4.12 自动加密响应增强

java 复制代码
@RestControllerAdvice
@RequiredArgsConstructor
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    private final EncryptionService encryptionService;

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return returnType.hasMethodAnnotation(EncryptResponseBody.class)
        || returnType.getContainingClass().isAnnotationPresent(EncryptResponseBody.class);
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        String encryptedData = encryptionService.encrypt(body);
        return EncryptedResponse.builder()
        .encryptedData(encryptedData)
        .code(200)
        .msg("success")
        .build();
    }
}

4.13 WebMvc配置与全局异常处理

java 复制代码
// WebMvc配置
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
    private final EncryptionService encryptionService;
    private final ObjectMapper objectMapper;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new DecryptRequestBodyResolver(encryptionService, objectMapper));
    }
}

// 全局异常处理
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(EncryptionException.class)
    public ResponseEntity<Map<String, Object>> handleEncryptionException(EncryptionException e) {
        log.error("加解密异常:{}", e.getMessage(), e);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
        .body(Map.of("code", 400, "msg", e.getMessage()));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, Object>> handleException(Exception e) {
        log.error("系统异常:{}", e.getMessage(), e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
        .body(Map.of("code", 500, "msg", "系统内部错误"));
    }
}

4.14 业务Controller示例

java 复制代码
// 公钥获取接口
@RestController
@RequestMapping("/api/key")
@RequiredArgsConstructor
public class KeyController {
    private final KeyPairManager keyPairManager;

    @GetMapping("/public")
    public ResponseEntity<Map<String, String>> getPublicKey() {
        return ResponseEntity.ok(Map.of("publicKey", keyPairManager.getPublicKeyBase64()));
    }
}

// 业务接口
@RestController
@RequestMapping("/api/user")
public class UserController {

    @PostMapping("/register")
    @EncryptResponseBody
    public ResponseEntity<Map<String, Object>> register(@DecryptRequestBody UserDTO user) {
        // 业务逻辑处理
        System.out.println("收到用户注册请求:" + user);
        return ResponseEntity.ok(Map.of(
            "msg", "注册成功",
            "username", user.getUsername(),
            "userId", System.currentTimeMillis()
        ));
    }
}

5 前端完整对接代码

1、获取公钥

javascript 复制代码
const response = await fetch('/api/key/public');
const { publicKey } = await response.json();

2、加密请求

javascript 复制代码
// 生成 AES 密钥
const aesKey = window.crypto.getRandomValues(new Uint8Array(32));

// 导入 RSA 公钥
const publicKeyObj = await window.crypto.subtle.importKey(
  'spki',
  base64ToArrayBuffer(publicKey),
  { name: 'RSA-OAEP', hash: 'SHA-256' },
  false,
  ['encrypt']
);

// RSA 加密 AES 密钥
const encryptedKey = await window.crypto.subtle.encrypt(
  { name: 'RSA-OAEP' },
  publicKeyObj,
  aesKey
);

// 生成时间戳和 Nonce
const timestamp = Date.now();
const nonce = crypto.randomUUID();
const aad = `${timestamp}:${nonce}`;

// AES-GCM 加密业务数据
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const aesKeyObj = await window.crypto.subtle.importKey(
  'raw',
  aesKey,
  { name: 'AES-GCM' },
  false,
  ['encrypt']
);

const encryptedData = await window.crypto.subtle.encrypt(
  {
    name: 'AES-GCM',
    iv: iv,
    additionalData: new TextEncoder().encode(aad),
    tagLength: 128
  },
  aesKeyObj,
  new TextEncoder().encode(JSON.stringify(userData))
);

// 发送请求
const response = await fetch('/api/user/register', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    encryptedKey: arrayBufferToBase64(encryptedKey),
    encryptedData: arrayBufferToBase64(
      concatenate(iv, new Uint8Array(encryptedData))
    ),
    timestamp: timestamp,
    nonce: nonce
  })
});

6 生产级注意事项与踩坑指南

6.1 核心安全边界

  1. 本方案仅解决数据传输的加密、防篡改、防重放,不能替代身份认证体系,需与JWT、OAuth2等身份认证方案结合使用。
  2. HTTPS是基础,本方案是HTTPS之上的加固,绝对不能因为用了接口加密就关闭HTTPS
  3. 不适合大文件传输、日志采集等高并发大流量场景,加解密会有性能开销。

6.2 密钥管理优化

  • 生产环境禁止启动时生成密钥对,需将密钥对配置在配置中心(Nacos/Apollo),支持热更新与平滑轮换,建议3个月更换一次密钥。
  • 高安全场景建议使用KMS密钥管理服务,不要将密钥硬编码在代码或配置文件中。

6.3 常见问题排查

  1. AES解密失败:AEADBadTagException
    1. 最常见原因:前后端AAD不一致,检查timestamp+nonce的拼接顺序、分隔符是否完全一致
    2. 其他原因:IV长度不是12字节、密钥不匹配、数据被篡改
  2. RSA解密失败:Data must not be longer than 245 bytes
    1. 原因:RSA仅用于加密AES密钥,禁止用RSA加密业务数据;2048位RSA最多加密245字节数据
    2. 其他原因:前后端填充方式不一致,必须统一RSA-OAEP
  3. 防重放误判
    1. 原因:客户端重试未更换nonce、前后端时间差超过5分钟、Redis配置错误导致nonce未正确写入

6.4 性能评估

操作 平均耗时
RSA-OAEP 2048位解密 ~0.8ms
AES-256-GCM加解密(1KB以内数据) <0.1ms
全链路加解密(含Redis校验) ~1-2ms
4核8G服务器单接口QPS 3000+

常规业务系统(QPS 1000-5000)完全可以无压力使用,性能开销完全可控。

7 总结

如果你的系统只是普通的Web管理系统,HTTPS已经足够满足安全需求;但如果你面对的是高敏感数据传输、App/小程序开发、需要防逆向、防Hook、防网关日志泄露的场景,接口级加解密不是过度设计,而是必要的安全防线。本文提供的方案,从算法选型、工程化设计、前后端兼容、生产踩坑指南等多个维度,实现了一套可直接落地生产的全链路加解密方案,无需修改业务代码,只需添加注解即可实现接口的自动加解密。

更多容易可以看仓库文档:快速开始

我的仓库地址:gitee.com/liyongde/ja...


转发请携带作者信息 @怒放吧德德 @一个有梦有戏的人

持续创作很不容易,作者将以尽可能的详细把所学知识分享各位开发者,一起进步一起学习。转载请携带链接,转载到微信公众号请勿选择原创,谢谢!

👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍

谢谢支持!

相关推荐
陈随易4 小时前
真的,你可以不用TypeScript
前端·后端·程序员
郑州光合科技余经理5 小时前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
大大水瓶5 小时前
Tomcat
java·tomcat
dustcell.5 小时前
haproxy七层代理
java·开发语言·前端
游离态指针6 小时前
以为发消息=下单成功?RabbitMQ从0到秒杀实战的完整踩坑笔记
java
李慕婉学姐6 小时前
Springboot智慧社区系统设计与开发6n99s526(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
掘金酱6 小时前
「寻找年味」 沸点活动|获奖名单公示🎊
前端·人工智能·后端
颜酱6 小时前
栈的经典应用:从基础到进阶,解决LeetCode高频栈类问题
javascript·后端·算法
BD_Marathon6 小时前
工厂方法模式
android·java·工厂方法模式