Restful API AK/SK认证

Posted 李某乐

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Restful API AK/SK认证相关的知识,希望对你有一定的参考价值。

AK/SK简介

AK(Access Key ID,用于标识用户)/SK(Secret Access Key,是用户用于加密认证的字符串和验证认证字符串的密钥,SK必须保密),主要用于对用户的调用行为进行鉴权和认证,相当于专用的用户名和密码

AK/SK认证流程

客户端根据双方协商好的规则算法生成Signature认证字符串,并将生成的Signature认证字符串设置到header中。当API网关/服务端接收到请求后,判断请求中是否包含Signature认证字符串。如果包含认证字符串,则执行下一步操作。
基于HTTP请求信息,使用与客户端相同的规则算法,生成Signature字符串并于与客户端提供的Signature字符串进行比对,如果内容不一致,则认为认证失败,拒绝该请求;如果内容一致,则表示认证成功,系统将按照客户端的请求内容进行操作。

客户端:
    构建http请求(包含 access key);
    使用请求内容和 使用secret access key计算的签名(signature);
    发送请求到服务端。
服务端:
    根据发送的access key 查找数据库获得对应的secret-key;
    使用一样的算法将请求内容和 secret-key一块儿计算签名(signature),和步骤2同样;
    对比用户发送的签名和服务端计算的签名,二者相同则认证经过,不然失败。

实现基本思路

  1. 客户端需要在认证服务器中预先设置(AK 或叫 app ID) 和 SK。
  2. 获取当前时间时间戳并生成请求唯一标识(随机码)
  3. 在调用API前,客户端需要将对 时间戳、请求标识、请求参数结合SK进行签名生成一个额外的sign字符串
  4. 将时间戳、请求标识、AK以及生成的sign字符串设置到请求header中
  5. 服务端收到客户端的请求后,先判断header中设置的四类认证数据是否存在。
  6. 根据header中的时间戳与当前时间比对判断是否该请求以过期,防止抓包后的恶意请求
  7. 根据header中的请求标识判断出该请求是否唯一(每次请求将唯一标识保存,待下次请求进来后进行比对判断。可设置保存时长)
  8. 根据AK获取客户端预先在认证服务器设置好的SK
  9. 将时间戳、请求标识、请求参数结合客户端预先设置好的SK使用与客户端相同的签名生成方式生成一个临时的sign字符串并与客户端请求中包含的sign字符串比较。
  10. 5、6、7、8、9这五步全部通过继续执行下一步操作,否则认证失败返回错误码

代码实现

基于上面的实现思路,大致写下代码,代码中加的有详细注释,逻辑就不一一解释了,写的比较简单。

拦截器懒得写了哈,我这就直接通过AOP 前置通知来实现认证信息的获取以及认证

