Spring Boot实现接口签名案例

wen java案例 2

本文目录导读:

Spring Boot实现接口签名案例

  1. 项目结构
  2. 完整实现代码
  3. POM依赖
  4. 签名流程说明

我来为你提供一个完整的Spring Boot接口签名实现案例。

项目结构

src/main/java/com/example/signature/
├── config/
│   └── SignConfig.java
├── interceptor/
│   └── SignInterceptor.java
├── annotation/
│   └── SignVerify.java
├── controller/
│   └── UserController.java
└── utils/
    └── SignUtil.java

完整实现代码

1 签名工具类

package com.example.signature.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
public class SignUtil {
    private static final Logger logger = LoggerFactory.getLogger(SignUtil.class);
    /**
     * 生成签名
     * @param params 请求参数(不包含sign)
     * @param secret 密钥
     * @return 签名
     */
    public static String generateSign(Map<String, String> params, String secret) {
        // 移除sign和空值参数
        Map<String, String> sortedParams = new TreeMap<>();
        if (params != null) {
            for (Map.Entry<String, String> entry : params.entrySet()) {
                if (!"sign".equals(entry.getKey()) && StringUtils.hasText(entry.getValue())) {
                    sortedParams.put(entry.getKey(), entry.getValue());
                }
            }
        }
        // 拼接参数
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
            sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
        }
        // 拼接密钥
        String stringToSign = sb.append("key=").append(secret).toString();
        logger.debug("待签名字符串: {}", stringToSign);
        return md5(stringToSign);
    }
    /**
     * MD5加密
     */
    public static String md5(String data) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] digest = md.digest(data.getBytes(StandardCharsets.UTF_8));
            return bytesToHex(digest);
        } catch (Exception e) {
            logger.error("MD5加密失败", e);
            return null;
        }
    }
    /**
     * HMAC-SHA256签名
     */
    public static String hmacSha256(String data, String secret) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            mac.init(secretKeySpec);
            byte[] digest = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(digest);
        } catch (Exception e) {
            logger.error("HMAC-SHA256签名失败", e);
            return null;
        }
    }
    /**
     * 字节数组转十六进制
     */
    private static String bytesToHex(byte[] bytes) {
        StringBuilder hexString = new StringBuilder();
        for (byte b : bytes) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) {
                hexString.append('0');
            }
            hexString.append(hex);
        }
        return hexString.toString().toUpperCase();
    }
    /**
     * 生成随机签名
     */
    public static String generateRandomSign(String appId, String timestamp, String nonce, String secret) {
        Map<String, String> params = new HashMap<>();
        params.put("appId", appId);
        params.put("timestamp", timestamp);
        params.put("nonce", nonce);
        return generateSign(params, secret);
    }
    /**
     * 验证非空
     */
    public static boolean checkNonNull(Map<String, String> params) {
        if (params == null || params.isEmpty()) {
            return false;
        }
        for (String value : params.values()) {
            if (!StringUtils.hasText(value)) {
                return false;
            }
        }
        return true;
    }
}

2 自定义注解

package com.example.signature.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 签名验证注解
 */
@Target({ElementType.METHOD, ElementType.TYPE}) // 方法或类上使用
@Retention(RetentionPolicy.RUNTIME)
public @interface SignVerify {
    /**
     * 是否需要验证
     */
    boolean required() default true;
    /**
     * 超时时间(毫秒)
     */
    long timeout() default 300000; // 默认5分钟
}

3 拦截器

