Loading

AES/SM4 对称加密算法原理以及上手

一、对称加密本质(先打底层认知)

同一把密钥,既用于加密,也用于解密

数学表达可以写成:
C=E(K,P),P=D(K,C)

  • P:明文
  • C:密文
  • K:密钥
  • E:加密函数
  • D:解密函数
    👉 本质不是“加密”,而是可逆的变换函数

ES 每一轮包含 4 个步骤:

  1. SubBytes(字节替换)
  2. ShiftRows(行移位)
  3. MixColumns(列混淆)
  4. 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
}
  1. 严格按接口文档传参
    fileId、bizId、stream、hash 字段名必须和文档一致,不能自己乱改。
  2. 异常处理必须完整
  • 文件不存在判断
  • SDK 返回空 / 失败判断
  • 流关闭(try-with-resources)
  • 日志打印(机试非常看重)
  1. 解密方法是重点
    一般会给:AES/SM4 解密密钥 + 加密方式,直接套工具类即可。
  2. Hash 存证 / 查询
    就是简单的 SDK 调用,参数校验 + 返回值判断写好就能拿满分。
  3. 代码规范
    注释清晰、方法名和题目完全一致、变量见名知意。

测试

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>
posted @ 2026-04-16 23:03  A零号  阅读(36)  评论(0)    收藏  举报