SpringBoot中拦截器的运用(验证某些请求的时间戳和RSA签名)

本文介绍了在SpringBoot中实现拦截器的方法。需继承WebMvcConfigurerAdapter类,实现相应抽象方法,将自定义拦截器注入bean容器并配置拦截请求。创建拦截器类实现HandlerInterceptor接口,进行RSA加解密验签和时间戳有效性校验。实现拦截器需完成创建拦截器类和重写addInterceptors方法两步。

   SpringBoot中实现拦截器,需要继承WebMvcConfigurerAdapter类,这个类中实现了webMvcConfig接口的抽象类,其次我们可以实现该类中相应的抽象方法。

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(paramsValidInterceptor())
                .addPathPatterns("/terminalCall/syncMachine")
                .addPathPatterns("/terminalCall/channelGoodsList");
    }
    /**
     * 注册对终端调用接口进行签名调用的验证拦截器
     * @return
     */
    @Bean
    public ParamsValidInterceptor paramsValidInterceptor(){
        return new ParamsValidInterceptor();
    }

  上述这段代码做了两件事情,第一,将我们自定义拦截器给注入到bean容器中进行管理,第二,将需要拦截的请求放入拦截器中,这样,我们只需要去实现其中的具体业务逻辑即可。

  • 创建自己的拦截器类并实现 HandlerInterceptor 接口
package com.youfuli.vendor.interceptor;


import com.youfuli.vendor.constant.Messages;
import com.youfuli.vendor.utils.RSA2;

import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.ResourceUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.apache.commons.codec.binary.Base64;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;

import java.util.*;

/**
 * Created by 李啸天 on 2019/3/13.
 */
@Slf4j
@Component
public class ParamsValidInterceptor extends HandlerInterceptorAdapter {

    @Value("${rsa.pubKey}")
    String pubKey;

    @Value("${rsa.priKey}")
    String priKey;

    @Value("${rsa.pwd}")
    String pwd;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String times = (System.currentTimeMillis()/1000)+"";
        log.info("当前时间戳为{}",times);
        log.info("传入验证签名方法的路径为{}",request.getRequestURL());
        Map<String,String[]> maps = request.getParameterMap();
        //时间戳是否失效
            Assert.isTrue(checkTimeStamp(new Long(maps.get("timeStamp")[0])), Messages.TIMESPAN_INVALID);
        //签名数据
        String signStr = maps.get("sign")[0];
        log.info("传入签名数据为{}",signStr);
        //参与签名数据
        String returnString = key_sort(maps);
        log.info("传入参与签名数据为{}",returnString);
        //拿取公钥
        File pubfile = ResourceUtils.getFile(pubKey);
        String publicKey = txt2String(pubfile).replaceAll("(\\\r\\\n|\\\r|\\\n|\\\n\\\r)", "");

        //拿取私钥
        File prifile = ResourceUtils.getFile(priKey);
        String privateKey = txt2String(prifile).replaceAll("(\\\r\\\n|\\\r|\\\n|\\\n\\\r)", "");

