API接口签名验证

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 过去对于接口的验证我一般都是直接在登录时为用户发放token,用户在随后的操作中携带了token则允许请求。但是这样的验证方式存在有一定的问题,如果token被泄露被他人获取,那么就会有非法请求的风险。其他人可以使用这个token自行调用接口进行请求,传入非法参数甚至进行注入攻击等,可能会造成严重的问题。

一、使用背景

过去对于接口的验证我一般都是直接在登录时为用户发放token,用户在随后的操作中携带了token则允许请求。


但是这样的验证方式存在有一定的问题,如果token被泄露被他人获取,那么就会有非法请求的风险。其他人可以使用这个token自行调用接口进行请求,传入非法参数甚至进行注入攻击等,可能会造成严重的问题。


即存在以下安全问题:

  • 请求身份是否合法
  • 请求参数是否被篡改


为了防止这种情况的发生,我们验证接收到的数据与客户端发送的数据一致,且让接口只能被客户端请求。


二、实现方案

既然要实现接口只能被客户端请求,那么我们不难想到可以与客户端达成某些约定,让客户端按一定的规则发送请求。


只需要服务端与客户端约定一套密钥,客户端在发送请求时拼接上私钥后使用单向加密算法进行加密,服务端收到后使用相同的私钥和加密算法进行加密后验证是否与前端客户端传递的值相同。


这样的方案可以使得用户即使拿到了token没有私钥的话加密后的数据与服务端加密后的肯定不相同而无法通过验证。篡改参数同样会产生相同的问题,从而保证了接口的安全性。


三、具体流程

  • 按照请求参数名的字母升序排列非空请求参数(包含accesskey),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串A;
  • 在字符串A最后拼接上secretkey得到字符串B;
  • 对B进行MD5运算,并将得到的字符串所有字符转换为大写,得到sign值;
  • 携带参数accesskey和sign进行请求

四、优化

以上方式虽然解决了非法用户请求和请求参数被篡改的问题,但是还存在着重复使用请求参数伪造二次请求的隐患。


为了解决这个问题我们不难想到请求时生成一个唯一的标识符,那么服务端在接收到请求之后只需要验证是否已经接受过改请求的标识即可。


不过以上方式还存在一个问题就是服务端要保存那么多的请求标识并不现实,所以我们可以设置一个过期时间,过期了就将标识删除。并让请求的时候携带一个时间戳,超过时间的请求直接打回。


五、代码实现

假设请求接口:test?param1=hello&param2=world

那么前端需要发送请求:test?accesskey=xxx&param1=hello&param2=world&nonce=随机唯一标识&timestamp=当前时间戳&sign=xxx


sign = MD5(“accesskey=xxx&nonce=随机唯一标识¶m1=hello¶m2=world&secretkey=xxx×tamp=当前时间戳”).toUpperCase()


注意请求接口中的参数可以不按顺序,而用于生成sign的字符串中的各个参数必须按照一定的顺序(与后端约定)


后端验签:

@Component
public class ApiVerifyUtil {
    public static final String SECRET_KEY = "secretkey";
    public static final String ACCESS_KEY = "accesskey";
    public static final String TIMESTAMP_KEY = "timestamp";
    public static final String NONCE_KEY = "nonce";
    public static final String SIGN_KEY = "sign";
    private static final HashMap<String, String> KEY_PAIR;
    static {
        KEY_PAIR = new HashMap<>();
        KEY_PAIR.put("app1", "password1");  // 为客户端分配的密钥
        KEY_PAIR.put("app2", "password2");
    }
    @Autowired
    private RedisTemplate<String, String> redisTemplateBean;
    private static RedisTemplate<String, String> redisTemplate;
    @PostConstruct
    public void init() {
        redisTemplate = redisTemplateBean;
    }
    public static final Integer OK = 0;
    public static final Integer PARAMS_ERROR = 1;
    public static final Integer LACK_ACCESSKEY = 2;
    public static final Integer ACCESSKEY_INVALID = 3;
    public static final Integer LACK_NONCE = 4;
    public static final Integer LACK_TIMESTAMP = 5;
    public static final Integer LACK_SIGN = 6;
    public static final Integer REQUEST_TIMEOUT = 7;
    public static final Integer REQUEST_REPEATED = 8;
    public static final Integer REQUEST_INVALID = 9;
    // 超时时间(ms)
    public static final long TIMEOUT = 1000 * 60 * 15;
    public static final String AND = "&";
    public static final String EQUALS = "=";
    public static Integer verify(HashMap<String, String> params) {
        if (params == null || params.isEmpty()) {
            return PARAMS_ERROR;
        }
        // accessKey为空或不合法(即不存在对应的密钥)或请求参数不全则直接打回
        String accessKey, secretKey, timestamp, nonce, sign;
        if ((accessKey = params.get(ACCESS_KEY)) == null) {
            return LACK_ACCESSKEY;
        }
        if ((secretKey = KEY_PAIR.get(accessKey)) == null) {
            return ACCESSKEY_INVALID;
        }
        if ((timestamp = params.get(TIMESTAMP_KEY)) == null) {
            return LACK_TIMESTAMP;
        } else if (Long.parseLong(timestamp) - System.currentTimeMillis() > TIMEOUT) {
            // 超过15分钟
            return REQUEST_TIMEOUT;
        }
        if ((nonce = params.get(NONCE_KEY)) == null) {
            return LACK_NONCE;
        } else {
            Set<String> nonceSet = redisTemplate.opsForSet().members("nonce");
            if (nonceSet != null && nonceSet.contains(nonce)) {
                return REQUEST_REPEATED;
            }
        }
        if ((sign = params.get(SIGN_KEY)) == null) {
            return LACK_SIGN;
        }
        params.remove(SIGN_KEY);
        // 加密需要拼接上密钥
        params.put(SECRET_KEY, secretKey);
        ArrayList<String> keyList = new ArrayList<>(params.keySet());
        // 按顺序构造
        Collections.sort(keyList);
        StringBuilder sb = new StringBuilder();
        for (String key : keyList) {
            sb.append(key).append("=").append(params.get(key)).append("&");
        }
        String newSign = sb.toString();
        newSign = MD5.create().digestHex16(newSign.substring(0, newSign.length() - 1).toUpperCase());
        if (newSign.equals(sign)) {
            redisTemplate.opsForSet().add("nonce", nonce);
            return OK;
        }
        return REQUEST_INVALID;
    }
    public static HashMap<String, String> getParams(HttpServletRequest httpServletRequest) {
        String queryString = httpServletRequest.getQueryString();
        String[] split = queryString.split(AND);
        HashMap<String, String> params = new HashMap<>();
        for (String param : split) {
            params.put(param.split(EQUALS)[0], param.split(EQUALS)[1]);
        }
        return params;
    }
    public static HashMap<String, String> getParams(String queryString) {
        String[] split = queryString.split(AND);
        HashMap<String, String> params = new HashMap<>();
        for (String param : split) {
            params.put(param.split(EQUALS)[0], param.split(EQUALS)[1]);
        }
        return params;
    }
}

