Spring Boot接口签名校验设计与实现

发布于:2025-08-17 ⋅ 阅读:(18) ⋅ 点赞:(0)

在现代Web应用开发中,API安全是一个不可忽视的重要环节。本文将详细介绍如何在Spring Boot应用中实现接口签名校验机制,确保API调用的安全性。

一、接口签名校验概述

接口签名校验是一种常见的API安全防护手段,其核心思想是:客户端和服务器端通过约定的算法对请求参数进行加密处理,生成签名,服务器端收到请求后验证签名是否合法,从而判断请求是否被篡改或伪造。

签名校验的主要作用:

  1. 防篡改:确保请求参数在传输过程中未被修改
  2. 防重放:通过时间戳防止请求被重复使用
  3. 身份验证:验证调用方的合法性

二、签名校验流程设计

在我们的实现中,签名校验流程如下:

  1. 客户端准备请求参数
  2. 客户端按照约定规则生成签名
  3. 客户端将签名和必要参数放入HTTP头
  4. 服务端拦截器校验签名有效性
  5. 服务端根据校验结果决定是否继续处理请求

三、核心代码实现

1. 拦截器配置

首先,我们需要配置一个拦截器来拦截需要进行签名校验的请求:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Autowired
    private AppValidInterceptor appValidInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(appValidInterceptor)
                .addPathPatterns("/test123"); // 指定需要拦截的路径
    }
}

2. 签名校验拦截器

这是签名校验的核心实现:

@Slf4j
@Component
public class AppValidInterceptor implements HandlerInterceptor {
    
    @Resource
    private MessageSource messageSource;
    
    @Resource
    private EncryptionUtils encryptionUtils;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        // 从请求头获取签名相关参数
        String sign = request.getHeader(AppConstant.REQUEST_HEADER_SIGN);
        String appVersion = request.getHeader(AppConstant.REQUEST_HEADER_VERSION);
        String timestampsStr = request.getHeader(AppConstant.REQUEST_HEADER_TIMESTAMPS);
        long timestamps = Long.parseLong(timestampsStr);
        String uri = request.getRequestURI().replace("//", "/");

        log.info("请求URI:{}", uri);

        // 检查是否需要签名校验
        if(uri.contains("test123")) {
            // 基本参数校验
            if(StrUtil.isBlank(appVersion)){
                log.error("请求被拒绝:{}", uri);
                throw new ServiceException(getMessage("default_req_reject_message"));
            }
            
            // 版本号处理
            int version = Integer.parseInt(appVersion.replace(".", ""));
            
            // 针对低版本的特殊处理
            if(version <= 450){
                // 时间戳校验(15秒内有效)
                if(Math.abs(System.currentTimeMillis() / 1000 - timestamps) > 15) {
                    log.error("签名错误,时间戳大于15S,系统时间戳:{},客户端时间戳:{}",
                            System.currentTimeMillis()/1000, timestamps);
                    throw new ServiceException(getMessage("default_req_reject_message"));
                }
                
                // 签名校验
                String expectedContent = appVersion + "." + uri + "/" + timestamps;
                if(!Objects.equals(encryptionUtils.decrypt(sign), expectedContent)) {
                    log.error("签名错误,sign解析失败,解析值:{}", encryptionUtils.decrypt(sign));
                    throw new ServiceException(getMessage("default_req_reject_message"));
                }
            }
        }
        return true;
    }
    
    private String getMessage(String code) {
        return messageSource.getMessage(code, null, LocaleContextHolder.getLocale());
    }
}

3. 加密工具类

我们使用RSA非对称加密算法进行签名验证:

@Component
public class EncryptionUtils {
    @Value("classpath:keytool/public_key.pem")
    private Resource publicKeyResource;

    @Value("classpath:keytool/private_key.pem")
    private Resource privateKeyResource;

    private PublicKey publicKey;
    private PrivateKey privateKey;

    @PostConstruct
    public void init() throws Exception {
        publicKey = loadPublicKey();
        privateKey = loadPrivateKey();
    }

    // 加载公钥
    private PublicKey loadPublicKey() throws Exception {
        InputStream inputStream = publicKeyResource.getInputStream();
        String publicKeyStr = IoUtil.read(inputStream, StandardCharsets.UTF_8);
        byte[] publicBytes = Base64.getDecoder().decode(publicKeyStr);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes);
        return keyFactory.generatePublic(keySpec);
    }

    // 加载私钥
    private PrivateKey loadPrivateKey() throws Exception {
        InputStream inputStream = privateKeyResource.getInputStream();
        String privateKeyStr = IoUtil.read(inputStream, StandardCharsets.UTF_8);
        byte[] privateBytes = Base64.getDecoder().decode(privateKeyStr);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateBytes);
        return keyFactory.generatePrivate(keySpec);
    }

    // RSA加密
    public String encrypt(String plaintext) {
        try {
            Cipher cipher = Cipher.getInstance("RSA");
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            byte[] encryptedBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(encryptedBytes);
        } catch (Exception e) {
            log.error("加密失败", e);
            return null;
        }
    }

    // RSA解密
    public String decrypt(String ciphertext) {
        try {
            Cipher cipher = Cipher.getInstance("RSA");
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            byte[] encryptedBytes = Base64.getDecoder().decode(ciphertext);
            byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
            return new String(decryptedBytes, StandardCharsets.UTF_8);
        } catch (Exception e) {
            log.error("解密失败", e);
            return null;
        }
    }
}