package com.example.signature.interceptor;
import com.alibaba.fastjson.JSON;
import com.example.signature.annotation.SignVerify;
import com.example.signature.utils.SignUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Component
public class SignInterceptor implements HandlerInterceptor {
    private static final Logger logger = LoggerFactory.getLogger(SignInterceptor.class);
    // 密钥管理(实际项目中应该从配置中心或数据库中获取)
    private static final Map<String, String> CLIENT_SECRETS = new HashMap<>();
    static {
        // 演示用的客户端密钥
        CLIENT_SECRETS.put("test_app", "test_secret_key_123456");
    }
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 如果不是控制器方法,直接放行
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        // 获取注解
        SignVerify methodSign = handlerMethod.getMethodAnnotation(SignVerify.class);
        SignVerify classSign = handlerMethod.getBeanType().getAnnotation(SignVerify.class);
        // 如果没有注解,直接放行
        if (methodSign == null && classSign == null) {
            return true;
        }
        // 判断是否需要验证
        SignVerify signVerify = methodSign != null ? methodSign : classSign;
        if (!signVerify.required()) {
            return true;
        }
        // 获取请求参数
        Map<String, String> params = getParams(request);
        // 验证参数
        if (!verifyParams(params)) {
            return responseError(response, "请求参数不完整");
        }
        // 验证时间戳
        String timestamp = params.get("timestamp");
        if (!verifyTimestamp(timestamp, signVerify.timeout())) {
            return responseError(response, "请求已过期");
        }
        // 获取签名
        String clientSign = params.get("sign");
        String appId = params.get("appId");
        // 获取客户端密钥
        String secret = CLIENT_SECRETS.get(appId);
        if (secret == null) {
            return responseError(response, "无效的appId");
        }
        // 验证签名
        String serverSign = SignUtil.generateSign(params, secret);
        logger.debug("客户端签名: {}, 服务端签名: {}", clientSign, serverSign);
        if (!clientSign.toUpperCase().equals(serverSign)) {
            return responseError(response, "签名验证失败");
        }
        return true;
    }
    /**
     * 获取请求参数
     */
    private Map<String, String> getParams(HttpServletRequest request) throws IOException {
        Map<String, String> params = new HashMap<>();
        // 获取GET参数
        Map<String, String[]> parameterMap = request.getParameterMap();
        for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
            params.put(entry.getKey(), entry.getValue()[0]);
        }
        // 获取POST body参数
        if ("POST".equalsIgnoreCase(request.getMethod())) {
            BufferedReader reader = request.getReader();
            if (reader != null) {
                try {
                    String line;
                    StringBuilder body = new StringBuilder();
                    while ((line = reader.readLine()) != null) {
                        body.append(line);
                    }
                    if (body.length() > 0) {
                        // 解析body参数(这里简化处理,只处理JSON格式)
                        String bodyStr = body.toString();
                        if (bodyStr.startsWith("{")) {
                            com.alibaba.fastjson.JSONObject jsonObject = JSON.parseObject(bodyStr);
                            for (String key : jsonObject.keySet()) {
                                if (!params.containsKey(key)) {
                                    params.put(key, jsonObject.getString(key));
                                }
                            }
                        }
                    }
                } finally {
                    reader.close();
                }
            }
        }
        return params;
    }
    /**
     * 验证参数完整性
     */
    private boolean verifyParams(Map<String, String> params) {
        return params.containsKey("appId") 
                && params.containsKey("timestamp") 
                && params.containsKey("nonce") 
                && params.containsKey("sign");
    }
    /**
     * 验证时间戳
     */
    private boolean verifyTimestamp(String timestamp, long timeout) {
        try {
            long clientTime = Long.parseLong(timestamp);
            long currentTime = System.currentTimeMillis();
            return Math.abs(currentTime - clientTime) <= timeout;
        } catch (NumberFormatException e) {
            return false;
        }
    }
    /**
     * 返回错误信息
     */
    private boolean responseError(HttpServletResponse response, String message) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(401);
        PrintWriter writer = response.getWriter();
        writer.write("{\"code\":401, \"message\":\"" + message + "\"}");
        writer.flush();
        writer.close();
        return false;
    }
}

4 配置类

