一、对称加密本质(先打底层认知)
同一把密钥,既用于加密,也用于解密
数学表达可以写成:
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 {// ====================== 机试提供的 SDK 客户端(按实际 SDK 类名替换)======================@Resourceprivate FileShareSdkClient fileShareSdkClient;// ====================== 1. 上传文件接口 4.2.1 ======================/*** 上传文件到分布式文件共享服务* @param filePath 本地文件路径* @param bizId 业务ID* @return 文件唯一标识 fileId* @throws Exception 上传异常*/@Overridepublic 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 下载异常*/@Overridepublic 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 解密异常*/@Overridepublic 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 存证异常*/@Overridepublic 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 查询异常*/@Overridepublic 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();}
}
配套通用解密工具类
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(不会真的调用远程服务)@Mockprivate FileShareSdkClient fileShareSdkClient;// 注入我们要测试的实现类@InjectMocksprivate 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位密钥@BeforeEachvoid setUp() {// 测试前初始化(可选)}// ==================== 1. 测试文件上传 ====================@Testvoid 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. 测试文件下载 ====================@Testvoid 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. 测试文件解密 ====================@Testvoid decryptFileData_success() {// 构造一个测试用加密文件(实际运行会自动生成)File encryptedFile = new File("test_encrypted.tmp");// 执行解密File decryptedFile = Assertions.assertDoesNotThrow(() ->fileShareService.decryptFileData(encryptedFile, TEST_DECRYPT_KEY));Assertions.assertNotNull(decryptedFile);}// ==================== 4. 测试 Hash 存证 ====================@Testvoid uploadHash_success() throws Exception {// 1. MockHashUploadResponse 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 查询 ====================@Testvoid queryHash_success() throws Exception {// 1. MockHashInfo 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>
