0


【java】java接口安全之接口签名

1.自定义的加签规则

1.采用Http协议的get或post方式提交
其中get请求参数放在Params中,post请求参数放在Params中,或者body中传递
2.增加接口请求时间戳参数:timestamp ,放到 Params,参与签名计算
如:timestamp=1665727846  ,(秒)

3.签名算法说明
1)获取params参数和body中的非空、非空串的参数
2)所有的参数按参数名升序排序
3)按参数名及参数值互相连接组成一个请求参数串(paramStr),格式如下:
        name1=value1&name2=value2 ...
4)将secretKey(服务器密钥:4f6cxxxx38d892xxx),拼接到请求参数串的尾部,得到签名字符串(signStr)
        paramStr+"&key="+secretKey
5)将signStr通过MD5加密,得到 签名(sign) 

4. timestamp 、sign 放到 Params中,与其他接口请求参数一起发送给服务端

2.自定义的接口签名解签工具类

package com.camojojo.common.core.utils;

import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.camojojo.common.core.constant.WebError;
import com.camojojo.common.core.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.MultiValueMap;
import reactor.core.publisher.Flux;

import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;

/**
 * 接口签名验证
 * @author:yuchen
 * @createTime:2022/10/13 19:50
 */
@Slf4j
public class SignUtil {

    private static final String secretKey = "key=xxxxxxxxx";
    private static final String SIGN = "sign";
    private static final String TIMESTAMP = "timestamp";

    public static void checkSign(ServerHttpRequest request,long expireTime) {
        log.info("签名验证");
        String timeStr = request.getQueryParams().getFirst(TIMESTAMP);
        String sign =  request.getQueryParams().getFirst(SIGN);
        log.info("sign={},timeStr={}",sign,timeStr);
        if(StrUtil.isBlank(timeStr) || StrUtil.isBlank(sign)){
            throw new ServiceException(WebError.ERROR_COMMON_PARAMETER_IS_EMPTY,"参数为空");
        }
        long requestTime = Long.valueOf(timeStr);
        long now = System.currentTimeMillis()/1000;
        log.info("now={}",now);
        // 请求发起时间与当前时间超过expireTime,则接口请求过期
        if(now - requestTime > expireTime){
            throw new ServiceException(WebError.ERROR_API_REQUEST_EXPIRE,"接口请求过期");
        }
        Map<String, String> paramMap = new HashMap<>();
        String body = resolveBodyFromRequest(request);
        log.info("获取body中的参数={}",body);
        MultiValueMap<String, String> queryParams = request.getQueryParams();
        queryParams.forEach((k, v) -> {
            if(v != null && StrUtil.isNotBlank(v.get(0)) && !k.equalsIgnoreCase(SIGN)){
                paramMap.put(k,v.get(0));
            }
        });
        log.info("获取params中的参数={}",paramMap);
        if(StrUtil.isNotBlank(body)){
            JSONObject jsonObject = JSON.parseObject(body);
            for(String key :jsonObject.keySet()){
                if(StrUtil.isNotBlank(jsonObject.getString(key))){
                    paramMap.put(key,jsonObject.getString(key));
                }
            }
        }
        String serverSign = getSign(paramMap);
        log.info("serverSign={}",serverSign);
        if(!serverSign.equals(sign)){
            throw new ServiceException(WebError.ERROR_API_SIGN,"接口签名错误");
        }
    }

    public static String getSign(Map<String, String> map) {
        //按Key进行排序
        Map<String, String> sortMap = sortMapByKey(map);
        StringBuilder signStr= new StringBuilder();
        if(sortMap != null){
            for (Map.Entry<String, String> entry : sortMap.entrySet()) {
                signStr.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
            }
        }
        String sign = signStr.append(secretKey).toString();
        log.info("signStr={}",signStr);
        return SecureUtil.md5(sign);
    }

    /**
     * 使用 Map按key进行排序
     * @param map
     * @return
     */
    public static Map<String, String> sortMapByKey(Map<String, String> map) {
        if (map == null || map.isEmpty()) {
            return null;
        }
        //升序排序
        Map<String, String> sortMap = new TreeMap<>(String::compareTo);
        sortMap.putAll(map);
        return sortMap;
    }

    private static String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) {
        // 获取请求体
        Flux<DataBuffer> body = serverHttpRequest.getBody();
        AtomicReference<String> bodyRef = new AtomicReference<>();
        body.subscribe(buffer -> {
            CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
            DataBufferUtils.release(buffer);
            bodyRef.set(charBuffer.toString());
        });
        return bodyRef.get();
    }

}

3.进一步优化

    在实际实现中,发现body中参数可能包含数组,或者对象数组,此时按照以上需求也需要对每个key进行排序,否则会由于顺序原因造成前后端生成的签名不一致。

    后续优化,将body转成流,再进行md5加密,生成签名。这样就避免了对body处理繁琐的加签操作。
标签: java spring 前端

本文转载自: https://blog.csdn.net/qq_38971617/article/details/128083610
版权归原作者 程序员屿辰 所有, 如有侵权,请联系我们删除。

“【java】java接口安全之接口签名”的评论:

还没有评论