package com.example.signature.config;
import com.example.signature.interceptor.SignInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class SignConfig implements WebMvcConfigurer {
    @Autowired
    private SignInterceptor signInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(signInterceptor)
                .addPathPatterns("/api/**")  // 添加需要拦截的路径
                .excludePathPatterns("/api/public/**");  // 排除不需要拦截的路径
    }
}

5 控制器示例

package com.example.signature.controller;
import com.example.signature.annotation.SignVerify;
import com.example.signature.utils.SignUtil;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api")
@SignVerify  // 类级别注解,所有接口都需要验证
public class UserController {
    /**
     * 需要签名验证的接口
     */
    @PostMapping("/user/info")
    public Map<String, Object> getUserInfo(@RequestBody Map<String, String> params) {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "success");
        result.put("data", "用户信息:" + params.get("userId"));
        return result;
    }
    /**
     * 不需要签名验证的接口(覆盖类级注解)
     */
    @GetMapping("/public/config")
    @SignVerify(required = false)
    public Map<String, Object> getPublicConfig() {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "success");
        result.put("data", "公共配置");
        return result;
    }
    /**
     * 获取测试签名(用于演示)
     */
    @GetMapping("/getTestSign")
    public Map<String, String> getTestSign(
            @RequestParam String appId,
            @RequestParam String timestamp,
            @RequestParam String nonce) {
        String secret = "test_secret_key_123456";
        Map<String, String> testParams = new HashMap<>();
        testParams.put("appId", appId);
        testParams.put("timestamp", timestamp);
        testParams.put("nonce", nonce);
        String sign = SignUtil.generateSign(testParams, secret);
        Map<String, String> result = new HashMap<>();
        result.put("sign", sign);
        result.put("appId", appId);
        result.put("timestamp", timestamp);
        result.put("nonce", nonce);
        return result;
    }
}

6 客户端调用示例

package com.example.signature.client;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.*;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class SignClient {
    // 客户端信息
    private static final String APP_ID = "test_app";
    private static final String SECRET = "test_secret_key_123456";
    private static final String BASE_URL = "http://localhost:8080/api";
    public static void main(String[] args) {
        // 构造请求参数
        Map<String, String> params = new HashMap<>();
        params.put("appId", APP_ID);
        params.put("timestamp", String.valueOf(System.currentTimeMillis()));
        params.put("nonce", UUID.randomUUID().toString().replace("-", ""));
        params.put("userId", "123456");
        // 生成签名
        String sign = com.example.signature.utils.SignUtil.generateSign(params, SECRET);
        params.put("sign", sign);
        // 发送请求
        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<Map<String, String>> entity = new HttpEntity<>(params, headers);
        String url = BASE_URL + "/user/info";
        ResponseEntity<String> response = restTemplate.exchange(
                url, HttpMethod.POST, entity, String.class);
        System.out.println("请求URL: " + url);
        System.out.println("请求参数: " + params);
        System.out.println("响应结果: " + response.getBody());
    }
}

POM依赖

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Fastjson -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>2.0.25</version>
    </dependency>
    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

签名流程说明

1 签名步骤

  1. 客户端准备参数:appId、timestamp、nonce、业务参数
  2. 生成待签名字符串:将参数按字母排序,用&拼接
  3. 生成签名:使用MD5或HMAC-SHA256算法
  4. 服务端验证:获取参数、验证时间戳、重新生成签名并对比

2 安全特性

  • 时间戳验证:防止重放攻击
  • 随机数nonce:保证每次请求唯一性
  • 签名验证:防止参数篡改
  • appId隔离:支持多客户端

3 扩展建议

  • 支持更多签名算法(RSA、AES等)
  • 添加nonce缓存,防止重复请求
  • 集成Redis,实现令牌管理
  • 支持接口限流
  • 添加统一的错误码体系

这样,一个完整的Spring Boot接口签名案例就实现了,你可以根据实际需求进行调整和扩展。

抱歉,评论功能暂时关闭!