发现一肉鸡接口,快来围攻啦~

简介: 本文开篇以系统登录页面的安全加固实践说起,详细分析了服务端提供的`loginEncryptKey` API 代码存在的问题,如接口对任意用户名返回密钥导致的安全风险,以及在高并发场景下可能引起的Redis缓存穿透问题。针对这些问题,提出了改进措施:首先对接口请求参数进行严格校验,确保用户名合法且存在于系统中;其次,优化加密密钥的生成与获取逻辑,采用预生成密钥池结合自定义哈希算法的方式,减少重复生成密钥的资源消耗。此外,为了适应集群环境,提出了利用Redis哈希表存储密钥和结合本地缓存的解决方案。最后,文章建议将密钥预置于应用配置文件中,简化了集群环境下的数据同步需求。

系统登录页面,为防止明文传输用户密码,开发者做了安全加固。

服务端暴露一个GET形式的 loginEncryptKey 的API,用来根据登录名 username 获取 加密秘钥 encryptKey。 前端页面 获取到 encryptKey 后,在请求login登录接口时,会对 用户password 进行加密传输。

这个 loginEncryptKey 接口的服务端怎么写的呢?下面是完整代码,其中SysLoginModel中定义了 username 属性。程序利用redis缓存,来存储RSA公私钥。

@PostMapping(value = "/loginEncryptKey")
public Result<String> loginEncryptKey(@RequestBody SysLoginModel sysLoginModel) {
   
    String cacheKey = LOGIN_ENCRYPT_KEY_CACHE + sysLoginModel.getUserName();
    //缓存获取
    String cache = redisUtil.get(cacheKey, "");
    if ("" != cache) {
   
        String publicKeyStr = JSON.parseObject(cache).getString("public");
        return Result.successWithMsg(publicKeyStr);
    }

    KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
    // 初始化密钥对生成器,密钥大小为96-1024位
    keyPairGen.initialize(1024, new SecureRandom());
    // 生成一份密钥对,保存在keyPair中
    KeyPair keyPair = keyPairGen.generateKeyPair();
    // 得到公钥字符串
    String publicKeyString = Base64.encode(keyPair.getPublic().getEncoded());
    // 得到私钥字符串
    String privateKeyString = Base64.encode(keyPair.getPrivate().getEncoded());

    //缓存写入
    JSONObject cacheMap = new JSONObject();
    cacheMap.put("public", publicKeyString);
    cacheMap.put("private", privateKeyString);
    redisUtil.set(cacheKey, cacheMap.toJSONString(), 7 * 24 * 60 * 60);

    return Result.successWithMsg(publicKeyString);
}



在进行API测试中发现,这个接口相当肉鸡!给username传任意值,包括空值,都可以获取到一个秘钥。

image.png

显然,这为恶意攻击开辟了绿道。当流量攻击像疯狗一样呼啸而来,就会发生redis缓存穿透,这里虽然不是穿透到数据库,但是不停地生成RSA密钥对,这个程序开销也不小。



我们来看看怎么修复、完善这个API的实现逻辑。

首先,接口要校验请求参数,这是对程序员的底线要求。

判空吗?

光判空,显然是不够的。那怎么办?

除了判空,还要判断参数的合法性。对于明显错误的传值,如username是一段1024的大文本,直接pass掉。就是说,凡是不符合系统username规则的参数值,诸如包含了“@”、“-”以外的非法符号,诸如长度≤3,直接拦截掉。

其次,我们可以判断username在系统里是否存在。不存在,则中止程序,直接响应“非法请求”。当然,这里不能每次都实时查库,用本地缓存为上。

其次,再来说如何更好地生成、获取encryptKey

打开脑洞---->如果不用redis,岂不妙哉!

统一用一份encryptKey?显然,不够安全。

因此,程序可以预先生成一批比如10份 encryptKey。 然后,当接口收到请求时,根据username的哈希值,然后做取模运算,从这批encryptKey里命中一个公钥返回。

随机从这10份 encryptKey 里直接取一个公钥返回,不香吗?当然不行!因为后面的login接口里,需要用同样的 encryptKey 私钥来解密password。就是说,login接口也要使用上面的根据username获取encryptKey的方法。

既然涉及到复用,在程序设计上,我们没理由不进行封装。我们来看下面的代码:

private static int customHash(String username) {
   
    int hash = 0;
//        hash=username.hashCode(); // 默认的哈希算法 hashCode(s) = s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
    // 自定义哈希算法,防止被破解
    for (int i = 0; i < username.length(); i++) {
   
        hash = hash * 17 + username.charAt(i);
    }
    int mod = hash % 10; // 这里的 10 要定义成常量,因为这意味着要预先生成10份encryptKey。
    return mod;
}

上面的封装OK吗? - - 不OK(封装得不彻底)!

要封装的,应该是下面这样的代码,其中的KeyType是hutool工具包中的枚举,表示公钥或私钥。

private static String getEncryptKeyByUserName(String username, KeyType keyType) {
   
    int hash = 0;
    // 自定义哈希算法,防止被破解
    for (int i = 0; i < username.length(); i++) {
   
        hash = hash * 17 + username.charAt(i);
    }
    int mod = hash % 10;
    return getKey(encryptKeys[mod], keyType);
}

有了这种不依赖redis缓存的方案,上面判断username是否在系统里存在的校验,似乎也可以省掉了。

集群环境下,不依赖分布式缓存还真玩不转

服务端程序在集群部署环境下,由于http请求是无状态的,就要实现 encryptKey 数据在各节点之间的共享。

我们改造一下上面的方案。