@Before("executePointcut()")
    public void before(JoinPoint joinPoint){
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        Object[] args = joinPoint.getArgs();
        JSONObject json = (JSONObject) args[args.length - 1];
        Map sortedMap = JsonToMap.sortParams(json);
        Long timeStamp = Long.parseLong(request.getHeader("TimeStamp"));
        String nonce = request.getHeader("nonce");
        String s = map.get(nonce);
        if (s!=null){
            log.error("重复的请求...");
            Asserts.fail("Repeat request");
        }
        map.put(nonce,nonce);
        //开启守护线程 清除请求唯一标识
        executorService.execute(new RemoveMapRunnable(nonce));
        String sign = request.getHeader("sign");
        if (timeStamp==null||timeStamp<1||StringUtils.isNotEmpty(nonce)
        ||StringUtils.isNotEmpty(sign)){
            long endTime = System.currentTimeMillis();
            if (endTime-timeStamp > l){
                log.error("请求过期失效..");
                Asserts.fail("Request expired");
            }
        }else{
            log.error("认证参数缺失..");
            Asserts.fail("Missing authentication parameters");
        }
        if(!SignUtil.checkReqInfo(timeStamp, nonce, sign, sortedMap)){
            log.error("认证失败,sign={}",sign);
            Asserts.fail("Authentication failed");
        }
        log.info("认证成功...");
    }
    
    private class RemoveMapRunnable implements Runnable{
        private String nonce;
        public RemoveMapRunnable(String nonce){
            this.nonce = nonce;
        }
        @Override
        public void run() {
            synchronized (this){
                try {
                    Thread.sleep(l);
                    map.remove(this.nonce);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
/**
     * @Author lijl
     * @MethodName wrapperHeader
     * @Description 通过请求参数,包装请求header信息(含签名信息)
     * @Date 16:14 2021/11/11
     * @Version 1.0
     * @param reqParam
     * @return: {sign=02C89AD7CEC9C05831520015CD7C3413F1DE03822D2DA015A7B353B7E7F38E7D, nonce=6b10f2ee-aba6-4032-bc9f-ca82c76b30d1, TimeStamp=1636684729852}
    **/
    public static Map<String, Object> wrapperHeader(Map<String, Object> reqParam) {
        Long ts = System.currentTimeMillis();
        String nonce = UUID.randomUUID().toString();
        Map<String, Object> header = new HashMap<>();
        //进行接口调用时的时间戳,即当前时间戳(毫秒),服务端会校验时间戳,例如时间差超过30秒则认为请求无效,防止重复请求的攻击
        header.put("TimeStamp", ts);
        //每个请求提供一个唯一的标识符,服务器能够防止请求被多次使用
        header.put("nonce", nonce);
        //按签名算法获取sign
        String sign = getSign(appSecret, ts, nonce, reqParam);
        header.put("sign", sign);
        return header;
    }

    /**
     * @Author lijl
     * @MethodName getSign
     * @Description 按签名算法获取sign
     * @Date 16:04 2021/11/11
     * @Version 1.0
     * @param appSecret
     * @param ts
     * @param nonce
     * @param reqParam
     * @return: java.lang.String
    **/
    private static String getSign(String appSecret, Long ts, String nonce, Map<String, Object> reqParam) {
        // 计算签名规则:sign = HMACSHA256("ts=1623388123195&noce=d50e301d-ee2c-446e-8f28-013f0fee09fb&appSecret=1ZLAzEgQHfBd19vSapdL8lxzA&1=2&1=2")
        // 1.请求参数key升序
        // 2.待加密字符串
        StringBuffer s = new StringBuffer();
        s.append("&ts=").append(ts).append("&noce=").append(nonce).append("&appSecret=").append(appSecret);
        reqParam.forEach((k, v) -> s.append("&").append(k).append("=").append(v));
        // 3.对待加密字符串进行加密(对字符串HMACSHA256处理,得到sign值)
        try {
            return HMACSHA256(s.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * @Author lijl
     * @MethodName checkReqInfo
     * @Description 验证请求是否有效
     * @Date 10:36 2021/11/12
     * @Version 1.0
     * @param ts
     * @param nonce
     * @param sign
     * @param reqParam
     * @return: 是否有效(方便测试我用Boolean,可根据业务需要,返回对应错误信息,不一定用Boolean)
    **/
    public static Boolean checkReqInfo(Long ts, String nonce, String sign,Map<String, Object> reqParam) {
        String srvSign = getSign(appSecret, ts, nonce, reqParam);
        // 目前能想到的安全验证就这些,或许大家还能想到其他验证,让接口更加安全
        return sign.equalsIgnoreCase(srvSign);
    }

    /**
     * @Author lijl
     * @MethodName HMACSHA256
     * @Description HMAC-SHA256算法
     * @Date 10:32 2021/11/12
     * @Version 1.0
     * @param data
     * @return: java.lang.String
    **/
    public static String HMACSHA256(String data) throws Exception {
        Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
        SecretKeySpec secret_key = new SecretKeySpec(appSecret.getBytes("UTF-8"), "HmacSHA256");
        sha256_HMAC.init(secret_key);
        byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
        StringBuilder sb = new StringBuilder();
        for (byte item : array) {
            sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
        }
        return sb.toString().toUpperCase();
    }

    public static void main(String[] args) {
        Map<String, Object> reqParam = new HashMap<String, Object>();
        reqParam.put("1", "2");
        reqParam.put("2", "1");
        //请求头(行sign值等信息)
        Map<String, Object> reqHeader = wrapperHeader(reqParam);
        System.out.println(reqHeader);
        // ==================客户端发起请求,参数param,并把header带入请求中

        // ============================服务器端,收到请求
        // 1.验证请求信息
        // 2处理业务逻辑
        // 3.返回数据到客户端
        long ts = (long) reqHeader.get("TimeStamp");
        String nonce = (String) reqHeader.get("nonce");
        String sign = (String) reqHeader.get("sign");
        Boolean valid = checkReqInfo(ts,nonce,sign,reqParam);
        if (valid){
            System.out.println("有效请求,继续处理...");
        }else {
            System.out.println("无效");
        }
    }

以上是关于Restful API AK/SK认证的主要内容,如果未能解决你的问题,请参考以下文章

Restful API AK/SK认证

Restful API AK/SK认证

公有云API的认证方式:Token认证和AK/SK认证

034.认证方式 | Token认证 及 AK/SK认证

跨平台应用开发进阶(三十二) :AK/SK鉴权原理简介

网关加签校验失败,请检查鉴权配置