buguge - Keep it simple,stupid

知识就是力量,但更重要的,是运用知识的能力why buguge?

导航

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

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

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

这个 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传任意值,包括空值,都可以获取到一个秘钥。

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



我们来看看怎么修复、完善这个接口的生成加密key的逻辑。

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

判空吗?

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

除了判空,还要判断参数的合法性。对于明显错误的传值,如username是一段1024的大文本,直接pass掉。再如,username不符合系统的用户名设置规则,诸如包含了符号(如果允许@符号,那排除@符号),也直接pass掉。

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

其次,再来说如何更好地生成、获取encryptKey(加密key)

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

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

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

随机从这10个 encryptKey 里直接取一个返回,不香吗?当然不行了,因为后面的login接口里,得用同样的 encryptKey 来解密用户密码。就是说,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(封装得不彻底)!

要封装的,应该是↓↓

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

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

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

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

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

预先生成的一批10个 encryptKey , 按照 prefix+index 的命名方式,存储到redis里。缓存key如 LOGIN_ENCRYPT_KEY_0、LOGIN_ENCRYPT_KEY_1、...、LOGIN_ENCRYPT_KEY_9。

然后,重构 getEncryptKeyByUserName 方法↓↓

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

这种改造,则有必要保留判断username是否在系统里存在的校验,以免非法请求频繁访问redis。同时,如能再搭配上本地缓存,则是锦上添花。↓↓(下面代码中的 LocalCacheUtil#getCache 是本地缓存工具,实现代码略)

private String getEncryptKeyByUserName(String username) {
    int hash = 0;
    // 自定义哈希算法,防止被破解
    for (int i = 0; i < username.length(); i++) {
        hash = hash * 17 + username.charAt(i);
    }
    int mod = hash % 10;
    String cacheKey = LOGIN_ENCRYPT_KEY_CACHE + mod;
    return LocalCacheUtil.getCache(cacheKey
                , TimeUnit.HOURS.toSeconds(1)
                , () -> {
                    return redisUtil.get(cacheKey);
                });
}




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

posted on 2024-11-11 12:46  buguge  阅读(16)  评论(0编辑  收藏  举报