第三方接口安全签名(Signature)实现方案"/>
java/springboot服务第三方接口安全签名(Signature)实现方案
前言
有的时候,我们需要把我们系统里的接口开放给第三方应用或企业使用,那第三方的系统并不在我们自己的认证授权用户体系内,此时,要如何保证我们接口的数据安全和身份识别呢?
在为第三方系统提供接口的时候,肯定要考虑接口数据的安全问题,比如数据是否被篡改,数据是否已经过时,数据是否可以重复提交等问题。其中我认为最终要的还是数据是否被篡改。
业内普遍的做法是给第三方application分配appId和appSecret,然后进行接口签名校验。
签名流程
签名规则
1、线下分配appid和appsecret,针对不同的调用方分配不同的appid和appsecret
2、加入timestamp(时间戳),10分钟内数据有效
3、加入流水号nonce(防止重复提交),至少为10位。针对查询接口,流水号只用于日志落地,便于后期日志核查。 针对办理类接口需校验流水号在有效期内的唯一性,以避免重复请求。
4、加入signature,所有数据的签名信息。
以上红色字段放在请求头中。
签名的生成
signature 字段生成规则如下。
数据部分
Path:按照path中的顺序将所有value进行拼接
Query:按照key字典序排序,将所有key=value进行拼接
Form:按照key字典序排序,将所有key=value进行拼接
Body:
Json: 按照key字典序排序,将所有key=value进行拼接(例如{"a":"a","c":"c","b":{"e":"e"}} => a=ab=e=ec=c)
String: 整个字符串作为一个拼接
如果存在多种数据形式,则按照path、query、form、body的顺序进行再拼接,得到所有数据的拼接值。
上述拼接的值记作 Y。
请求头部分
X=”appid=xxxnonce=xxxtimestamp=xxx”
生成签名
最终拼接值=XY
最后将最终拼接值按照如下方法进行加密得到签名。
signature=org.apachemons.codec.digest.HmacUtils.hmacSha256Hex(app secret, 拼接的值);
签名算法实现
指定哪些接口或者哪些实体需要进行签名
import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target;import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME;@Target({TYPE, METHOD}) @Retention(RUNTIME) @Documented public @interface Signature {String ORDER_SORT = "ORDER_SORT";//按照order值排序String ALPHA_SORT = "ALPHA_SORT";//字典序排序boolean resubmit() default true;//允许重复请求String sort() default Signature.ALPHA_SORT; }
指定哪些字段需要进行签名
import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target;import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME;@Target({FIELD}) @Retention(RUNTIME) @Documented public @interface SignatureField {//签名顺序int order() default 0;//字段name自定义值String customName() default "";//字段value自定义值String customValue() default ""; }
核心算法
/*** 生成所有注有 SignatureField属性 key=value的 拼接*/ public static String toSplice(Object object) {if (Objects.isNull(object)) {return StringUtils.EMPTY;}if (isAnnotated(object.getClass(), Signature.class)) {Signature sg = findAnnotation(object.getClass(), Signature.class);switch (sg.sort()) {case Signature.ALPHA_SORT:return alphaSignature(object);case Signature.ORDER_SORT:return orderSignature(object);default:return alphaSignature(object);}}return toString(object); }private static String alphaSignature(Object object) {StringBuilder result = new StringBuilder();Map<String, String> map = new TreeMap<>();for (Field field : getAllFields(object.getClass())) {if (field.isAnnotationPresent(SignatureField.class)) {field.setAccessible(true);try {if (isAnnotated(field.getType(), Signature.class)) {if (!Objects.isNull(field.get(object))) {map.put(field.getName(), toSplice(field.get(object)));}} else {SignatureField sgf = field.getAnnotation(SignatureField.class);if (StringUtils.isNotEmpty(sgf.customValue()) || !Objects.isNull(field.get(object))) {map.put(StringUtils.isNotBlank(sgf.customName()) ? sgf.customName() : field.getName(), StringUtils.isNotEmpty(sgf.customValue()) ? sgf.customValue() : toString(field.get(object)));}}} catch (Exception e) {LOGGER.error("签名拼接(alphaSignature)异常", e);}}}for (Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator(); iterator.hasNext(); ) {Map.Entry<String, String> entry = iterator.next();result.append(entry.getKey()).append("=").append(entry.getValue());if (iterator.hasNext()) {result.append(DELIMETER);}}return result.toString(); }/*** 针对array, collection, simple property, map做处理*/ private static String toString(Object object) {Class<?> type = object.getClass();if (BeanUtils.isSimpleProperty(type)) {return object.toString();}if (type.isArray()) {StringBuilder sb = new StringBuilder();for (int i = 0; i < Array.getLength(object); ++i) {sb.append(toSplice(Array.get(object, i)));}return sb.toString();}if (ClassUtils.isAssignable(Collection.class, type)) {StringBuilder sb = new StringBuilder();for (Iterator<?> iterator = ((Collection<?>) object).iterator(); iterator.hasNext(); ) {sb.append(toSplice(iterator.next()));if (iterator.hasNext()) {sb.append(DELIMETER);}}return sb.toString();}if (ClassUtils.isAssignable(Map.class, type)) {StringBuilder sb = new StringBuilder();for (Iterator<? extends Map.Entry<String, ?>> iterator = ((Map<String, ?>) object).entrySet().iterator(); iterator.hasNext(); ) {Map.Entry<String, ?> entry = iterator.next();if (Objects.isNull(entry.getValue())) {continue;}sb.append(entry.getKey()).append("=").append(toSplice(entry.getValue()));if (iterator.hasNext()) {sb.append(DELIMETER);}}return sb.toString();}return NOT_FOUND; }
签名的校验
header中的参数如下
签名实体
import com.googlemon.base.MoreObjects; import com.googlemon.collect.Sets; import org.hibernate.validator.constraints.NotBlank;import java.util.Set;@ConfigurationProperties(prefix = "wmhopenapi.validate", exceptionIfInvalid = false) @Signature public class SignatureHeaders {public static final String SIGNATURE_HEADERS_PREFIX = "wmhopenapi-validate";public static final Set<String> HEADER_NAME_SET = Sets.newHashSet();private static final String HEADER_APPID = SIGNATURE_HEADERS_PREFIX + "-appid";private static final String HEADER_TIMESTAMP = SIGNATURE_HEADERS_PREFIX + "-timestamp";private static final String HEADER_NONCE = SIGNATURE_HEADERS_PREFIX + "-nonce";private static final String HEADER_SIGNATURE = SIGNATURE_HEADERS_PREFIX + "-signature";static {HEADER_NAME_SET.add(HEADER_APPID);HEADER_NAME_SET.add(HEADER_TIMESTAMP);HEADER_NAME_SET.add(HEADER_NONCE);HEADER_NAME_SET.add(HEADER_SIGNATURE);}/*** 线下分配的值* 客户端和服务端各自保存appId对应的appSecret*/@NotBlank(message = "Header中缺少" + HEADER_APPID)@SignatureFieldprivate String appid;/*** 线下分配的值* 客户端和服务端各自保存,与appId对应*/@SignatureFieldprivate String appsecret;/*** 时间戳,单位: ms*/@NotBlank(message = "Header中缺少" + HEADER_TIMESTAMP)@SignatureFieldprivate String timestamp;/*** 流水号【防止重复提交】; (备注:针对查询接口,流水号只用于日志落地,便于后期日志核查; 针对办理类接口需校验流水号在有效期内的唯一性,以避免重复请求)*/@NotBlank(message = "Header中缺少" + HEADER_NONCE)@SignatureFieldprivate String nonce;/*** 签名*/@NotBlank(message = "Header中缺少" + HEADER_SIGNATURE)private String signature;public String getAppid() {return appid;}public void setAppid(String appid) {this.appid = appid;}public String getAppsecret() {return appsecret;}public void setAppsecret(String appsecret) {this.appsecret = appsecret;}public String getTimestamp() {return timestamp;}public void setTimestamp(String timestamp) {this.timestamp = timestamp;}public String getNonce() {return nonce;}public void setNonce(String nonce) {this.nonce = nonce;}public String getSignature() {return signature;}public void setSignature(String signature) {this.signature = signature;}@Overridepublic String toString() {return MoreObjects.toStringHelper(this).add("appid", appid).add("appsecret", appsecret).add("timestamp", timestamp).add("nonce", nonce).add("signature", signature).toString();} }
根据request 中 header值生成SignatureHeaders实体
private SignatureHeaders genrateSignatureHeaders(Signature signature, HttpServletRequest request) throws Exception {//NOSONARMap<String, Object> headerMap = Collections.list(request.getHeaderNames()).stream().filter(headerName -> SignatureHeaders.HEADER_NAME_SET.contains(headerName)).collect(Collectors.toMap(headerName -> headerName.replaceAll("-", "."), headerName -> request.getHeader(headerName)));PropertySource propertySource = new MapPropertySource("signatureHeaders", headerMap);SignatureHeaders signatureHeaders = RelaxedConfigurationBinder.with(SignatureHeaders.class).setPropertySources(propertySource).doBind();Optional<String> result = ValidatorUtils.validateResultProcess(signatureHeaders);if (result.isPresent()) {throw new ServiceException("WMH5000", result.get());}//从配置中拿到appid对应的appsecretString appSecret = limitConstants.getSignatureLimit().get(signatureHeaders.getAppid());if (StringUtils.isBlank(appSecret)) {LOGGER.error("未找到appId对应的appSecret, appId=" + signatureHeaders.getAppid());throw new ServiceException("WMH5002");}//其他合法性校验Long now = System.currentTimeMillis();Long requestTimestamp = Long.parseLong(signatureHeaders.getTimestamp());if ((now - requestTimestamp) > EXPIRE_TIME) {String errMsg = "请求时间超过规定范围时间10分钟, signature=" + signatureHeaders.getSignature();LOGGER.error(errMsg);throw new ServiceException("WMH5000", errMsg);}String nonce = signatureHeaders.getNonce();if (nonce.length() < 10) {String errMsg = "随机串nonce长度最少为10位, nonce=" + nonce;LOGGER.error(errMsg);throw new ServiceException("WMH5000", errMsg);}if (!signature.resubmit()) {String existNonce = redisCacheService.getString(nonce);if (StringUtils.isBlank(existNonce)) {redisCacheService.setString(nonce, nonce);redisCacheService.expire(nonce, (int) TimeUnit.MILLISECONDS.toSeconds(RESUBMIT_DURATION));} else {String errMsg = "不允许重复请求, nonce=" + nonce;LOGGER.error(errMsg);throw new ServiceException("WMH5000", errMsg);}}//设置appsecretsignatureHeaders.setAppsecret(appSecret);return signatureHeaders; }
生成签名前需要几个步骤,如下。
(1)、appid是否合法
(2)、根据appid从配置中心中拿到appsecret
(3)、请求是否已经过时,默认10分钟
(4)、随机串是否合法
(5)、是否允许重复请求
生成header信息参数拼接
String headersToSplice = SignatureUtils.toSplice(signatureHeaders);
生成header中的参数,mehtod中的参数的拼接
private List<String> generateAllSplice(Method method, Object[] args, String headersToSplice) {List<String> pathVariables = Lists.newArrayList(), requestParams = Lists.newArrayList();String beanParams = StringUtils.EMPTY;for (int i = 0; i < method.getParameterCount(); ++i) {MethodParameter mp = new MethodParameter(method, i);boolean findSignature = false;for (Annotation anno : mp.getParameterAnnotations()) {if (anno instanceof PathVariable) {if (!Objects.isNull(args[i])) {pathVariables.add(args[i].toString());}findSignature = true;} else if (anno instanceof RequestParam) {RequestParam rp = (RequestParam) anno;String name = mp.getParameterName();if (StringUtils.isNotBlank(rp.name())) {name = rp.name();}if (!Objects.isNull(args[i])) {List<String> values = Lists.newArrayList();if (args[i].getClass().isArray()) {//数组for (int j = 0; j < Array.getLength(args[i]); ++j) {values.add(Array.get(args[i], j).toString());}} else if (ClassUtils.isAssignable(Collection.class, args[i].getClass())) {//集合for (Object o : (Collection<?>) args[i]) {values.add(o.toString());}} else {//单个值values.add(args[i].toString());}values.sort(Comparator.naturalOrder());requestParams.add(name + "=" + StringUtils.join(values));}findSignature = true;} else if (anno instanceof RequestBody || anno instanceof ModelAttribute) {beanParams = SignatureUtils.toSplice(args[i]);findSignature = true;}if (findSignature) {break;}}if (!findSignature) {LOGGER.info(String.format("签名未识别的注解, method=%s, parameter=%s, annotations=%s", method.getName(), mp.getParameterName(), StringUtils.join(mp.getMethodAnnotations())));}}List<String> toSplices = Lists.newArrayList();toSplices.add(headersToSplice);toSplices.addAll(pathVariables);requestParams.sort(Comparator.naturalOrder());toSplices.addAll(requestParams);toSplices.add(beanParams);return toSplices; }
对最终的拼接结果重新生成签名信息
SignatureUtils.signature(allSplice.toArray(new String[]{}), signatureHeaders.getAppsecret());
依赖第三方工具包
<dependency><groupId>org.apachemons</groupId><artifactId>commons-lang3</artifactId> </dependency> <dependency><groupId>org.springframework</groupId><artifactId>spring-beans</artifactId> </dependency>
使用示例
生成签名
//初始化请求头信息 SignatureHeaders signatureHeaders = new SignatureHeaders(); signatureHeaders.setAppid("111"); signatureHeaders.setAppsecret("222"); signatureHeaders.setNonce(SignatureUtils.generateNonce()); signatureHeaders.setTimestamp(String.valueOf(System.currentTimeMillis())); List<String> pathParams = new ArrayList<>(); //初始化path中的数据 pathParams.add(SignatureUtils.encode("18237172801", signatureHeaders.getAppsecret())); //调用签名工具生成签名 signatureHeaders.setSignature(SignatureUtils.signature(signatureHeaders, pathParams, null, null)); System.out.println("签名数据: " + signatureHeaders); System.out.println("请求数据: " + pathParams);
输出结果
拼接结果: appid=111^_^appsecret=222^_^nonce=c9e778ba668c8f6fedf35634eb493af6304d54392d990262d9e7c1960b475b67^_^timestamp=1538207443910^_^w8rAwcXDxcDKwsM=^_^ 签名数据: SignatureHeaders{appid=111, appsecret=222, timestamp=1538207443910, nonce=c9e778ba668c8f6fedf35634eb493af6304d54392d990262d9e7c1960b475b67, signature=0a7d0b5e802eb5e52ac0cfcd6311b0faba6e2503a9a8d1e2364b38617877574d} 请求数据: [w8rAwcXDxcDKwsM=]
介绍下HmacSHA1
HmacSHA1是基于散列消息鉴别码(HMAC)算法的一种实现。HMAC是一种用于验证消息完整性和身份验证的密钥相关的哈希算法。
HmacSHA1结合了SHA-1散列函数和密钥的使用,以生成一个具有固定长度输出的消息认证码(MAC)。这个MAC可用于验证数据在传输过程中是否被篡改或冒充。
HmacSHA1算法的工作流程如下:
-
准备消息和密钥:将要进行认证的消息和密钥准备好。
-
密钥填充:如果密钥长度小于64字节,将其填充为64字节;如果密钥长度大于64字节,则使用SHA-1哈希将其缩短为64字节。
-
内部秘密哈希:将每个密钥字节与0x36异或,然后与消息进行SHA-1散列。
-
最终哈希:将第3步得到的哈希结果与原始密钥再次进行处理。将每个密钥字节与0x5C异或,然后与第3步得到的哈希结果进行SHA-1散列。
-
输出认证码:最终的SHA-1哈希结果即为HmacSHA1的认证码。
HmacSHA1提供了一种安全的消息认证码算法,它不仅依赖于SHA-1的散列性质,还利用了密钥的混合性。因此,即使攻击者能够获取消息和认证码,也很难破解出原始密钥,从而保护了数据的完整性和安全性。
当在Java中使用HmacSHA1来对GET和POST请求数据进行接口签名,并在拦截器或过滤器中进行验签时,你可以按照以下步骤编写代码:
-
创建一个拦截器或过滤器类,实现
javax.servlet.Filter
接口或org.springframework.web.servlet.HandlerInterceptor
接口。 -
在拦截器或过滤器中,获取GET或POST请求的相关数据,包括URL、请求参数等。
-
获取请求头中的appId、timestamp、nonce和signature。
-
根据接口定义的规则,将appId、timestamp和nonce按指定的顺序拼接起来,然后使用密钥对拼接后的字符串进行HmacSHA1签名生成签名结果。
-
将生成的签名结果与请求头中的signature进行比较,如果一致,则验证通过,否则验证失败。
以下是一个示例代码,展示了如何在Java中使用HmacSHA1对GET和POST请求进行接口签名和验签:
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;@WebFilter(urlPatterns = "/api/*")
public class SignatureFilter implements Filter {private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";private static final String SECRET_KEY = "yourSecretKey";@Overridepublic void init(FilterConfig filterConfig) throws ServletException {// 初始化操作}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {HttpServletRequest httpRequest = (HttpServletRequest) request;String method = httpRequest.getMethod();String url = httpRequest.getRequestURL().toString();// 获取请求头中的参数String appId = httpRequest.getHeader("appId");String timestamp = httpRequest.getHeader("timestamp");String nonce = httpRequest.getHeader("nonce");String signature = httpRequest.getHeader("signature");try {String signPayload = appId + timestamp + nonce;if (method.equalsIgnoreCase("POST")) {// 获取POST请求的请求体数据// 这里假设请求体为JSON格式,因为是以字符串的形式读取,所以不存在key乱序的问题String body = httpRequest.getReader().lines().reduce("", (accumulator, actual) -> accumulator + actual);signPayload += body;} else if (method.equalsIgnoreCase("GET")) {// 获取GET请求的参数String queryString = httpRequest.getQueryString();if (queryString != null) {signPayload += queryString;}}String generatedSignature = generateHmacSHA1Signature(signPayload);if (generatedSignature.equals(signature)) {// 验签通过,继续处理请求chain.doFilter(request, response);} else {// 验签失败,返回错误响应HttpServletResponse httpResponse = (HttpServletResponse) response;httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);httpResponse.getWriter().write("Invalid signature");}} catch (NoSuchAlgorithmException | InvalidKeyException e) {e.printStackTrace();// 处理异常情况}}@Overridepublic void destroy() {// 销毁操作}private String generateHmacSHA1Signature(String data) throws NoSuchAlgorithmException, InvalidKeyException {byte[] secretKeyBytes = SECRET_KEY.getBytes();SecretKeySpec secretKeySpec = new SecretKeySpec(secretKeyBytes, HMAC_SHA1_ALGORITHM);Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);mac.init(secretKeySpec);byte[] dataBytes = data.getBytes();byte[] signatureBytes = mac.doFinal(dataBytes);return Base64.getEncoder().encodeToString(signatureBytes);}
}
上述代码示例中的SECRET_KEY
是用于生成HmacSHA1签名的密钥,请替换为你自己的密钥。你可以根据你的应用需求添加其他验证逻辑和错误处理逻辑。
请注意,在实际应用中,还需要确保请求头中的appId、timestamp和nonce的合法性,并加入适当的时效性检查和重放攻击防护措施。
更多推荐
java/springboot服务第三方接口安全签名(Signature)实现方案
发布评论