六、后续优化

  1. 加入接口被调用的阈值限制,对接口访问频率设置一定阈值,对超过阈值的请求进行屏蔽及预警。
  2. 白名单机制:指定一些可以访问我们暴露接口的域名,不在白名单里面的域名发过来的请求,直接拒绝。
相关文章
|
9天前
|
人工智能 自然语言处理 API
Multimodal Live API:谷歌推出新的 AI 接口,支持多模态交互和低延迟实时互动
谷歌推出的Multimodal Live API是一个支持多模态交互、低延迟实时互动的AI接口,能够处理文本、音频和视频输入,提供自然流畅的对话体验,适用于多种应用场景。
54 3
Multimodal Live API:谷歌推出新的 AI 接口,支持多模态交互和低延迟实时互动
|
4天前
|
前端开发 API 数据库
Next 编写接口api
Next 编写接口api
|
10天前
|
XML JSON 缓存
阿里巴巴商品详情数据接口(alibaba.item_get) 丨阿里巴巴 API 实时接口指南
阿里巴巴商品详情数据接口(alibaba.item_get)允许商家通过API获取商品的详细信息,包括标题、描述、价格、销量、评价等。主要参数为商品ID(num_iid),支持多种返回数据格式,如json、xml等,便于开发者根据需求选择。使用前需注册并获得App Key与App Secret,注意遵守使用规范。
|
9天前
|
JSON API 开发者
淘宝买家秀数据接口(taobao.item_review_show)丨淘宝 API 实时接口指南
淘宝买家秀数据接口(taobao.item_review_show)可获取买家上传的图片、视频、评论等“买家秀”内容,为潜在买家提供真实参考,帮助商家优化产品和营销策略。使用前需注册开发者账号,构建请求URL并发送GET请求,解析响应数据。调用时需遵守平台规定,保护用户隐私,确保内容真实性。
|
10天前
|
搜索推荐 数据挖掘 API
淘宝天猫商品评论数据接口丨淘宝 API 实时接口指南
淘宝天猫商品评论数据接口(Taobao.item_review)提供全面的评论信息,包括文字、图片、视频评论、评分、追评等,支持实时更新和高效筛选。用户可基于此接口进行数据分析,支持情感分析、用户画像构建等,同时确保数据使用的合规性和安全性。使用步骤包括注册开发者账号、创建应用获取 API 密钥、发送 API 请求并解析返回数据。适用于电商商家、市场分析人员和消费者。
|
19天前
|
JSON API 开发工具
淘宝实时 API 接口丨淘宝商品详情接口(Taobao.item_get)
淘宝商品详情接口(Taobao.item_get)允许开发者获取商品的详细信息,包括基本信息、描述、卖家资料、图片、属性及销售情况等。开发者需注册账号、创建应用并获取API密钥,通过构建请求获取JSON格式数据,注意遵守平台规则,合理使用接口,确保数据准确性和时效性。
|
21天前
|
JSON 安全 API
Python调用API接口的方法
Python调用API接口的方法
88 5
|
20天前
|
JSON 缓存 监控
淘宝商品详情接口(Taobao.item_get)丨淘宝API接口指南
淘宝商品详情接口(Taobao.item_get)允许开发者通过HTTP GET方法获取淘宝商品的详细信息,包括商品ID、价格、库存等。请求需包含key、secret、num_iid等必选参数,支持缓存及多种返回格式。此接口广泛应用于电商数据分析、商品选品、价格监控等领域,提升商家运营效率。
|
24天前
|
JSON 搜索推荐 API
LAZADA关键词搜索API接口的获取与应用
Lazada作为东南亚领先的电商平台,为满足开发者和商户需求,开放了关键词搜索API接口。本文详细介绍该接口的获取与应用,助力提升电商业务效率。接口支持关键词搜索、指定搜索范围和排序方式,提供精准、灵活且全面的数据支持,促进电商应用和服务的优化与创新。
24 3
|
1月前
|
存储 数据可视化 API
重磅干货,免费三方网络验证[用户系统+CDK]全套API接口分享教程。
本套网络验证系统提供全面的API接口,支持用户注册、登录、数据查询与修改、留言板管理等功能,适用于不想自建用户系统的APP开发者。系统还包含CDK管理功能,如生成、使用、查询和删除CDK等。支持高自定义性,包括20个自定义字段,满足不同需求。详细接口参数及示例请参考官方文档。