• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

API签名认证

武飞扬头像
xc6
帮助1

API签名认证

  • 为什么需要API签名认证?

为了保证后台接口的安全,不能随便一个人就能调用后台接口。

  • 怎么设计API签名认证?

参数一:accessKey

参数二:secretKey

用于加密和解密,类似于公钥和私钥,都是无序,无规则。sk不能放到请求头中,防止被窃取

参数三:sign

参数四:请求参数

客户端通过aK 请求参数 签名算法进行加密(如MD5)得到一个不能被解密的值。

这个值就是sign。客户端会将sign和aK和请求参数传到服务端,服务端根据ak去数据库中查询sk,

用得到的sk 请求参数 同样的签名算法的到另一个sign2,然后比较客户端传过来的sign和sign2是否相同,

如果相同说明请求参数没有被篡改。

但是此时,如果有中间人捕获了一个正确的请求,就可以通过不断的重放来攻击服务器。

所以现在需要防止重放。

参数五:nonce

参数六:timestamp

每个请求都随机生成一个nonce,服务端可以考虑用redis记录该nonce,如果出现相同的nonce,说明该请求是重放过的。

但是如果有大量请求,则又会生成大量的数据存储在redis中,造成服务器压力。可以考虑对每个nonce设置一个过期时间。时间过后自动清理。

可是现在又会出现一个问题。中间人等上一个nonce过期后再进行重放,又可以攻击服务器。

此时需要引入timestamp时间戳

对每个请求设置一个时间戳,自定义时间(比如1分钟),服务器只接受当前时间与请求时间相差1分钟内的请求,视大于1分钟的请求为过期请求。现在就可以设置redis的缓存时间略大于1分钟,就可以解决重放攻击

  • 大概图解
    学新通

  • 怎么实现API签名认证算法

客户端实现

// 客户端调用部分

    /**
     * 将请求参数加密
     * @param body 请求参数
     * @return 请求头的参数
     */
	private Map<String, String> getHeaderMap(String body) {
        Map<String, String> hashMap = new HashMap<>();
        hashMap.put("accessKey", accessKey);
        // 一定不能直接发送
//        hashMap.put("secretKey", secretKey);
        hashMap.put("nonce", RandomUtil.randomNumbers(4));
        hashMap.put("body", body);
        hashMap.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
        // 加密
        hashMap.put("sign", SignUtils.genSign(body, secretKey));
        return hashMap;
    }

    public String getUsernameByPost(User user) {
        String json = JSONUtil.toJsonStr(user);
        // 利用HuTool的工具库来向网关发起请求
        HttpResponse httpResponse = HttpRequest.post(GATEWAY_HOST   "/api/name/user")
            // 加密
                .addHeaders(getHeaderMap(json))
                .body(json)
                .execute();
        System.out.println(httpResponse.getStatus());
        String result = httpResponse.body();
        System.out.println(result);
        return result;
    }
学新通
// 通过sk和body加密后进行传输

public class SignUtils {
    static  final String SALT = "xcxc";
    /**
     * 生成签名
     * @param body
     * @param secretKey
     * @return
     */
    public static String genSign(String body, String secretKey) {
        Digester md5 = new Digester(DigestAlgorithm.SHA256);
        String content = body   SALT   secretKey;
        return md5.digestHex(content);
    }
}
学新通

服务端部分

// 我是放到网关中一起处理

public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
       
        // 3. 用户鉴权(判断 ak、sk 是否合法)
        HttpHeaders headers = request.getHeaders();
        String accessKey = headers.getFirst("accessKey");
        String nonce = headers.getFirst("nonce");
        String timestamp = headers.getFirst("timestamp");
        String sign = headers.getFirst("sign");
        String body = headers.getFirst("body");
        User invokeUser = null;
        try {
            invokeUser = innerUserService.getInvokeUser(accessKey);
        } catch (Exception e) {
            log.error("getInvokeUser error", e);
        }
        if (invokeUser == null) {
            return handleNoAuth(response);
        }
		// 如果两次签名不一样,则拒绝此次请求
        String genSign = SignUtils.genSign(body, invokeUser.getSecretKey());
        if (!Objects.equals(genSign, sign)) {
            return handleNoAuth(response);
        }
        if (Long.parseLong(nonce) > 10000L) {
            return handleNoAuth(response);
        }
        // 时间和当前时间不能超过 1 分钟
        Long currentTime = System.currentTimeMillis() / 1000;
        final Long ONE_MINUTES = 60 * 1L;
        if ((currentTime - Long.parseLong(timestamp)) >= ONE_MINUTES) {
            return handleNoAuth(response);
        }
        // nonce不能存在redis,否则视为重放
        if (redisTemplate.hasKey(nonce)) {
            return handleNoAuth(response);
        }
        // 添加gatewayKey,防止前台绕过网关向后台发请求
        PathContainer pathContainer = request.getPath().pathWithinApplication();
        // 添加gatewayKey,防止下游接口直接被访问
        ServerHttpRequest.Builder mutate = request.mutate();
        mutate.header(GATEWAY_KEY, GATEWAY_VALUE);
        exchange.mutate().request(mutate.build()).build();
        // 将nonce写入redis
        // redis缓存时间要略大于时间戳设置的时间
        redisTemplate.boundValueOps(nonce).set(nonce,2, TimeUnit.MINUTES);

        // 实际情况中是从数据库中查出 secretKey
        String secretKey = invokeUser.getSecretKey();
        String serverSign = SignUtils.genSign(body, secretKey);
        if (sign == null || !sign.equals(serverSign)) {
            return handleNoAuth(response);
        }
        // 4. 请求的模拟接口是否存在,以及请求方法是否匹配
        InterfaceInfo interfaceInfo = null;
        try {
            interfaceInfo = innerInterfaceInfoService.getInterfaceInfo(path, method);
        } catch (Exception e) {
            log.error("getInterfaceInfo error", e);
        }
        if (interfaceInfo == null) {
            return handleNoAuth(response);
        }
        // 查询该用户是否还存在调用次数
        boolean leftInvokeCount = innerUserInterfaceInfoService.leftInvokeCount(interfaceInfo.getId(), invokeUser.getId());
        if (!leftInvokeCount) {
            return handleInvokeError(response);
        }
        // 5. 请求转发,调用模拟接口   响应日志
        //     Mono<Void> filter = chain.filter(exchange);
        //        return filter;
        return handleResponse(exchange, chain, interfaceInfo.getId(), invokeUser.getId());
    }
学新通
  • 需要注意的细节有哪些

    • 千万不要明文将sk放到请求中
    • 用什么方式加密就用什么方式解密
    • redis缓存的时间一定要略大于设置timestamp过期的时间

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhggbbhj
系列文章
更多 icon
同类精品
更多 icon
继续加载