双方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 wrapperHeader(String appId, String appSecret, Map reqParam) {
        Long ts = System.currentTimeMillis();
        String nonce = UUID.randomUUID().toString();
        Map header = new HashMap();
        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 reqParam) {
        // 计算签名规则:sign = md5("ts=1623388123195&noce=d50e301d-ee2c-446e-8f28-013f0fee09fb&appSecret=9ZLEzugQHfQd11vS8pd68lxzA¶m1=1¶m2=2")
        // 其他说明:这个规则双方来定,也可以不把reqParam带入计算
        // 1.请求参数key升序
        Map 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 : ¶m1=1¶m2=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 reqHeader, Map 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 reqParam = new HashMap();
        reqParam.put("param2", "2");
        reqParam.put("param1", "1");
        // A.2请求头(行sign值等信息)
        String appId = "000000";// AK appId相当于用户名
        String appSecret = "9ZLEzugQHfQd11vS8pd68lxzA";// SK 相当于密码,提供给对方一个随机密码,不直接体现在请求中
        Map 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或用其他方式判断字符串是否为空等。

你可能感兴趣的:(java,AK/SK,签名Sign)