4. 常量定义

public interface AppConstant {
    // 请求头字段名
    String REQUEST_HEADER_SIGN = "sign";
    String REQUEST_HEADER_VERSION = "appVersion";
    String REQUEST_HEADER_TIMESTAMPS = "timestamps";

    // 不需要校验的URI列表
    List<String> ignoreUri = Arrays.asList(
        "/platform/time",
        "/device/solarFlow/today/energy",
        "/device/smartPlug/energy"
    );

    // 设备相关不需要校验的API
    List<String> deviceIgnoreApi = Collections.singletonList(
        "/productModule/device/mobile/bind"
    );
}

四、签名生成与校验流程详解

客户端签名生成步骤:

  1. 准备参数
    1. 获取当前时间戳(秒级):long timestamp = System.currentTimeMillis() / 1000
    2. 获取应用版本号:如"1.0.0"
    3. 获取请求URI:如"/api/test"
  1. 拼接签名字符串
String signContent = appVersion + "." + uri + "/" + timestamp;
  1. 使用私钥加密生成签名
String sign = encryptWithPrivateKey(signContent);
  1. 设置HTTP头
    1. sign: 生成的签名
    2. appVersion: 应用版本号
    3. timestamps: 时间戳

服务端校验流程:

  1. 拦截请求:通过拦截器拦截指定路径的请求
  2. 获取请求头:提取sign、appVersion和timestamps
  3. 基本校验
    1. 检查必要参数是否存在
    2. 检查时间戳是否在有效期内(防止重放攻击)
  1. 签名验证
    1. 使用私钥解密sign得到原始字符串
    2. 按照相同规则拼接预期字符串
    3. 比较解密结果与预期字符串是否一致
  1. 结果处理
    1. 验证通过:放行请求
    2. 验证失败:返回错误信息

五、安全性增强建议

  1. HTTPS传输:确保所有API请求都通过HTTPS传输,防止中间人攻击
  2. 动态密钥:可以考虑定期更换密钥,增强安全性
  3. 请求限流:对频繁失败的请求进行限流,防止暴力破解
  4. 签名算法升级:支持多种签名算法,便于后期升级
  5. 详细日志:记录详细的校验日志,便于问题排查和安全审计

六、拓展实现

1. 支持多种签名算法

我们可以扩展加密工具类,支持多种算法:

public enum SignAlgorithm {
    RSA("RSA"),
    AES("AES"),
    HMAC_SHA256("HmacSHA256");

    private String algorithmName;

    SignAlgorithm(String algorithmName) {
        this.algorithmName = algorithmName;
    }

    public String getAlgorithmName() {
        return algorithmName;
    }
}

// 在EncryptionUtils中添加方法
public String sign(String content, SignAlgorithm algorithm, String secret) {
    switch (algorithm) {
        case RSA:
            return rsaSign(content);
        case HMAC_SHA256:
            return hmacSha256(content, secret);
        // 其他算法...
        default:
            throw new IllegalArgumentException("不支持的签名算法");
    }
}

2. 更灵活的白名单配置

改进常量类,支持从配置文件读取白名单:

@Configuration
@ConfigurationProperties(prefix = "api.security")
public class ApiSecurityProperties {
    private List<String> ignoreUris = new ArrayList<>();
    private List<String> deviceIgnoreApis = new ArrayList<>();
    
    // getters and setters
}

然后在拦截器中注入使用:

@Resource
private ApiSecurityProperties apiSecurityProperties;

// 使用方式
if (apiSecurityProperties.getIgnoreUris().contains(uri)) {
    return true;
}

七、总结

本文详细介绍了Spring Boot中接口签名校验的实现方案,包括:

  1. 拦截器配置与实现
  2. RSA非对称加密的应用
  3. 签名生成与验证流程
  4. 安全性增强建议
  5. 可扩展性改进方案

通过合理的签名校验机制,可以有效提升API接口的安全性,防止常见的安全威胁。在实际项目中,可以根据具体需求调整签名算法、有效期等参数,在安全性和用户体验之间取得平衡。

完整的示例代码已在上文中给出,读者可以直接参考实现或根据实际需求进行修改。希望本文对您的API安全实践有所帮助!


网站公告

今日签到

点亮在社区的每一天
去签到