浅析数据产品中数据源密码的安全管理设计与实现

概述

最近在全职负责一款数据产品,从后端研发,到前端研发(半吊子),到测试验证(近乎全职),到产品功能交互等设计(又是半吊子),几乎只有我一个人。之所以是这样的局面,是因为被具备中国特色社会主义的公司老板压榨着。

设计

功能浅析

世面上的数据产品,很多很多。其中有不少产品,都大体上可以理解为,基于数据源,提供数据处理,数据监控,推送与可视化的产品。我负责的就是这样一款产品。

显然数据源是一切功能的基础。

界面设计粗略一窥,左侧菜单栏如下图,其中数据源是核心:
在这里插入图片描述
数据源列表页:
在这里插入图片描述
某个数据源的操作功能如下:
在这里插入图片描述
新增,查看,编辑数据源,共用同一个模态框,下图中mongo数据源类型其实也是jdbc中一种:
在这里插入图片描述
数据源类型有:
在这里插入图片描述
其中:

  1. 分享与审批是与权限相关的;
  2. 查看,编辑,删除,没啥好说;
  3. 复制,提供快捷参考复制其他数据源的功能。

需要注意的是,为了避免数据源的反复创建,设计一条数据源唯一性规则:url + username + password。

密码加密

本文的核心是密码的安全管理。常规的列表接口都是原样存储数据到数据表,然后近乎原样以列表形式返回,前端展示一个表格组件。

绝大多数数据产品里,前端控件都会对密码进行mask处理,但对于稍微具备一点点Chrome开发知识的人来说,都知道开发者模式,即打开F12(或右键,检查)点击Network,查看请求的RequestBody和responseBody,responseBody里面即是数据源密码,这就意味着密码有泄露的风险。

解决方案,不难想到加密处理:

  1. 加密操作由后端来实现,对返回给前端的数据,过滤password字段,加密返回给前端;
  2. 后续需要前后端交互的场景,如上图中点击联通测试,需要前端传输用户名,密码等信息给后端;
    1. 如果是新增数据源,传输给后端的密码是原始密码;
    2. 如果是编辑数据源,但是不更新密码,而是更新数据源名称等其他信息,则传输给后端的应该是加密后的密码;
    3. 如果是编辑数据源更新密码,则传输给后端的是原始密码。

所以问题来了:后端无法判断编辑更新情况下,前端传过来的密码是明文还是密文,需不需要解密。

加密问题

对于如上加解密问题。稍加思考后,想到一个可行的方案,有一定的折衷退步思考意味。方案如下:列表页返回的是加密后的密码。如果是查看或编辑某个数据源,则前端调用后端新增提供的获取数据源详情接口,此接口返回的数据是明文,也就是常规接口设计,前后端交互流程。

值得一提的是:在我们这个数据产品中,查看或编辑某个数据源这个权限并不是暴露出来的,需要申请并审批通过之后方可生效。满足安全性要求。

也就是说,用户可以看到数据源列表页概要信息,名称,创建人,创建时间,修改人,修改时间,负责人,最机密的信息是连接串,通过Chrome开发者模式,可以看到数据源用户名,但是看到的密码是加密处理的。

总结:数据库存储的数据是明文;交互流程:

  1. 列表页展示时,后端加密处理,控制台只能看到;
  2. 新增数据源时,输入明文,保存明文;
  3. 数据源查看时,无需加密处理,
  4. 数据源编辑时,无需加密处理,

优化

数据库存储加密?

代码片段

PageHelper.startPage(params.getInteger("pageNo"), params.getInteger("pageSize"));
if (StringUtils.isNotBlank(params.getString("sortField"))) {
    params.put("sortField", StringUtil.camelCaseToUnderscore(params.getString("sortField")));
}
List<DashboardDatasource> lists = datasourceMapper.findDataSourceByParams(params);
for (DashboardDatasource item : lists) {
    item.setConnectionStr(this.getConnectionStr(item.getConfig()));
    JSONObject config = JSONObject.parseObject(item.getConfig());
    // 存在密码时才加密(Redis无密码)
    if (StringUtils.isNotBlank(config.getString("password"))) {
        config.put("password", DecodeUtil.encrypt(config.getString("password")));
    }
    item.setConfig(config.toJSONString());
}
public String testConnection(JSONObject dataSource) {
    try {
        /*JSONObject config = dataSource.getJSONObject("config");
        // 新增数据源时无需解密
        if (!dataSource.getString("modalType").equals("add")) {
            // 解密一定要trim()
            config.put("password", DecodeUtil.desEncrypt(config.getString("password")).trim());
        }*/
        dataProvider.checkConnection();
        return JSONObject.toJSONString(ServiceUtil.returnSuccess());
    } catch (Exception e) {
        logger.error("datasource test error", e);
        return JSONObject.toJSONString(ServiceUtil.returnError(e.getMessage()));
    }
}

参考

Java String加解密踩坑

posted @ 2022-05-11 22:56  johnny233  阅读(20)  评论(0编辑  收藏  举报  来源