Implementing Open API Signature Verification in Spring Boot
This article explains the concept of open APIs and signature verification, outlines the end‑to‑end signing and verification flow, and provides complete Spring Boot code—including configuration properties, a signature manager, custom annotation, AOP aspect, request‑caching filter, and utility class—to securely validate third‑party requests without exposing credentials.
1. Concept
Open API
Open APIs are interfaces that can be called by third‑party systems without authentication credentials, but they usually require a signature to prevent malicious use. The system providing the open API is referred to as the "original system".
Signature Verification
Before invoking an open API, the third‑party system must generate a signature (a string) based on all request parameters according to the original system's rules. The original system validates the signature; only requests with a valid signature are accepted.
2. Signature Verification Call Flow
1. Agree on Signature Algorithm
The caller and the original system negotiate a signature algorithm (e.g., SHA256withRSA ) and a unique identifier (callerID) to distinguish each caller.
2. Issue Asymmetric Key Pair
After agreeing on the algorithm, the original system generates an RSA key pair for each caller. The private key is given to the caller; the public key stays with the original system.
Note: The caller must keep the private key securely on its backend. If the private key leaks, the caller loses trust.
3. Generate Request Parameter Signature
The caller uses the agreed algorithm and private key to sign the request parameters. The original system typically provides a JAR or code snippet to ensure the signing process matches its verification logic.
4. Send Request with Signature
The caller includes the agreed callerID in the path parameters and the generated signature in the request header.
3. Code Design
1. Signature Configuration Class
import cn.hutool.crypto.asymmetric.SignAlgorithm;
import lombok.Data;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* Signature related configuration
*/
@Data
@ConditionalOnProperty(value = "secure.signature.enable", havingValue = "true")
@Component
@ConfigurationProperties("secure.signature")
public class SignatureProps {
private Boolean enable;
private Map
keyPair;
@Data
public static class KeyPairProps {
private SignAlgorithm algorithm;
private String publicKeyPath;
private String publicKey;
private String privateKeyPath;
private String privateKey;
}
}2. Signature Manager
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.Sign;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import top.ysqorz.signature.model.SignatureProps;
import java.nio.charset.StandardCharsets;
@Component
@ConditionalOnBean(SignatureProps.class)
public class SignatureManager {
private final SignatureProps signatureProps;
public SignatureManager(SignatureProps signatureProps) {
this.signatureProps = signatureProps;
loadKeyPairByPath();
}
/** Verify signature */
public boolean verifySignature(String callerID, String rawData, String signature) {
Sign sign = getSignByCallerID(callerID);
if (ObjectUtils.isEmpty(sign)) {
return false;
}
return sign.verify(rawData.getBytes(StandardCharsets.UTF_8), HexUtil.decodeHex(signature));
}
/** Generate signature */
public String sign(String callerID, String rawData) {
Sign sign = getSignByCallerID(callerID);
if (ObjectUtils.isEmpty(sign)) {
return null;
}
return sign.signHex(rawData);
}
private Sign getSignByCallerID(String callerID) {
SignatureProps.KeyPairProps keyPairProps = signatureProps.getKeyPair().get(callerID);
if (ObjectUtils.isEmpty(keyPairProps)) {
return null;
}
return SecureUtil.sign(keyPairProps.getAlgorithm(), keyPairProps.getPrivateKey(), keyPairProps.getPublicKey());
}
private void loadKeyPairByPath() {
signatureProps.getKeyPair().forEach((key, kp) -> {
kp.setPublicKey(loadKeyByPath(kp.getPublicKeyPath()));
kp.setPrivateKey(loadKeyByPath(kp.getPrivateKeyPath()));
if (ObjectUtils.isEmpty(kp.getPublicKey()) || ObjectUtils.isEmpty(kp.getPrivateKey())) {
throw new RuntimeException("No public and private key files configured");
}
});
}
private String loadKeyByPath(String path) {
if (ObjectUtils.isEmpty(path)) {
return null;
}
return IoUtil.readUtf8(ResourceUtil.getStream(path));
}
}3. Custom Verification Annotation
import java.lang.annotation.*;
/** Annotation to mark controller methods that require signature verification */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface VerifySignature { }4. AOP Implementation of Verification Logic
import cn.hutool.crypto.CryptoException;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.util.ContentCachingRequestWrapper;
import top.ysqorz.common.constant.BaseConstant;
import top.ysqorz.config.SpringContextHolder;
import top.ysqorz.exception.auth.AuthorizationException;
import top.ysqorz.exception.param.ParamInvalidException;
import top.ysqorz.signature.model.SignStatusCode;
import top.ysqorz.signature.model.SignatureProps;
import top.ysqorz.signature.util.CommonUtils;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.util.Map;
@Aspect
@Component
@Slf4j
@ConditionalOnBean(SignatureProps.class)
public class RequestSignatureAspect implements PointCutDef {
@Resource
private SignatureManager signatureManager;
@Pointcut("@annotation(top.ysqorz.signature.enumeration.VerifySignature)")
public void annotatedMethod() {}
@Pointcut("@within(top.ysqorz.signature.enumeration.VerifySignature)")
public void annotatedClass() {}
@Before("apiMethod() && (annotatedMethod() || annotatedClass())")
public void verifySignature() {
HttpServletRequest request = SpringContextHolder.getRequest();
String callerID = request.getParameter(BaseConstant.PARAM_CALLER_ID);
if (ObjectUtils.isEmpty(callerID)) {
throw new AuthorizationException(SignStatusCode.UNTRUSTED_CALLER);
}
String signature = request.getHeader(BaseConstant.X_REQUEST_SIGNATURE);
if (ObjectUtils.isEmpty(signature)) {
throw new ParamInvalidException(SignStatusCode.REQUEST_SIGNATURE_INVALID);
}
String requestParamsStr = extractRequestParams(request);
verifySignature(callerID, requestParamsStr, signature);
}
public String extractRequestParams(HttpServletRequest request) {
String body = null;
if (request instanceof ContentCachingRequestWrapper) {
ContentCachingRequestWrapper wrapper = (ContentCachingRequestWrapper) request;
body = new String(wrapper.getContentAsByteArray(), StandardCharsets.UTF_8);
}
Map
paramMap = request.getParameterMap();
ServletWebRequest webRequest = new ServletWebRequest(request, null);
Map
uriTemplateVarMap = (Map
) webRequest.getAttribute(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
return CommonUtils.extractRequestParams(body, paramMap, uriTemplateVarMap);
}
public void verifySignature(String callerID, String requestParamsStr, String signature) {
try {
boolean verified = signatureManager.verifySignature(callerID, requestParamsStr, signature);
if (!verified) {
throw new CryptoException("The signature verification result is false.");
}
} catch (Exception ex) {
log.error("Failed to verify signature", ex);
throw new AuthorizationException(SignStatusCode.REQUEST_SIGNATURE_INVALID);
}
}
}5. Solving Single‑Read Request Body Issue
Spring Boot provides ContentCachingRequestWrapper to cache the request body so it can be read multiple times. A custom filter wraps incoming requests with this wrapper when needed.
import lombok.NonNull;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import top.ysqorz.signature.model.SignatureProps;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@ConditionalOnBean(SignatureProps.class)
public class RequestCachingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain)
throws ServletException, IOException {
boolean isFirst = !isAsyncDispatch(request);
HttpServletRequest wrapper = request;
if (isFirst && !(request instanceof ContentCachingRequestWrapper)) {
wrapper = new ContentCachingRequestWrapper(request);
}
filterChain.doFilter(wrapper, response);
}
}6. Registering the Filter
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.ysqorz.signature.model.SignatureProps;
@Configuration
public class FilterConfig {
@Bean
@ConditionalOnBean(SignatureProps.class)
public FilterRegistrationBean
requestCachingFilterRegistration(RequestCachingFilter filter) {
FilterRegistrationBean
bean = new FilterRegistrationBean<>(filter);
bean.setOrder(1);
return bean;
}
}7. Utility Class for Parameter Extraction
import cn.hutool.core.util.StrUtil;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
public class CommonUtils {
/** Concatenate request body, query parameters and path variables into a single string */
public static String extractRequestParams(@Nullable String body, @Nullable Map
paramMap,
@Nullable Map
uriTemplateVarMap) {
String paramStr = null;
if (!ObjectUtils.isEmpty(paramMap)) {
paramStr = paramMap.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(e -> {
String[] sorted = Arrays.stream(e.getValue()).sorted().toArray(String[]::new);
return e.getKey() + "=" + joinStr(",", sorted);
})
.collect(Collectors.joining("&"));
}
String uriVarStr = null;
if (!ObjectUtils.isEmpty(uriTemplateVarMap)) {
uriVarStr = joinStr(",", uriTemplateVarMap.values().stream().sorted().toArray(String[]::new));
}
return joinStr("#", body, paramStr, uriVarStr);
}
public static String joinStr(String delimiter, @Nullable String... strs) {
if (ObjectUtils.isEmpty(strs)) {
return StrUtil.EMPTY;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < strs.length; i++) {
if (ObjectUtils.isEmpty(strs[i])) continue;
sb.append(strs[i].trim());
if (i < strs.length - 1 && !ObjectUtils.isEmpty(strs[i + 1])) {
sb.append(delimiter);
}
}
return sb.toString();
}
}The article concludes with a reminder that the source code is available on GitHub (https://github.com/passerbyYSQ/DemoRepository) and invites readers to discuss, ask questions, or join related communities.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.