双方API交互:签名及验证-- (AK/SK)认证的实现

接口交互提供一个开发的接口地址和接口文档,

知道了url,知道了参数怎么传,坏人就来了:

1.可以任意请求任意参数值,调用你的接口。

2.频繁请求、恶意攻击,让你一直处理接口对应的业务逻辑。

3.拦截某个请求,拿到参数信息,重复对发起请求(正常业务一个请求只能处理一次吧),如果此接口是写入某个业务数据到数据库,那你数据库的数据越来越多,数据还是一样的。。。

........................


因此:还需要约定签名算法规则等内容,来防止各种问题。

AK/SK:
AK:Access Key Id,用于标示用户。
SK:Secret Access Key,是用户用于加密认证字符串和用来验证认证字符串的密钥,其中SK必须保密。
通过使用Access Key Id / Secret Access Key加密的方法来验证某个请求的发送者身份。

基本思路:
1.客户端需要在认证服务器中预先设置 access key(AK 或叫 app ID) 和 secure key(SK)。
2.在调用 API 时,客户端需要对参数和 access key 等信息结合 secure key 进行签名生成一个额外的 sign字符串。
3.服务器接收到用户的请求后,系统将使用AK对应的相同的SK和同样的认证机制生成认证字符串,并与用户请求中包含的认证字符串进行比对。如果认证字符串相同,系统认为用户拥有指定的操作权限,并执行相关操作;如果认证字符串不同,系统将忽略该操作并返回错误码。

 

基于上文的思路,客户端和服务器端分别如何实现?

package xxx;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
import org.springframework.util.CollectionUtils;
import xxx.MapUtil;
import xxx.StringUtil;

public class SignUtil {
    /**
     * 通过请求参数,包装请求header信息(含签名信息)(客户端用)
     * 
     * @Title:wrapperHeader
     * @Description: TODO 
     * @date 2021年6月11日 下午1:52:35
     * @author yqwang
     * @param appId
     * @param appSecret
     * @param reqParam
     * @return {appId=000000, sign=B562FFD6FC691A42CD7F46D068B3F74A, nonce=d50e301d-ee2c-446e-8f28-013f0fee09fb, ts=1623388123195}
     */
    public static Map<String, Object> wrapperHeader(String appId, String appSecret, Map<String, Object> reqParam) {
        Long ts = System.currentTimeMillis();
        String nonce = UUID.randomUUID().toString();
        Map<String, Object> header = new HashMap<String, Object>();
        header.put("ts", ts);// 进行接口调用时的时间戳,即当前时间戳(毫秒),服务端会校验时间戳,例如时间差超过20分钟则认为请求无效,防止重复请求的攻击
        header.put("nonce", nonce);// 每个请求提供一个唯一的标识符,服务器能够防止请求被多次使用
        header.put("appId", appId);// 用于标识哪个三方系统发来的请求
        String sign = getSign(appId, appSecret, ts, nonce, reqParam);// 按签名算法获取sign
        header.put("sign", sign);
        return header;
    }

    /**
     * 按签名算法获取sign(客户端和服务器端算法一致,都需要用)
     * 
     * @Title:getSign
     * @Description: TODO
     * @date 2021年6月11日 下午1:15:13
     * @author yqwang
     * @param appId
     * @param appSecret
     * @param ts
     *            时间戳
     * @param nonce
     *            请求唯一标识
     * @param reqParam
     *            请求参数
     * @return
     */
    public static String getSign(String appId, String appSecret, Long ts, String nonce, Map<String, Object> reqParam) {
        // 计算签名规则:sign = md5("ts=1623388123195&noce=d50e301d-ee2c-446e-8f28-013f0fee09fb&appSecret=9ZLEzugQHfQd11vS8pd68lxzA&param1=1&param2=2")
        // 其他说明:这个规则双方来定,也可以不把reqParam带入计算
        // 1.请求参数key升序
        Map<String, Object> treeMap = new TreeMap<>();
        treeMap.putAll(reqParam);
        // 2.待加密字符串
        StringBuffer s = new StringBuffer();
        s.append("ts=").append(ts).append("&noce=").append(nonce).append("&appSecret=").append(appSecret);
        // append : &param1=1&param2=2
        treeMap.forEach((k, v) -> s.append("&").append(k).append("=").append(v));
        // 3.对待加密字符串进行加密(对字符串md5处理,得到sign值)
        return string2MD5(s.toString());
    }

