AES/SM4 对称加密算法原理以及上手
一、对称加密本质(先打底层认知)
同一把密钥,既用于加密,也用于解密
数学表达可以写成:
C=E(K,P),P=D(K,C)
- P:明文
- C:密文
- K:密钥
- E:加密函数
- D:解密函数
👉 本质不是“加密”,而是可逆的变换函数
ES 每一轮包含 4 个步骤:
- SubBytes(字节替换)
- ShiftRows(行移位)
- MixColumns(列混淆)
- AddRoundKey(轮密钥加)
上手示例
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
/**
* 文件共享服务实现类
* 机试任务:实现 5 个核心接口方法
* 1. uploadFile - 上传文件
* 2. downloadFile - 下载文件
* 3. decryptFileData - 解密下载的文件
* 4. uploadHash - Hash存证
* 5. queryHash - 查询Hash
*/
@Slf4j
@Component
public class FileShareServiceImpl implements FileShareService {
@Resource
private FileShareSdkClient fileShareSdkClient;
// ====================== 1. 上传文件接口 4.2.1 ======================
/**
* 上传文件到分布式文件共享服务
* @param filePath 本地文件路径
* @param bizId 业务ID
* @return 文件唯一标识 fileId
* @throws Exception 上传异常
*/
@Override
public String uploadFile(String filePath, String bizId) throws Exception {
log.info("开始上传文件,本地路径:{},业务ID:{}", filePath, bizId);
File file = new File(filePath);
if (!file.exists()) {
throw new IllegalArgumentException("文件不存在:" + filePath);
}
// 构造 SDK 上传请求(按接口文档字段替换)
FileUploadRequest request = new FileUploadRequest();
request.setBizId(bizId);
request.setFileName(file.getName());
request.setFileSize(file.length());
request.setInputStream(new java.io.FileInputStream(file));
// 调用 SDK 上传
FileUploadResponse response = fileShareSdkClient.uploadFile(request);
// 校验返回结果
if (response == null || !response.isSuccess()) {
log.error("文件上传失败,响应:{}", response);
throw new RuntimeException("文件上传失败:" + (response != null ? response.getMsg() : "SDK返回空"));
}
String fileId = response.getFileId();
log.info("文件上传成功,fileId:{}", fileId);
return fileId;
}
// ====================== 2. 下载文件接口 4.2.2 ======================
/**
* 从分布式文件服务下载文件
* @param fileId 文件唯一标识
* @param savePath 本地保存路径
* @return 下载后的文件对象
* @throws Exception 下载异常
*/
@Override
public File downloadFile(String fileId, String savePath) throws Exception {
log.info("开始下载文件,fileId:{},保存路径:{}", fileId, savePath);
// 构造下载请求
FileDownloadRequest request = new FileDownloadRequest();
request.setFileId(fileId);
// 调用 SDK 下载(返回加密的文件流 + 元数据)
FileDownloadResponse response = fileShareSdkClient.downloadFile(request);
if (response == null || !response.isSuccess()) {
log.error("文件下载失败,响应:{}", response);
throw new RuntimeException("文件下载失败");
}
// 获取加密文件流
InputStream encryptedStream = response.getFileStream();
if (encryptedStream == null) {
throw new RuntimeException("下载文件流为空");
}
// 写入本地临时加密文件
File encryptedFile = new File(savePath + ".tmp");
try (FileOutputStream fos = new FileOutputStream(encryptedFile)) {
byte[] buffer = new byte[4096];
int len;
while ((len = encryptedStream.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
} finally {
encryptedStream.close();
}
log.info("文件下载完成(加密),临时文件:{}", encryptedFile.getAbsolutePath());
return encryptedFile;
}
// ====================== 3. 解密下载的文件 ======================
/**
* 解密下载的加密文件数据
* @param encryptedFile 加密文件
* @param decryptKey 解密密钥(从SDK/接口文档获取)
* @return 解密后的真实文件
* @throws Exception 解密异常
*/
@Override
public File decryptFileData(File encryptedFile, String decryptKey) throws Exception {
log.info("开始解密文件,加密文件路径:{}", encryptedFile.getAbsolutePath());
if (!encryptedFile.exists()) {
throw new IllegalArgumentException("加密文件不存在");
}
// 真实解密逻辑(按机试提供的加密算法实现:AES/SM4 等)
// 这里写通用标准解密模板,机试按文档替换算法即可
File decryptedFile = new File(encryptedFile.getAbsolutePath().replace(".tmp", ""));
// 调用 SDK/工具类解密
boolean decryptSuccess = CryptoUtil.decryptFile(encryptedFile, decryptedFile, decryptKey);
if (!decryptSuccess) {
throw new RuntimeException("文件解密失败");
}
// 删除临时加密文件
encryptedFile.delete();
log.info("文件解密成功,解密后文件:{}", decryptedFile.getAbsolutePath());
return decryptedFile;
}
// ====================== 4. Hash 存证接口 4.2.3 ======================
/**
* 文件Hash存证(防篡改、可溯源)
* @param fileId 文件ID
* @param fileHash 文件哈希值(SHA256/SM3)
* @param bizData 业务扩展数据
* @return 存证ID
* @throws Exception 存证异常
*/
@Override
public String uploadHash(String fileId, String fileHash, Map<String, String> bizData) throws Exception {
log.info("开始Hash存证,fileId:{},hash:{}", fileId, fileHash);
// 构造存证请求
HashUploadRequest request = new HashUploadRequest();
request.setFileId(fileId);
request.setFileHash(fileHash);
request.setBizData(bizData);
// 调用 SDK 存证
HashUploadResponse response = fileShareSdkClient.uploadHash(request);
if (response == null || !response.isSuccess()) {
log.error("Hash存证失败");
throw new RuntimeException("Hash存证失败");
}
String hashId = response.getHashId();
log.info("Hash存证成功,hashId:{}", hashId);
return hashId;
}
// ====================== 5. 查询 Hash 接口 4.2.4 ======================
/**
* 查询文件Hash存证信息
* @param fileId 文件ID
* @return Hash详情 + 存证时间 + 状态
* @throws Exception 查询异常
*/
@Override
public HashInfo queryHash(String fileId) throws Exception {
log.info("查询Hash存证,fileId:{}", fileId);
HashQueryRequest request = new HashQueryRequest();
request.setFileId(fileId);
HashQueryResponse response = fileShareSdkClient.queryHash(request);
if (response == null || !response.isSuccess()) {
log.error("Hash查询失败");
throw new RuntimeException("Hash查询失败");
}
log.info("Hash查询成功,结果:{}", response.getHashInfo());
return response.getHashInfo();
}
}
Controller
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.Map;
@RestController
@RequestMapping("/file/share")
@RequiredArgsConstructor
public class FileShareController {
private final FileShareService fileShareService;
// ==================== 1. 文件上传 ====================
@PostMapping("/upload")
public Result<String> uploadFile(
@RequestParam("file") MultipartFile file,
@RequestParam("bizId") String bizId) throws Exception {
// 转存为临时文件给 Service 使用
File tempFile = File.createTempFile("upload_", ".tmp");
file.transferTo(tempFile);
String fileId = fileShareService.uploadFile(tempFile.getAbsolutePath(), bizId);
tempFile.delete(); // 用完删除
return Result.success(fileId);
}
// ==================== 2. 文件下载 ====================
@GetMapping("/download")
public void downloadFile(
@RequestParam String fileId,
@RequestParam(required = false, defaultValue = "file") String fileName,
HttpServletResponse response) throws Exception {
// 1. 下载加密文件
File encryptedFile = fileShareService.downloadFile(fileId, "temp_" + System.currentTimeMillis());
// 2. 解密(密钥机试一般固定写死或从配置取)
File decryptedFile = fileShareService.decryptFileData(encryptedFile, "1234567890123456");
// 3. 写出到浏览器
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition",
"attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
try (OutputStream out = response.getOutputStream();
java.io.FileInputStream in = new java.io.FileInputStream(decryptedFile)) {
byte[] buf = new byte[4096];
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
}
decryptedFile.delete();
}
// ==================== 3. 解密接口(单独调用) ====================
@PostMapping("/decrypt")
public Result<File> decryptFile(
@RequestParam String encryptedFilePath,
@RequestParam String key) throws Exception {
File encryptedFile = new File(encryptedFilePath);
File decryptedFile = fileShareService.decryptFileData(encryptedFile, key);
return Result.success(decryptedFile);
}
// ==================== 4. Hash 存证 ====================
@PostMapping("/hash/upload")
public Result<String> uploadHash(
@RequestParam String fileId,
@RequestParam String fileHash,
@RequestBody(required = false) Map<String, String> bizData) throws Exception {
String hashId = fileShareService.uploadHash(fileId, fileHash, bizData);
return Result.success(hashId);
}
// ==================== 5. 查询 Hash ====================
@GetMapping("/hash/query")
public Result<HashInfo> queryHash(@RequestParam String fileId) throws Exception {
HashInfo hashInfo = fileShareService.queryHash(fileId);
return Result.success(hashInfo);
}
}
通用返回结果类
import lombok.Data;
@Data
public class Result<T> {
private int code;
private String msg;
private T data;
public static <T> Result<T> success(T data) {
Result<T> r = new Result<>();
r.setCode(200);
r.setMsg("success");
r.setData(data);
return r;
}
public static <T> Result<T> fail(String msg) {
Result<T> r = new Result<>();
r.setCode(500);
r.setMsg(msg);
return r;
}
}
配套通用解密工具类
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
/**
* 加解密工具类(按机试提供算法替换)
*/
public class CryptoUtil {
// 算法:AES / SM4(国密)
private static final String ALGORITHM = "AES/ECB/PKCS5Padding";
/**
* 文件解密
*/
public static boolean decryptFile(File src, File dest, String key) {
try (FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dest)) {
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES");
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] buffer = new byte[4096];
int len;
while ((len = in.read(buffer)) != -1) {
byte[] decrypt = cipher.update(buffer, 0, len);
out.write(decrypt);
}
byte[] finalBytes = cipher.doFinal();
if (finalBytes != null && finalBytes.length > 0) {
out.write(finalBytes);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
CBC 模式
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.security.SecureRandom;
/**
* CBC 模式 加解密工具类(机试推荐)
*/
public class CryptoUtil {
// 算法改为 CBC 模式(必须带 IV)
private static final String ALGORITHM = "AES/CBC/PKCS5Padding";
// CBC 固定 16 位 IV(机试一般会给固定 IV,也可以随机生成)
private static final String IV = "1234567890123456";
/**
* CBC 模式文件解密
*/
public static boolean decryptFile(File src, File dest, String key) {
try (FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dest)) {
// 密钥
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES");
// CBC 必须加 IV 参数
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes());
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);
byte[] buffer = new byte[4096];
int len;
while ((len = in.read(buffer)) != -1) {
byte[] decrypt = cipher.update(buffer, 0, len);
out.write(decrypt);
}
byte[] finalBytes = cipher.doFinal();
if (finalBytes != null && finalBytes.length > 0) {
out.write(finalBytes);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 可选:CBC 加密(机试备用)
*/
public static boolean encryptFile(File src, File dest, String key) {
try (FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dest)) {
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES");
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes());
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
byte[] buffer = new byte[4096];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(cipher.update(buffer, 0, len));
}
out.write(cipher.doFinal());
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
接口定义
import java.io.File;
import java.util.Map;
public interface FileShareService {
String uploadFile(String filePath, String bizId) throws Exception;
File downloadFile(String fileId, String savePath) throws Exception;
File decryptFileData(File encryptedFile, String decryptKey) throws Exception;
String uploadHash(String fileId, String fileHash, Map<String, String> bizData) throws Exception;
HashInfo queryHash(String fileId) throws Exception;
}
// Hash 信息实体
class HashInfo {
private String fileId;
private String fileHash;
private String createTime;
private String status;
// getter/setter
}
- 严格按接口文档传参
fileId、bizId、stream、hash 字段名必须和文档一致,不能自己乱改。 - 异常处理必须完整
- 文件不存在判断
- SDK 返回空 / 失败判断
- 流关闭(try-with-resources)
- 日志打印(机试非常看重)
- 解密方法是重点
一般会给:AES/SM4 解密密钥 + 加密方式,直接套工具类即可。 - Hash 存证 / 查询
就是简单的 SDK 调用,参数校验 + 返回值判断写好就能拿满分。 - 代码规范
注释清晰、方法名和题目完全一致、变量见名知意。
测试
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.File;
import java.util.HashMap;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
/**
* 文件共享服务 单元测试
* 覆盖:上传、下载、解密、Hash存证、Hash查询
*/
@ExtendWith(MockitoExtension.class)
public class FileShareServiceImplTest {
// Mock 掉第三方 SDK(不会真的调用远程服务)
@Mock
private FileShareSdkClient fileShareSdkClient;
// 注入我们要测试的实现类
@InjectMocks
private FileShareServiceImpl fileShareService;
// 测试常量
private final String TEST_FILE_PATH = "test.txt";
private final String TEST_BIZ_ID = "biz_123";
private final String TEST_FILE_ID = "file_456";
private final String TEST_HASH = "sha256_abcdef123456";
private final String TEST_DECRYPT_KEY = "1234567890123456"; // AES 16位密钥
@BeforeEach
void setUp() {
// 测试前初始化(可选)
}
// ==================== 1. 测试文件上传 ====================
@Test
void uploadFile_success() throws Exception {
// 1. 构造 Mock 返回
FileUploadResponse mockResponse = new FileUploadResponse();
mockResponse.setSuccess(true);
mockResponse.setFileId(TEST_FILE_ID);
when(fileShareSdkClient.uploadFile(any(FileUploadRequest.class))).thenReturn(mockResponse);
// 2. 执行测试
String fileId = fileShareService.uploadFile(TEST_FILE_PATH, TEST_BIZ_ID);
// 3. 断言
Assertions.assertEquals(TEST_FILE_ID, fileId);
verify(fileShareSdkClient, times(1)).uploadFile(any());
}
// ==================== 2. 测试文件下载 ====================
@Test
void downloadFile_success() throws Exception {
// 1. Mock SDK 返回
FileDownloadResponse mockResponse = new FileDownloadResponse();
mockResponse.setSuccess(true);
mockResponse.setFileStream(ClassLoader.getSystemResourceAsStream(TEST_FILE_PATH));
when(fileShareSdkClient.downloadFile(any())).thenReturn(mockResponse);
// 2. 执行下载
File downloadedFile = fileShareService.downloadFile(TEST_FILE_ID, "test_download.tmp");
// 3. 断言
Assertions.assertNotNull(downloadedFile);
Assertions.assertTrue(downloadedFile.exists());
}
// ==================== 3. 测试文件解密 ====================
@Test
void decryptFileData_success() {
// 构造一个测试用加密文件(实际运行会自动生成)
File encryptedFile = new File("test_encrypted.tmp");
// 执行解密
File decryptedFile = Assertions.assertDoesNotThrow(() ->
fileShareService.decryptFileData(encryptedFile, TEST_DECRYPT_KEY)
);
Assertions.assertNotNull(decryptedFile);
}
// ==================== 4. 测试 Hash 存证 ====================
@Test
void uploadHash_success() throws Exception {
// 1. Mock
HashUploadResponse mockResponse = new HashUploadResponse();
mockResponse.setSuccess(true);
mockResponse.setHashId("hash_789");
when(fileShareSdkClient.uploadHash(any())).thenReturn(mockResponse);
// 2. 执行
String hashId = fileShareService.uploadHash(TEST_FILE_ID, TEST_HASH, new HashMap<>());
// 3. 断言
Assertions.assertEquals("hash_789", hashId);
}
// ==================== 5. 测试 Hash 查询 ====================
@Test
void queryHash_success() throws Exception {
// 1. Mock
HashInfo mockHashInfo = new HashInfo();
mockHashInfo.setFileId(TEST_FILE_ID);
mockHashInfo.setFileHash(TEST_HASH);
HashQueryResponse mockResponse = new HashQueryResponse();
mockResponse.setSuccess(true);
mockResponse.setHashInfo(mockHashInfo);
when(fileShareSdkClient.queryHash(any())).thenReturn(mockResponse);
// 2. 执行
HashInfo hashInfo = fileShareService.queryHash(TEST_FILE_ID);
// 3. 断言
Assertions.assertEquals(TEST_FILE_ID, hashInfo.getFileId());
Assertions.assertEquals(TEST_HASH, hashInfo.getFileHash());
}
}
测试依赖
<dependencies>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<!-- Mockito -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>

浙公网安备 33010602011771号