        //对现有数据进行签名---本地签名校验
        byte [] signValidStr = RSA2.sign(returnString.getBytes(),privateKey);
        String signValid = Base64.encodeBase64String(signValidStr);
        boolean verfiy;
        if(signValid.equals(signStr)){
            verfiy = RSA2.verify(returnString.getBytes(),signValidStr,publicKey);
            log.info("当前验签结果为{}",verfiy);
        }else{
            verfiy=false;
        }
        if(verfiy==false){
            Assert.isTrue(verfiy,Messages.VALID_SIGN_ERROR);
        }
        return verfiy;
    }

    /**
    *排序
    **/
    public static String key_sort(Map<String, String[]> map) {
        String key_sort = "";

        TreeMap<String, String[]> map2 = new TreeMap<String, String[]>(new Comparator<String>() {
            public int compare(String obj1, String obj2) {
                // 降序排序
                return obj2.compareTo(obj1);
            }
        });
        map2 = new TreeMap<>(map);
        map2.remove("sign");

        Set<String> keySet = map2.keySet();
        Iterator<String> iter = keySet.iterator();
        while (iter.hasNext()) {
            String key = iter.next();
            key_sort = key_sort + key + "=" + map2.get(key)[0] + "&";
        }
        return key_sort.substring(0, key_sort.length() - 1);
    }

    /**
    *判断时间戳是否在一分钟内
    **/
    private static boolean checkTimeStamp(long ts)
    {
        if (Math.abs(ts - System.currentTimeMillis() / 1000) > 60)
        {
            return false;
        }
        return true;
    }

    /**
    *读取文本文件中的数据并输出为String
    **/
    public static String txt2String(File file){
        StringBuilder result = new StringBuilder();
        try{
            BufferedReader br = new BufferedReader(new FileReader(file));//构造一个BufferedReader类来读取文件
            String s = null;
            while((s = br.readLine())!=null){//使用readLine方法,一次读一行
                result.append(System.lineSeparator()+s);
            }
            br.close();
        }catch(Exception e){
            e.printStackTrace();
        }
        return result.toString();
    }

}

     因为把很多公共的代码给抽象出来,其中对传进来的参数进行排序后利用自身的RSA公私钥进行加解密验签操作,并且对时间戳进行了一分钟的有效性校验,校验成功则返回true继续执行下面的代码,否则返回false执行断言。

    简而言之,要实现拦截器功能需要完成2个步骤:

  • 创建自己的拦截器类并实现 HandlerInterceptor 接口
  • 重写WebMvcConfigurerAdapter类中的addInterceptors方法把自定义的拦截器类添加进来即可

   至于采用RSA加解密的规则,可以参考下面一段代码(另外一篇关于RSA加解密的博客大家也可以看看RSA证书加解密

package com.youfuli.vendor.utils;

import org.apache.commons.codec.binary.Base64;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by 李啸天 on 2019/3/13.
 */
public class RSA2 {
    public static final String KEY_ALGORITHM = "RSA";
    private static final String PUBLIC_KEY = "RSAPublicKey";
    private static final String PRIVATE_KEY = "RSAPrivateKey";
    public static final String SIGNATURE_ALGORITHM = "MD5withRSA";
    /**
     * RSA最大加密明文大小
     */
    private static final int MAX_ENCRYPT_BLOCK = 117;

    /**
     * RSA最大解密密文大小
     */
    private static final int MAX_DECRYPT_BLOCK = 2048;

    //获得公钥字符串
    public static String getPublicKeyStr(Map<String, Object> keyMap) throws Exception {
        //获得map中的公钥对象 转为key对象
        Key key = (Key) keyMap.get(PUBLIC_KEY);
        //编码返回字符串
        return encryptBASE64(key.getEncoded());
    }


    //获得私钥字符串
    public static String getPrivateKeyStr(Map<String, Object> keyMap) throws Exception {
        //获得map中的私钥对象 转为key对象
        Key key = (Key) keyMap.get(PRIVATE_KEY);
        //编码返回字符串
        return encryptBASE64(key.getEncoded());
    }

    //获取公钥
    public static PublicKey getPublicKey(String key) throws Exception {
        byte[] keyBytes;
        keyBytes = (new BASE64Decoder()).decodeBuffer(key);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        PublicKey publicKey = keyFactory.generatePublic(keySpec);
        return publicKey;
    }

    //获取私钥
    public static PrivateKey getPrivateKey(String key) throws Exception {
        byte[] keyBytes;
        keyBytes = (new BASE64Decoder()).decodeBuffer(key);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
        return privateKey;
    }

    //解码返回byte
    public static byte[] decryptBASE64(String key) throws Exception {
        return (new BASE64Decoder()).decodeBuffer(key);
    }


    //编码返回字符串
    public static String encryptBASE64(byte[] key) throws Exception {
        return (new BASE64Encoder()).encodeBuffer(key);
    }

    //***************************签名和验证*******************************
    public static byte[] sign(byte[] data, String privateKeyStr) throws Exception {
        PrivateKey priK = getPrivateKey(privateKeyStr);
        Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
        sig.initSign(priK);
        sig.update(data);
        return sig.sign();
    }

    public static boolean verify(byte[] data, byte[] sign, String publicKeyStr) throws Exception {
        PublicKey pubK = getPublicKey(publicKeyStr);
        Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
        sig.initVerify(pubK);
        sig.update(data);
        return sig.verify(sign);
    }

    //************************加密解密**************************
    public static byte[] encrypt(byte[] plainText, String publicKeyStr) throws Exception {
        PublicKey publicKey = getPublicKey(publicKeyStr);
        Cipher cipher = Cipher.getInstance(KEY_ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        int inputLen = plainText.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offSet = 0;
        int i = 0;
        byte[] cache;
        while (inputLen - offSet > 0) {
            if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
                cache = cipher.doFinal(plainText, offSet, MAX_ENCRYPT_BLOCK);
            } else {
                cache = cipher.doFinal(plainText, offSet, inputLen - offSet);
            }
            out.write(cache, 0, cache.length);
            i++;
            offSet = i * MAX_ENCRYPT_BLOCK;
        }
        byte[] encryptText = out.toByteArray();
        out.close();
        return encryptText;
    }

    public static byte[] decrypt(byte[] encryptText, String privateKeyStr) throws Exception {
        PrivateKey privateKey = getPrivateKey(privateKeyStr);
        Cipher cipher = Cipher.getInstance(KEY_ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        int inputLen = encryptText.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offSet = 0;
        byte[] cache;
        int i = 0;
        // 对数据分段解密
        while (inputLen - offSet > 0) {
            if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
                cache = cipher.doFinal(encryptText, offSet, MAX_DECRYPT_BLOCK);
            } else {
                cache = cipher.doFinal(encryptText, offSet, inputLen - offSet);
            }
            out.write(cache, 0, cache.length);
            i++;
            offSet = i * MAX_DECRYPT_BLOCK;
        }
        byte[] plainText = out.toByteArray();
        out.close();
        return plainText;
    }


    public static void main(String[] args) {
        Map<String, Object> keyMap;
        byte[] cipherText;
        String input = "Hello World!";
        try {
            keyMap = initKey();
            String publicKey = getPublicKeyStr(keyMap);
            System.out.println("公钥------------------");
            System.out.println(publicKey);
            String privateKey = getPrivateKeyStr(keyMap);
            System.out.println("私钥------------------");
            System.out.println(privateKey);

            System.out.println("测试可行性-------------------");
            System.out.println("明文=======" + input);

            cipherText = encrypt(input.getBytes(), publicKey);
            //加密后的东西
            System.out.println("密文=======" + new String(cipherText));
            //开始解密
            byte[] plainText = decrypt(cipherText, privateKey);
            System.out.println("解密后明文===== " + new String(plainText));
            System.out.println("验证签名-----------");

            String str = "431f4dfd4c862122fb8a4e004c9a093b";
            System.out.println("\n原文:" + str);
            byte[] signature = sign(str.getBytes(), privateKey);
            System.out.println(Base64.encodeBase64String(signature));
            boolean status = verify(str.getBytes(), signature, publicKey);
            System.out.println("验证情况:" + status);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static Map<String, Object> initKey() throws Exception {
        KeyPairGenerator keyPairGen = KeyPairGenerator
                .getInstance(KEY_ALGORITHM);
        keyPairGen.initialize(1024);
        KeyPair keyPair = keyPairGen.generateKeyPair();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        Map<String, Object> keyMap = new HashMap<String, Object>(2);
        keyMap.put(PUBLIC_KEY, publicKey);
        keyMap.put(PRIVATE_KEY, privateKey);
        return keyMap;
    }

}

此次写篇博客记录一下,既是积累也是分享。

<think>我们正在讨论文件上传请求签名验证机制设计。文件上传由于其特殊性(包含二进制数据),在签名验证时需要特别注意。 关键点: 1. 签名验证的目的:确保请求未被篡改、验证请求来源、防止重放攻击。 2. 文件上传请求通常包含二进制文件数据,这部分数据不能直接用于签名(因为可能很大,且二进制数据直接参与签名计算复杂且低效)。 3. 因此,通常的做法是:对除文件内容以外的其他参数进行签名,而文件内容则通过其他方式保证完整性(例如在签名中包含文件的哈希值)。 设计步骤: 步骤1:确定签名参数 通常包括: - HTTP方法(如POST) - 请求路径(如/upload) - 时间戳(防止重放) - 随机数(防止重放) - 其他业务参数(如用户ID、文件名等) - 文件的哈希值(如SHA256,用于验证文件完整性) 步骤2:构建待签名字符串 将选定的参数按照一定顺序(如字典序)拼接,形成待签名字符串。例如: method=POST&path=/upload&timestamp=1234567890&nonce=abcdef&file_hash=xxxxx 步骤3:生成签名 使用密钥(对称密钥或私钥)对字符串进行签名(如HMAC-SHA256或RSA签名)。 步骤4:客户端发送请求签名以及时间戳、随机数等放在HTTP头中(如X-Signature, X-Timestamp, X-Nonce),文件作为multipart/form-data的一部分上传。 步骤5:服务端验证 1. 检查时间戳随机数是否有效(防止重放)。 2. 根据同样的规则构建待签名字符串(注意:服务端需要计算上传文件的哈希值,并与请求中的file_hash比对,确保文件未被篡改)。 3. 使用相同的密钥算法验证签名。 注意:文件哈希值可以由客户端计算并作为参数之一,但需要确保客户端计算的哈希值正确,因此服务端必须重新计算并比对。 具体实现(以Spring Boot为例): 1. 客户端实现(以JavaScript为例): ```javascript async function uploadFile(file) { // 计算文件哈希 const fileBuffer = await file.arrayBuffer(); const hashBuffer = await crypto.subtle.digest('SHA-256', fileBuffer); const fileHash = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join(''); // 构建其他参数 const timestamp = Date.now(); const nonce = Math.random().toString(36).substring(2, 10); const method = 'POST'; const path = '/upload'; // 构建待签名字符串 const signParams = { method, path, timestamp, nonce, file_hash: fileHash }; // 按参数名排序 const sortedKeys = Object.keys(signParams).sort(); const signString = sortedKeys.map(key => `${key}=${signParams[key]}`).join('&'); // 使用密钥生成签名(假设密钥为secretKey) const encoder = new TextEncoder(); const keyData = encoder.encode('secretKey'); const cryptoKey = await crypto.subtle.importKey('raw', keyData, {name: 'HMAC', hash: 'SHA-256'}, false, ['sign']); const signature = await crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(signString)); // 将签名转为十六进制字符串 const signatureHex = Array.from(new Uint8Array(signature)).map(b => b.toString(16).padStart(2, '0')).join(''); // 发送请求 const formData = new FormData(); formData.append('file', file); formData.append('file_hash', fileHash); // 也可以不放在formData里,而是放在签名参数中,但服务端需要从两个地方获取(建议统一放在请求头中) const response = await fetch('/upload', { method: 'POST', headers: { 'X-Timestamp': timestamp, 'X-Nonce': nonce, 'X-Signature': signatureHex }, body: formData }); return response.json(); } ``` 2. 服务端实现(Spring Boot): 我们可以创建一个过滤器(Filter)来处理签名验证。 ```java public class FileUploadSignatureFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 只处理文件上传请求 if ("/upload".equals(request.getRequestURI()) && "POST".equalsIgnoreCase(request.getMethod())) { // 获取请求头中的签名时间戳、随机数 String signature = request.getHeader("X-Signature"); String timestamp = request.getHeader("X-Timestamp"); String nonce = request.getHeader("X-Nonce"); // 1. 验证时间戳(比如在5分钟内有效)随机数(检查是否使用过) if (!validateTimestampAndNonce(timestamp, nonce)) { response.sendError(HttpStatus.BAD_REQUEST.value(), "Invalid timestamp or nonce"); return; } // 2. 获取文件并计算哈希(注意:这里需要先解析multipart请求,但过滤器里直接解析multipart可能比较麻烦,因此可以考虑在拦截器或Controller中做,或者使用前置的CommonsMultipartResolver) // 另一种做法:将文件哈希由客户端计算并放在请求头中(X-File-Hash),这样在过滤器中可以直接获取,但需要确保客户端不会篡改文件而保持哈希不变(所以服务端还是要重新计算?) // 这里我们假设客户端将文件哈希放在请求头X-File-Hash中 String clientFileHash = request.getHeader("X-File-Hash"); if (clientFileHash == null) { response.sendError(HttpStatus.BAD_REQUEST.value(), "Missing file hash"); return; } // 3. 构建待签名字符串 Map<String, String> signParams = new TreeMap<>(); // TreeMap默认按键排序 signParams.put("method", request.getMethod()); signParams.put("path", request.getRequestURI()); signParams.put("timestamp", timestamp); signParams.put("nonce", nonce); signParams.put("file_hash", clientFileHash); StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> entry : signParams.entrySet()) { if (sb.length() > 0) { sb.append("&"); } sb.append(entry.getKey()).append("=").append(entry.getValue()); } String signString = sb.toString(); // 4. 使用密钥(这里从配置中读取)生成签名 String serverSignature = HmacUtils.hmacSha256Hex("secretKey", signString); // 5. 比较签名 if (!serverSignature.equals(signature)) { response.sendError(HttpStatus.UNAUTHORIZED.value(), "Invalid signature"); return; } // 6. 注意:服务端在后续处理中还需要重新计算上传文件的哈希,并与clientFileHash比较,以确保文件未被篡改(这一步可以在Controller里做) } // 验证通过,继续执行 filterChain.doFilter(request, response); } private boolean validateTimestampAndNonce(String timestamp, String nonce) { // 检查时间戳是否在有效期内(例如5分钟) long current = System.currentTimeMillis(); long time = Long.parseLong(timestamp); if (current - time > 5 * 60 * 1000) { return false; } // 检查随机数是否使用过(可以用缓存记录在有效期内的随机数,比如5分钟内的随机数,防止重放) // 这里简化,实际需要维护一个缓存(如ConcurrentHashMap或Redis) return true; } } ``` 注意:在过滤器中获取文件内容来计算哈希值是不现实的,因为文件内容在请求体中,过滤器里读取后,后续就无法读取了。因此,我们采用客户端计算文件哈希并放在请求头中的方式。但这样就需要在后续处理中(比如Controller)再对文件内容计算一次哈希,并与请求头中的哈希值比对,以确保文件在传输过程中没有被篡改。 步骤6:在Controller中验证文件哈希 ```java @PostMapping("/upload") public ResponseEntity<?> uploadFile(@RequestParam("file") MultipartFile file, @RequestHeader("X-File-Hash") String clientFileHash) throws Exception { // 计算文件的SHA-256哈希 byte[] fileBytes = file.getBytes(); String serverFileHash = DigestUtils.sha256Hex(fileBytes); if (!serverFileHash.equals(clientFileHash)) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("File hash mismatch"); } // 处理文件上传... return ResponseEntity.ok("Upload success"); } ``` 安全增强建议: - 使用HTTPS:防止传输过程中被窃听或篡改。 - 密钥管理:使用安全的密钥存储(如Vault)定期轮换。 - 重放攻击防护:时间戳随机数(nonce)机制需要严格实现,随机数在有效期内不能重复使用。 总结:文件上传的签名验证机制主要通过对元数据(包括文件哈希)进行签名,而文件本身的完整性则通过哈希值来保证。服务端需要分两步验证签名验证文件哈希验证。 相关问题: 1. 如何安全地存储管理签名密钥? 2. 文件上传签名验证中,如何避免大文件哈希计算对服务器性能的影响? 3. 如何设计一个防重放攻击的机制? 4. 在微服务架构中,如何统一处理文件上传的签名验证? 5. 除了签名验证,还有哪些方法可以增强文件上传的安全性?</think>### 文件上传请求签名验证机制设计与实现 在文件上传场景中设计签名验证机制需要解决两个核心问题:**如何对二进制文件进行签名****如何平衡安全性与性能**。以下是分步设计方案: --- #### 一、签名机制设计原则 1. **文件内容参与签名**:确保文件内容未被篡改(如恶意代码注入) 2. **元数据完整性保护**:防止文件名、类型等关键参数被修改 3. **防重放攻击**:通过时间戳+随机数机制 4. **密钥安全管理**:使用动态密钥而非硬编码[^1] --- #### 二、签名生成流程(客户端) ```mermaid sequenceDiagram participant Client as 客户端 participant Server as 服务端 Client->>Client: 1. 计算文件哈希值(SHA256) Client->>Client: 2. 组装元数据:<br/>文件名、类型、大小、时间戳、随机数 Client->>Client: 3. 生成待签名字符串:<br/>method=POST&path=/upload&file_hash=xxx... Client->>Client: 4. 使用HMAC-SHA256生成签名 Client->>Server: 5. 发送请求:<br/>- 文件二进制流<br/>- 元数据(HTTP头)<br/>- X-Signature签名头 ``` --- #### 三、关键实现细节 ##### 1. 签名参数设计 | 参数 | 示例值 | 作用说明 | |---------------|---------------------|----------------------------| | X-File-Hash | 7d87...a2c3 (SHA256)| 文件内容完整性验证[^1] | | X-Timestamp | 1718100000 | 防重放(±5分钟有效) | | X-Nonce | 6f4b8c3a | 防重放(单次有效) | | X-File-Name | contract.pdf | 原始文件名验证 | | X-File-Type | application/pdf | 文件类型验证[^1] | ##### 2. 签名生成算法 ```javascript // 客户端示例 (JavaScript) async function generateSignature(file, secretKey) { // 1. 计算文件哈希 const fileHash = await crypto.subtle.digest('SHA-256', file); // 2. 组装元数据 const params = { method: 'POST', path: '/upload', timestamp: Date.now(), nonce: generateRandomString(8), file_hash: hex(fileHash), file_name: file.name, file_type: file.type, file_size: file.size }; // 3. 生成待签名字符串 const signString = Object.keys(params) .sort() .map(key => `${key}=${params[key]}`) .join('&'); // 4. HMAC签名 const encoder = new TextEncoder(); const key = await crypto.subtle.importKey( 'raw', encoder.encode(secretKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ); const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(signString)); return { headers: { 'X-Timestamp': params.timestamp, 'X-Nonce': params.nonce, 'X-File-Hash': params.file_hash, 'X-Signature': hex(signature) }, file }; } ``` ##### 3. 服务端验证实现 (Spring Boot) ```java @RestController public class FileUploadController { @PostMapping("/upload") public ResponseEntity<?> uploadFile( @RequestParam("file") MultipartFile file, @RequestHeader("X-Signature") String signature, @RequestHeader("X-Timestamp") long timestamp, @RequestHeader("X-Nonce") String nonce) { // 1. 验证时间窗口 (±5分钟) if (Math.abs(System.currentTimeMillis() - timestamp) > 300_000) { return ResponseEntity.status(400).body("签名过期"); } // 2. 检查随机数是否使用过 if (nonceCache.contains(nonce)) { return ResponseEntity.status(400).body("重复请求"); } nonceCache.add(nonce, 5, TimeUnit.MINUTES); // 3. 计算文件哈希 String fileHash = DigestUtils.sha256Hex(file.getBytes()); // 4. 重建签名字符串 Map<String, String> params = new TreeMap<>(); params.put("method", "POST"); params.put("path", "/upload"); params.put("timestamp", String.valueOf(timestamp)); params.put("nonce", nonce); params.put("file_hash", fileHash); params.put("file_name", file.getOriginalFilename()); params.put("file_type", file.getContentType()); params.put("file_size", String.valueOf(file.getSize())); String signString = params.entrySet().stream() .map(e -> e.getKey() + "=" + e.getValue()) .collect(Collectors.joining("&")); // 5. 验证签名 String serverSignature = HmacUtils.hmacSha256Hex(secretKey, signString); if (!serverSignature.equals(signature)) { return ResponseEntity.status(401).body("签名无效"); } // 6. 安全处理文件(类型检查、病毒扫描等)[^1] if (!isAllowedType(file.getContentType())) { return ResponseEntity.status(403).body("文件类型不允许"); } // 7. 存储文件(隔离存储、权限控制)[^1] storageService.save(file); return ResponseEntity.ok("上传成功"); } } ``` --- #### 四、安全增强措施 1. **动态密钥管理**: ```java // 从安全存储获取密钥 String secretKey = keyManagementService.getKey(userId); ``` 2. **文件内容二次验证**: ```java // 验证客户端服务端计算的哈希一致性 if (!fileHash.equals(request.getHeader("X-File-Hash"))) { return ResponseEntity.status(400).body("文件内容被篡改"); } ``` 3. **大文件优化方案**: - 分块签名:对文件分块计算哈希,再汇总签名 - 流式处理:使用`InputStream`逐块计算哈希,避免内存溢出 4. **密钥轮换机制**: ```mermaid graph LR A[密钥版本v1] -->|使用中| B[上传请求] C[密钥版本v2] -->|准备| D[密钥存储] B --> E{检测到密钥过期} E -->|是| F[返回密钥版本号] F --> G[客户端用新密钥重试] ``` --- #### 五、与Spring Security的整合 在`SecurityConfig`中添加前置过滤器: ```java @Override protected void configure(HttpSecurity http) { http .addFilterBefore(new FileUploadSignatureFilter(), UsernamePasswordAuthenticationFilter.class) .authorizeRequests() .antMatchers("/upload").hasRole("USER"); } // 签名验证过滤器 public class FileUploadSignatureFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { if ("/upload".equals(request.getRequestURI())) { // 执行签名验证逻辑 if (!signatureValid) { response.setStatus(401); return; } } chain.doFilter(request, response); } } ``` --- ### 总结建议 1. **必选要素**: - 文件内容哈希参与签名 - 时间戳+随机数防重放 - 密钥动态管理 2. **性能优化**: - 大文件使用流式哈希计算 - 签名验证前置到网关层 - 缓存已验证的随机数 3. **组合方案**: ```mermaid pie title 文件上传安全层级 “签名验证” : 45 “类型检查” : 20 “病毒扫描” : 20 “访问控制” : 15 ``` > 最终实现应满足:$$ \text{安全强度} \propto \frac{\text{关键参数完整性} \times \text{密钥强度}}{\text{时间窗口}} $$[^1] --- ### 相关问题 1. 如何实现大文件的分块签名验证? 2. 在微服务架构中如何统一管理签名密钥? 3. 文件上传签名验证与JWT认证如何协同工作? 4. 如何防止签名验证过程中的时序攻击? 5. 对于浏览器端文件上传,如何安全存储签名密钥?
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值