预先生成的一批10份 encryptKey , 按照 prefix+index 的命名方式,利用HSET key field value存储到redis的哈希表里。其中,hset的key是LOGIN_ENCRYPT_KEY,field依次分别是 PrivateKey0、PublicKey0、...、PrivateKey9、PublicKey9。

然后,重构 getEncryptKeyByUserName 方法↓↓

private String getEncryptKeyByUserName(String username, KeyType keyType) {
   
    int hash = 0;
    // 自定义哈希算法,防止被破解
    for (int i = 0; i < username.length(); i++) {
   
        hash = hash * 17 + username.charAt(i);
    }
    int mod = hash % 10;
    return redisUtil.hget(LOGIN_ENCRYPT_KEY_CACHE, keyType + mod);
}

这种改造,则有必要配合本地缓存前置校验username是否存在,以免非法请求频繁访问redis。同时,在获取redis时也搭配上本地缓存,则是锦上添花。↓↓(下面代码中的 LocalCacheUtil#getCache 是本地缓存工具,代码略)

private String getEncryptKeyByUserName(String username, KeyType) {
   
    int hash = 0;
    // 自定义哈希算法,防止被破解
    for (int i = 0; i < username.length(); i++) {
   
        hash = hash * 17 + username.charAt(i);
    }
    int mod = hash % 10;
    return LocalCacheUtil.getCache(cacheKey
                , 30 //本地缓存30秒
                , () -> {
   
                    return redisUtil.hget(LOGIN_ENCRYPT_KEY_CACHE, keyType + mod);
                });
}

不过,这种锦上添花所缓存的encryptKey,会存在redis缓存与本地缓存不一致的情况。即,在30s时间临界点,redis缓存到期失效,本地缓存还存在。这会导致登录时请求到集群里其他节点的login接口解密失败,从而引发登录bug。锦上添花反成画蛇添足了。

如何消除这种缓存不一致引发的bug呢?我们可以保守评估应用程序的生命周期,并将缓存TTL设为这个值。例如应用程序每隔2个月会因发版而重启,则设置redis缓存TTL为2month、本地缓存的有效期也为2month。

集群环境下,不依赖分布式缓存还真玩不转?

基于上面的方案,我们换一种思路,来替代用redis预先生成这批10份 encryptKey。这里,我们用应用配置,就是说,在应用配置里预置这批10份 encryptKey。——放在apollo配置中心吗?没必要,就放本地 application.yml 里就行。

这样一改,丸美应对集群模式下的数据共享问题!同时,也不需要去查库校验username是否存在了。

只要思想不滑坡,办法总比问题多!




至此,本文结束。本文重点阐释生成、获取加密key的优化办法。接口限流、接口增加签名机制、布隆过滤器等技术,不在讨论范围内。

目录
相关文章
|
安全 网络安全
QQ上不了,下载慢,先不要慌,先看看是不是华为防火墙上做了手脚
QQ上不了,下载慢,先不要慌,先看看是不是华为防火墙上做了手脚
162 0
|
4月前
|
安全 网络协议 关系型数据库
黑客红客,都用过这个工具扫端口!
黑客红客,都用过这个工具扫端口!
|
4月前
|
域名解析 缓存 网络协议
揭秘DNS协议:从'http://www.example.com'到IP地址的奇幻旅程,你不可不知的互联网幕后英雄!
【8月更文挑战第4天】在互联网的广袤空间里,每台设备都有唯一的IP地址,但记忆这些数字组合并不直观。因此,DNS(域名系统)作为关键桥梁出现,将易记的域名转换为IP地址。DNS协议工作于应用层,支持用户通过域名访问资源。DNS系统包含多级服务器,从根服务器到权威服务器,共同完成域名解析。查询过程始于客户端,经过递归或迭代查询,最终由权威服务器返回IP地址,使浏览器能加载目标网页。
158 12
|
7月前
|
数据采集 定位技术 Python
Python爬虫IP代理技巧,让你不再为IP封禁烦恼了! 
本文介绍了Python爬虫应对IP封禁的策略,包括使用代理IP隐藏真实IP、选择稳定且数量充足的代理IP服务商、建立代理IP池增加爬虫效率、设置合理抓取频率以及运用验证码识别技术。这些方法能提升爬虫的稳定性和效率,降低被封禁风险。
|
搜索推荐
什么是网络营销?做网络营销怎么用代理IP?
什么是网络营销?做网络营销怎么用代理IP?
|
数据采集 Prometheus 监控
自建稳定的HTTP代理池(妈妈再也不用担心被封了) | 实用教程
对于爬虫技术人员来说,自建HTTP代理池是提高爬虫效率和成功率的关键一环。今天,我们来聊聊怎么搭建稳定高效的自建HTTP代理池。
|
存储 移动开发 前端开发
如何开发趣味H5小游戏《在线抓娃娃机》
作为一个H5游戏开发爱好者,最近写了一款非常有趣的小游戏,即《在线抓娃娃机》([在线体验](http://claw.kjeek.com/))。在此总结分享一下开发经验,希望能够对大家有所启发。
如何开发趣味H5小游戏《在线抓娃娃机》
|
7月前
|
API 数据安全/隐私保护 对象存储
大咖与小白的日常:如何设置公司内网访问
小白实现了在线编辑Office文件,但是却发现是公网可访问的,数据安全无法保证,这可把她急坏了。如何做到只能在公司内部访问呢?
136 0
|
定位技术 SEO
【号外】-网站时光机
有些东西也只能留在时光机中了
1102 0
【号外】-网站时光机