    /**
     * 验证请求是否有效(服务器端用)
     * 
     * @Title:checkReqInfo
     * @Description: TODO
     * @date 2021年6月11日 下午1:19:21
     * @author yqwang
     * @param reqHeader
     * @param reqParam
     * @return 是否有效(方便测试我用Boolean,可根据业务需要,返回对应错误信息,不一定用Boolean)
     */
    public static Boolean checkReqInfo(Map<String, Object> reqHeader, Map<String, Object> reqParam) {
        // 1.没有header : 无效请求
        if (CollectionUtils.isEmpty(reqHeader)) return false;
        // 2.没有ts(请求时间戳):无效请求
        Long ts = MapUtil.get(reqHeader, "ts", Long.class);
        if (ts == null) return false;
        // 3.超过20分钟:无效请求
        if (System.currentTimeMillis() - ts > 20 * 60 * 1000) return false;
        // 4.如果带有请求唯一标识,则需要先验证此标识是否已经被处理过,防止重复请求(不一定每个项目都要求考虑此项,这里只是一个思路,需要的可以加上);如果处理过,返回false,如果没处理过,则把这个唯一标识存到redis(随意)
        String nonce = MapUtil.getStr(reqHeader, "nonce");
       /* if (StringUtil.isNotBlank(nonce)) {
            // 判断是否重复请求
            Boolean isRepeat = isRepeatReq(nonce);
            if (isRepeat) {
                return false;// 4.1重复请求:无效请求
            } else {
                saveToRedis(nonce);// 4.2标记此请求正在被处理或已被处理
            }
        }*/
        // 5.appId是否存在(用户是否存在),不存在则算无效请求
        String appId = MapUtil.getStr(reqHeader, "appId");
        if(StringUtil.isBlank(appId))return false;
        // 5.1去库中或配置中获取appId对应的appSecret  (这里方便测试,先写死)
        String appSecret = "9ZLEzugQHfQd11vS8pd68lxzA";//getAppSecretByAppId(appId);
        // 5.2没有此appId对应信息:无效请求
        if(StringUtil.isBlank(appSecret))return false;
        
        // 6.sign验证
        // 6.1 没传sign:无效请求
        String sign = MapUtil.getStr(reqHeader, "sign","");
        if(StringUtil.isBlank(sign))return false;
        //6.2最后验证sign值(按约定的sign计算方式,服务器端也算出一个sign,将这里计算出的sign和请求中的sign比较,是否一致)
        String srvSign = getSign(appId, appSecret, ts, nonce, reqParam);
        System.out.println(sign);
        System.out.println(srvSign);
        // 目前能想到的安全验证就这些,或许大家还能想到其他验证,让接口更加安全
        return sign.equalsIgnoreCase(srvSign);
    }

    /**
     * MD5加码 生成32位md5码
     */
    public static String string2MD5(String inStr) {
        MessageDigest md5 = null;
        try {
            md5 = MessageDigest.getInstance("MD5");
        } catch (Exception e) {
            System.err.println("MD5加码失败");
            return "";
        }
        char[] charArray = inStr.toCharArray();
        byte[] byteArray = new byte[charArray.length];

        for (int i = 0; i < charArray.length; i++)
            byteArray[i] = (byte) charArray[i];
        byte[] md5Bytes = md5.digest(byteArray);
        StringBuilder hexValue = new StringBuilder();
        for (int i = 0; i < md5Bytes.length; i++) {
            int val = ((int) md5Bytes[i]) & 0xff;
            if (val < 16) hexValue.append("0");
            hexValue.append(Integer.toHexString(val));
        }
        return hexValue.toString().toUpperCase();
    }

    // 测试
    public static void main(String[] args) {
        // A.客户端:请求(header+param)
        // A.1请求参数
        Map<String, Object> reqParam = new HashMap<String, Object>();
        reqParam.put("param2", "2");
        reqParam.put("param1", "1");
        // A.2请求头(行sign值等信息)
        String appId = "000000";// AK appId相当于用户名
        String appSecret = "9ZLEzugQHfQd11vS8pd68lxzA";// SK 相当于密码,提供给对方一个随机密码,不直接体现在请求中
        Map<String, Object> reqHeader = wrapperHeader(appId, appSecret, reqParam);
        // {appId=000000, sign=B562FFD6FC691A42CD7F46D068B3F74A, nonce=d50e301d-ee2c-446e-8f28-013f0fee09fb, ts=1623388123195}
        System.out.println(reqHeader);
        
        // ==================客户端发起请求,参数param,并把header带入请求中
        
        // ============================服务器端,收到请求
        // 1.验证请求信息,2处理业务逻辑,3.返回数据到客户端
        // 1.验证请求信息(方便测试,不再赘述如何获取请求的header和参数信息!直接用上文定义的reqHeader, reqParam)
        Boolean valid = checkReqInfo(reqHeader, reqParam);
        if(!valid){
           //无效,不再处理业务信息,返回失败
            System.out.println("无效");
        }
        System.out.println("有效请求,继续处理...");
        
        //2处理业务逻辑,3.返回数据到客户端...省略
    }
}

请直接看main()方法!!

注意:

文中用到了2个工具类,都可以按自己项目中的工具或变一下方式去实现,我就不贴出来了,大家都看得懂。
1.MapUtil 目前就是从map中获取某个值,这个可以自行处理。

2.StringUtil是我们内部的字符串工具类,可更换为org.apache.commons.lang.StringUtils或用其他方式判断字符串是否为空等。

  • 9
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
com.jd.open.api:open-api-sdk:2.0是京东开放平台的Java开发工具包,用于开发者与京东开放平台进行对接和交互的SDK。 京东开放平台是京东商城提供给商家和开发者的一套开放平台服务,包括商品查询、订单管理、用户授权、营销推广等功能。开发者通过使用open-api-sdk可以方便地使用京东开放平台的各种接口,节省开发时间和精力。 open-api-sdk的版本号为2.0,表示这已经是该工具包的第二个大版本,在之前版本的基础上进行了更新和改进。新版本的sdk通常包含更多功能、修复了之前版本中的bug,并提供更好的兼容性和稳定性。 使用com.jd.open.api:open-api-sdk:2.0可以通过调用相应的接口实现与京东开放平台的连接和数据交互。例如,开发者可以使用该工具包中提供的接口发送商品查询请求,获取商品的详细信息;也可以使用接口进行订单管理,包括订单创建、取消、查询等操作。 此外,open-api-sdk还提供了其他一些功能,如用户授权、优惠券领取与使用、营销推广等。开发者可以根据自己的需求选择使用相应的接口,与京东开放平台进行集成开发。 总之,com.jd.open.api:open-api-sdk:2.0是京东开放平台提供给开发者的Java开发工具包,可以方便地与京东开放平台进行对接和交互实现商品查询、订单管理、用户授权等功能。开发者可以根据具体需求使用该工具包中提供的接口,进行开发和集成。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yqwang_cn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值