SaaS安全设计与编码实践

  • 前言

    最近项目组花费很大力气进行安全整改,作为在线交谈微服务的模块owner,我进行了本模块的全量的安全分析,并对存在问题的地方进行了新的设计与编码落实。
    本文是我在安全处理过程后的一个总结,仅供参考。
    在线交谈微服务使用的框架包括平台部门的BDF以及Spring Boot。
  • 总览

    • 数据越权访问防御
    • 页面越权访问防御
    • 敏感数据保护
    • 日志打印安全
    • 文件上传下载安全
    • XSS防御
    • SQL注入防御
    • 命令注入防御
    • 不合法输入防御
    • token、网址等不能硬编码在代码中
    • 接口认证
    • 隐私声明与隐私数据管理
  • 数据越权访问防御

    • 解决方式

      • 租户数据操作beql里都带上tenant_id,用户数据操作带上用户的userId。
      • bs和beql都要配置authid,前台调的服务的authid不能是0(引入RestFilter来保证)。
      • 数据修改类接口只允许修改前台开放修改的字段,其他字段传过来则忽略。
    • 安全编码

      • 数据库操作带tenant_id,用户数据操作带userId。
          
            <beql name="queryChannelConfigBycodeName" authId="88001002001001" binding-rest="true">
            <content>
                
            content>
            <description>
                
            description>
        beql>
        
        如上是查询渠道配置的beql,可以看到beql里配置了租间Id,租间Id从上下文取,确保只能查询自己这个租间的数据。
        获取租间ID的代码如下:
         public Entity queryChannelConfigBycodeName(Map<String, Object> params) {
            String tenantSpaceId = BaseUtils.getTenantId();
            params.put("tenant_id", BaseUtils.getTenantId());
            params.put("partdbId", partdbLocalCache.getPartdbId(tenantSpaceId));
            List<Entity> channelConfigInfos = EntityUtil.queryEntities(ENTITY_NAME_CHANNELCONFIG,
                QUERY_CHANNELCONFIG_BYCODENAME, params);
            if ((channelConfigInfos != null) && (channelConfigInfos.size() != 0)) {
            Entity entity = channelConfigInfos.get(0);
            JSONObject workTimes = JSONObject.parseObject(entity.getString("workTime"));
            entity.put("workTime", workTimes);
                return entity;
            } else {
                log.error("ChannelConfigInfo is empty, pls check");
                return null;
            }
        }
        
      • 所有的bs和beql都配置了authid,且ccmessaging引入RestFilter,禁止前台调用authid是0的服务。
        <filter>
            <filter-name>restFilterfilter-name>
            <filter-class>com.huawei.servicecloud.base.filter.RestFilterfilter-class>
        filter>
        <filter-mapping>
            <filter-name>restFilterfilter-name>
            <url-pattern>/rest/ccmessaging/*url-pattern>
        filter-mapping>
        
      • 数据修改类接口,只允许修改前台开放修改的字段。beql里insert、update等只配置前台开放的字段。或者重新构造输入map。
        插入数据的时候tenant_id从上下文取而不是从输入取:
        public Map<String, String> saveChannelConfig(JSONObject request){
            // ...
            Map<String, Object> params = request;
            long opTime = DateTimeUtils.getCurrentUTCTimeMillis();
            params.put("createTime", opTime);
            String tenantSpaceId = BaseUtils.getTenantId();
            params.put("tenantSpaceId", tenantSpaceId);
            // ...
        }
        
        渠道配置新增和修改的时候都排除掉available字段。这个字段是租户挂起的时候用来标记渠道失效以拒绝新的来话。
        创建渠道配置,后台直接指定available为1,而不是读取用户输入。
        String workTime = workTimes.toJSONString();
        params.put("workTime", workTime);
        params.put("state", "0");
        params.put("available", "1");
        params.put("ccucsAddr", getCcucsServerInfo(tenantSpaceId));
        boolean isSuccess = configurationDao.saveChannelConfig(params);
        
        更新渠道配置,remove了available字段。
        if (params.get("available") != null) {
            params.remove("available");
        }
        boolean isSuccess = configurationDao.updateChannelConfig(params);
        
  • 页面越权访问防御

    • 解决方式

      • html, gadget, uslx都配一下authid,权限随菜单权限。
    • 安全编码

      • ccmessaging.web上配置service.cloud.lib.xml
        
        <authentications>
            
            <authentication authid="88001002001001" description="渠道配置权限">
                <resource url="/ccmessaging/page/chatconfigquery/chatconfig-query.html">resource>
                <resource url="/webclient/chat_client/pages/third_portal.html">resource>
            authentication>
            
            <authentication authid="88001002001002" description="常用语管理权限">
                <resource url="/ccmessaging/page/chatphrase/chatphrase.html">resource>
            authentication>
            
            <authentication authid="88001002001003" description="工作台配置权限">
                <resource url="/ccmessaging/page/chatholiday/chatholiday.html">resource>
            authentication>
            
            <authentication authid="88001002002001" description="个性化设置权限">
                <resource url="/ccmessaging/page/chatpersonalization/chatpersonalization.html">resource>
            authentication>
            
            <authentication authid="88001002002002" description="在线交谈工作台权限">
                <resource url="/ccmessaging/page/agentconsole/agentconsole.html">resource>
            authentication>
            
            <authentication authid="88001002002003" description="个性化常用语权限">
                <resource url="/ccmessaging/page/chatphraseservice/chatphraseservice.html">resource>
            authentication>
        authentications>
        
  • 敏感数据保护

    • 解决方式

      • 数据库存储时配置entity的加密。
      • redis存储时手动做加密。
      • 前台展示的时候用password控件展示,且不展示title。
    • 安全编码

      • Message.entity.xml的content配置了encrypt="true",ChannelConfig的verifyCode、appSecret配置了encrypt="true"
      • ConfigurationService里缓存channelConfig到redis时对verifyCode和appSecret加密
        @Override
        public ChannelConfig queryChannelConfigById(String channel, String id) {
            Map<String, Object> params = new HashMap<>(NUMBER_32);
            params.put("channel", channel);
            params.put("id", id);
            String key = generateUserSystemMessageKey(id);
            ChannelConfig channelConfig = cacheService.queryEntity(key, ChannelConfig.class);
            if (channelConfig.getId() == null) {
                Entity config = getChannelConfigById(params);
                if (config == null) {
                    channelConfig = null;
                } else {
                String appSecret = config.getString("appSecret");
                String verifyCode = config.getString("verifyCode");
                Encryption encryption = EncryptionFactory.getEncyption();
                String appSecretStr = null;
                String appSecretKey = null;
                String verifyCodeStr = null;
                String verifyCodeKey = null;
                if (appSecret != null) {
                    CipherInfo enc1 = encryption.encode(INITIAL, appSecret);
                    appSecretStr = enc1.getEncryptedPassword();
                    appSecretKey = enc1.getEncryptedKey();
                }
                if (verifyCode != null) {
                    CipherInfo enc1 = encryption.encode(INITIAL, verifyCode);
                    verifyCodeStr = enc1.getEncryptedPassword();
                    verifyCodeKey = enc1.getEncryptedKey();
                }
                try {
                    channelConfig = ConvertUtils.map2Bean(config, ChannelConfig.class);
                    channelConfig.setAppSecretKey(appSecretKey);
                    channelConfig.setAppSecret(appSecretStr);
                    channelConfig.setVerifyCode(verifyCodeStr);
                    channelConfig.setVerifyCodeKey(verifyCodeKey);
                    cacheService.saveEntity(key, channelConfig, ChannelConfig.class);
                        cacheService.setKeyExpire(key, CONFIG_CACHE_EXPIRE, TimeUnit.MINUTES);
                    } catch (IllegalAccessException e) {
                        log.error("IllegalAccessException", e);
                    } catch (InstantiationException e) {
                        log.error("InstantiationException", e);
                    } catch (IntrospectionException e) {
                        log.error("IntrospectionException", e);
                    } catch (InvocationTargetException e) {
                        log.error("InvocationTargetException", e);
                    }
                }
            }
            return channelConfig;
        }
        
      • 前台使用password展示verifycode和appKey,并且禁止了autocomplete和鼠标悬停title
         <bes:field label="AppSecret">
             <bes:input x-property="appSecret" validator="maxlength(80)"  type="password" autocomplete="off" id="appSecret">bes:input>
         bes:field>
         <bes:field label="{{'ccmessaging.chat.sumbitverification.AppSecret'|i18n}}" disable="true">
           <bes:input  x-property="$Model.AppSecret" type="password">bes:input>
         bes:field>
        
  • 日志打印安全

    • 解决思路

      • 去除对token、对话内容的打印日志。
      • log4j2.xml里配置敏感信息过滤,同时限制日志的数量和大小。
    • 安全编码

      • wechatadapter, webadapter和ccmessaging都没有打印对话内容。
      • wechatadapter, webadapter和ccmessaging使用统一的log4j2.xml配置,里面有敏感信息过滤配置和日志数量与大小限制。
  • 文件上传下载安全

    • 解决思路

      • 上传下载校验文件的类型和大小。
      • 文件存盘时文件名由app随机生成。
    • 安全编码

      • ccmessaging头像上传前后台均校验文件的类型和大小。
        前台校验代码:
        //图片类型验证
        verificationFile(file) {
           var fileTypes = [".jpg", ".png"];
           var filePath = file.value;
           //当括号里面的值为0、空字符、false 、null 、undefined的时候就相当于false
           if (filePath) {
              var isNext = false;
              var fileEnd = filePath.substring(filePath.indexOf(".")).toLocaleLowerCase();
              for (var i = 0; i < fileTypes.length; i++) {
                if (fileTypes[i] == fileEnd) {
                    isNext = true;
                    break;
                }
            }
            if (!isNext) {
                file.value = "";
                return false;
             }
             return true;
           } else {
            return false;
          }
        }
        
        //图片大小验证
        verificationFileSize(file) {
            var fileSize = 0;
            var fileMaxSize = 6144;//6M
            var filePath = file.value;
            if (filePath) {
                fileSize = file.files[0].size;
                var size = fileSize / 1024;
                if (size > fileMaxSize) {
                    file.value = "";
                    return false;
                } else if (size <= 0) {
                    file.value = "";
                    return false;
                }
                return true;
            } else {
                return false;
            }
        }
        
        后台校验代码:
        /**
        * 上传文件的最大大小,以字节为单位
        */
        private static final int MAX_FILE_SIZE_LIMIT = PropUtils.getInt("file.maxsize", 6 * 1024 * 1024);
        
        private void validateUploadedFile(String fileName, String filePath) {
          File uploadedFile = new File(filePath);
          if (uploadedFile.length() > MAX_FILE_SIZE_LIMIT) {
              throw new RuntimeException("upload file validate fail, the file size exceeds the limit 6MB!");
          }
          String[] formatList = fileName.split("\\.");
          String formatName = formatList[formatList.length -1];
          if(!formatName.equalsIgnoreCase("jpg") && !formatName.equalsIgnoreCase("png"))
          {
            throw new RuntimeException("upload file validate fail, Avatar image type error");
          }
        }
        
      • wechatadapter保存微信多媒体文件时,文件名由wechatadapter自行随机生成。或者通过USFileUtils.getFile(...)来防止传入的文件名有特殊含义的字符。
        ❌目前看***的实现还有问题。
        @Override
        public <T> ResponseEntity<?> process(WechatMessage wechatMessage, ChannelConfig configuration) {
            MaterialGetResult materialGetResult = wechatService.getMedia(wechatMessage.getMediaId(),
            configuration.getAppId(), configuration.getAppSecret());
            log.debug("ImageFilename{}", materialGetResult.getFilename());
            String fileName = materialGetResult.getFilename();
            String saveFilePath = UUID.randomUUID().toString() + fileService.getFileSavePath(fileName);
            try {
               FileUtils.copyInputStreamToFile(materialGetResult.getContent(),
                new File(saveFilePath));
               log.debug("saveFilePath {} ", saveFilePath);
            } catch (IOException e) {
                log.info("create file failed", e);
            }
            String convertFileName = System.getProperty("user.dir") + File.separator + "tmp" + File.separator
            + UUID.randomUUID().toString() + fileName.substring(0, fileName.length() - NUMBER_4) + "-thumbnail" + ".jpg";
            FileConvertor.generateThumb(saveFilePath, convertFileName);
            log.debug("convertFileName {} ", convertFileName);
            obsFileStorage.storage(convertFileName, "wechat/" + wechatMessage.getMediaId() + "-thumbnail" + ".jpg");
            obsFileStorage.storage(saveFilePath, "wechat/" + wechatMessage.getMediaId() + ".jpg");
            Optional.of(saveFilePath).ifPresent(path -> fileService.deleteFile(path));
            fileService.deleteFile(convertFileName);
        ...
        
  • XSS防御

    • 解决方式

      • 前台对用户输入编码,转换< > & " ' \ / ( )等特殊字符。
      • 后台字符校验。
    • 安全编码

      • gadget、uslx、html里通过bes components的x-property属性默认对输出进行编码。
        <bes:field label="{{'ccmessaging.chat.chatholiday.description'|i18n}}">
            <bes:input x-property="desc" validator="maxlength(20);specialStrValidate;">
            bes:input>
        bes:field>
        
      • 前台需要输出html标签的地方通过ng-bind-html="$Get('$sce').trustAsHtml(xxx)"对输出做处理。
        <pre class="hlds-media__body-abstract  hlds-user-textbubble" style="word-break:break-all;white-space:pre-wrap;" ng-if="talkdata.mediaType == 'TEXT'"
        ng-bind-html="$Get('$sce').trustAsHtml(talkdata.content)">
        pre>
        
      • 后台字符校验。
        后台特殊字符校验。
  • SQL注入防御

    • 解决方式

      • 平台的beql功能提供预编译beql的能力,可以防止SQL注入。
    • 安全编码

      • 平台的beql功能提供预编译beql的能力,可以防止SQL注入。
  • 命令注入防御

    • 安全设计

      • 校验命令里是否有黑名单字符,安全工具包已经有了对应的实现,直接用安全工具包。
    • 安全编码

      • 通过安全工具包防止命令注入。
        wechatadapter里调用ffmpeg命令执行缩略图生成和多媒体编码格式转换
         private static int exec(String... args) {
            int exitVal = NUMBER;
            String cmd = composeCmd(args);
            try {
                log.info("cmd {} ", cmd);
                Process process = UsOSUtils.runtimeExec(cmd);
                BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
                BufferedReader stderrReader = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
                String stdoutLine;
        ...
        
  • 不合法输入防御

    • 安全设计

      • 前后台都进行必填非必填校验。
      • 前后台都做下数据长度校验。
      • 前后台都做数据格式校验。
      • 前后台都对输入进行不合法字符校验。
      • 前后台都对输入做下枚举值校验。
    • 安全编码

      • 必填非必填校验。
        前台通过控件的required validator校验:
        <div>
            <bes:field label="{{'ccmessaging.chat.channelconfig.robotAccessCode'|i18n}}" required="true">
                <bes:input x-property="robotAccessCode" validator="required;maxlength(80);specialStrValidate;">
                 bes:input>
           bes:field>
        div>
        
        后台必填非必填校验:
        if (StringUtils.isEmpty(channelParamers.getString("robotAccessCode"))) {
            return constructResponse(PARAM_NULL, "robotAccessCode is null");
        }
        
      • 数据长度校验。
        前台通过控件的maxlength validator校验:
        <div style="background:#fff">
        <bes:field label="{{'ccmessaging.chat.chatmanage.sessionEndText'|i18n}}" required="true">
               <bes:input x-property="sessionEndText" validator="required;maxlength(80);specialStrValidate">bes:input>
        bes:field>
        div>
        
        后台对应校验字符长度:
        if (channelParamers.getBoolean("autoEndSession")) {
            int sessionEndText = channelParamers.getString("sessionEndText").length();
            if (sessionEndText > MAX_TEXT_SIZE_LIMIT) {
                return constructResponse(PARAM_ERROR, "sessionEndText Extra long");
            }
        }
        
      • 数据格式校验。
        渠道配置前台通过控件的range(start, end) validator校验会话超时时间是1-60。
        <bes:field label="{{'ccmessaging.chat.chatmanage.intervalTime'|i18n}}" required="true">
            <bes:input placeholder="1-60" x-property="intervalTime" validator="required;number;maxlength(3);range(1,60);specialStrValidate">bes:input>
        bes:field>
        
        后台校验:
        Pattern pattern = Pattern.compile("[0-9]*");
        if (StringUtils.isEmpty(channelParamers.getString("intervalTime"))) {
            return constructResponse(PARAM_NULL, "intervalTime is null");
        } else if (!pattern.matcher(channelParamers.getString("intervalTime")).matches()
                || (NUMBER_1 > Integer.parseInt(channelParamers.getString("intervalTime")))
                || (NUMBER_60 < Integer.parseInt(channelParamers.getString("intervalTime")))) {
            return constructResponse(PARAM_NULL, "intervalTime is not in range!");
        }
        
      • 不合法字符校验。
        不合法字符校验。
      • 枚举值校验。
        前台通过bes:select控件确保。
        后台校验:
        if (StringUtils.isEmpty(channelParamers.getString("robotGender"))) {
            return constructResponse(PARAM_NULL, "robotGender is null");
        } else if (!channelParamers.getString("robotGender").equals("1")
            && !channelParamers.getString("robotGender").equals("2")) {
            return constructResponse(PARAM_ERROR, "robotGender is wrong");
        }
        
  • 代码中不能存在网址、token等的硬编码

    • 安全设计

      • 网址、token、密钥、密码移到配置文件中。
    • 安全编码

      • 网址、token、密钥、密码移到配置文件中。
        配置文件:
        servicecloud.wechatadapter.Ffmpegpath=./ffmpeg/ffmpeg    
        servicecloud.wechatadapter.weixin.baseUrl = https://api.weixin.qq.com
        servicecloud.wechatadapter.weixin.mediaUrl = http://file.api.weixin.qq.com
        servicecloud.wechatadapter.weixin.mpUrl = https://mp.weixin.qq.com
        servicecloud.wechatadapter.weixin.mchUrl = https://api.mch.weixin.qq.com
        servicecloud.wechatadapter.weixin.openUrl = https://open.weixin.qq.com
        
        SpringBoot工程通过PropertySource读取:
        @SpringBootApplication(exclude = {RedisRepositoriesAutoConfiguration.class,
        RedisAutoConfiguration.class})
        @ServletComponentScan
        @Configuration
        @Log4j2
        @PropertySource(value = "file:./config/servicecloud.wechatadapter.conf.properties", ignoreResourceNotFound = true)
        public class Application  extends AbstractApplication implements EmbeddedServletContainerCustomizer{
        ...
        
        无法注入的地方通过PathMatchingResourcePatternResolver来读取:
        ...
        private static final String CONFIG_FILE_PATTERN = "file:./config/servicecloud.wechatadapter.conf.properties";
        private static Properties properties = new Properties();
        static {
            try {
                Resource[] resources = new PathMatchingResourcePatternResolver().getResources(CONFIG_FILE_PATTERN);
                for (Resource resource : resources) {
                    properties.load(resource.getInputStream());
                }
            } catch (IOException e) {
                log.error("error occurred while loading PropertyUtil.", e);
            }
        }
        ...
        
        BDF框架里通过公共的SystemPropertyUtil读取。
  • 接口认证

    • 安全设计

      • 操作员从前台调用的接口通过sum_token认证。
      • WEB客户端调用webadapter的收发无认证,webadater调ccmessaging的收发接口通过SIA认证,解析用户身份通过chat自己实现的accessToken认证
      • 微信客户端调用wechatadapter通过微信的timestamp、echostr、nonce、signature认证,wechatadapter调ccmessaging的收发接口通过SIA认证
      • open api通过api fabricak sk认证
    • 安全编码

      • 操作员从前台调用的接口通过sum_token认证。
        BDFDecodeTokenFilter做的。
      • WEB客户端调用webadapter的收发无认证,webadater调ccmessaging的收发接口通过SIA认证,解析用户身份通过chat自己实现的accessToken认证
        设计见:***。
        代码见webadapter与ccmessaging的TokenFilterTokenService
      • 微信客户端调用wechatadapter通过微信的timestamp、echostr、nonce、signature认证,wechatadapter调ccmessaging的收发接口通过SIA认证
        微信到wechatadapter的认证:
        @GetMapping(value = "/social/on/wechat/**")
        public void checkWxToken(HttpServletRequest request, HttpServletResponse response) throws Exception {
            // 随机字符串
            String echostr = request.getParameter("echostr");
            String url = request.getRequestURI();
            log.debug("url  {}", url);
            String id = url.substring(url.lastIndexOf("/") + 1);
            log.debug("id {} ", id);
            ChannelConfig configuration = chatService.getChatConfigurationById(id, WECHAT);
            String codeName = configuration.getCodeName();
            // 1)将token、timestamp、nonce三个参数进行字典序排序
            ArrayList<String> vadilations = new ArrayList<String>();
            // 时间戳
            String timestamp = request.getParameter("timestamp");
            // 随机数
            String nonce = request.getParameter("nonce");
            vadilations.add(configuration.getVerifyCode());
            vadilations.add(timestamp);
            vadilations.add(nonce);
            Collections.sort(vadilations);
            // 2)将三个参数字符串拼接成一个字符串进行sha1加密
            String result = DigestUtils.sha1Hex(vadilations.get(0) + vadilations.get(1) + vadilations.get(NUMBER_2));
            PrintWriter pw = response.getWriter();
            // 3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
            // signature:微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数
            String signature = request.getParameter("signature");
            if (result.equals(signature)) {
                log.info("Token check success!");
                chatService.markSuccess(id, codeName);
                pw.append(echostr);
            } else {
                log.info("Token check failed!");
                pw.append("fail");
            }
            pw.flush();
            pw.close();
        }
        
        wechatadapter调ccmessaging走SIA认证:
        public ResponseEntity<?> doGetTextRequest(JSONObject params, String url) throws Throwable {
           HttpHeaders headers = constructHttpHeader();
           setAuthToken(MICRO_SERVICE_NAME, headers);
           headers.add("Accept", "application/json, */*");
           headers.set(ACCEPT_LANGUAGE, CHINA_LANGUAGE);
           HttpEntity<Void> httpEntity = constructHttpEntity(headers, null);
           URI uri = constructUriWithQueryParam(params, url);
           RestTemplate restTemplate = RestTemplateBuilder.create();
           return restTemplate.exchange(uri, HttpMethod.GET, httpEntity, String.class);
        }
        
        private URI constructUriWithQueryParam(JSONObject params, String url) throws Throwable {
            URI uri = new URI(url);
            UriComponentsBuilder builder = UriComponentsBuilder.fromUri(uri);
            params.entrySet().iterator().forEachRemaining(entry -> builder.queryParam(entry.getKey(), entry.getValue()));
            return builder.build().encode().toUri();
        }
        
        private void setAuthToken(String serviceName, HttpHeaders headers) {
           headers.add(AUTH_TOKEN_NAME, apiAuthService.getToken(serviceName));
        }
        
  • 隐私声明

    • 安全设计

      • 隐私声明文档。
        提供隐私声明文档给资料同事。
      • H5客户端接入时在聊天区域展示隐私声明提示。
      • 微信客户端接入时推送隐私声明提示。
      • 隐私数据清理。个人交谈数据定期清理、随租户过期/失效清理;OBS上的多媒体数据设置90天的有效期。
    • 安全编码

      • 隐私声明文档。
        Chat个人数据说明V1.2.xlsx
      • H5客户端接入时展示隐私声明提示。
      • 微信接入成功后主动推送隐私声明提示。
        boolean isFirst = getParamFromRedis(wechatMessage.getFromUserName());
        log.debug("isFirst {} ", isFirst);
        if (isFirst) {
            // 调用创建会话的接口
            JSONObject createConnectResponse = chatService.createConnect(wechatMessage, configuration);
            log.debug("configurationId {} ", createConnectResponse.toJSONString());
            if ("0".equals(createConnectResponse.get("resultCode"))) {
                 String content = I18nUtils.getMessage("WECHAT.PRIVACY.STATEMENT", new Locale(configuration.getLanguage()));
                 chatService.sendFailMessagetoWechat(wechatMessage, configuration, content);
                 ScThreadPoolService.getInstance().execute(WxProcessorFatory.createProcess(wechatMessage, configuration));
        
      • 隐私数据清理。
        租户失效、到期时删除;ccmessaging定时任务检测超期会话并清除。
        租户失效、到期时删除:开放接口给ccprovision调用,ccmessaging app另起一个线程来清理。
        // UserServiceFacade.deleteAllccmessagingInfo
        // ...
        @Override
        public Map<String, String> deleteAllccmessagingInfo(String tenantSpaceId) {
            if (!agentPersonalizationDao.deleteAgentPersonalization(tenantSpaceId)) {
                return constructResponse(RESULTCODE_FAILED,"delete AgentPersonalizationInfo fail");
            }
            if (!configurationDao.deleteAgentPersonalization(tenantSpaceId)) {
                return constructResponse(RESULTCODE_FAILED,"delete ChannelConfigInfo fail");
            }
            if (!chatPhraseDao.deleteChatPhraseByTenantSpaceId(tenantSpaceId)) {
                return constructResponse(RESULTCODE_FAILED,"delete chatPhraseInfo fail");
            }
            if (!chatPhraseTypeDao.deleteChatPhraseTypeByTenantSpaceId(tenantSpaceId)) {
                return constructResponse(RESULTCODE_FAILED,"delete chatPhraseTypeInfo fail");
            }
            if (!chatHolidayDao.deleteChatHolidayByTenantSpaceId(tenantSpaceId)) {
                return constructResponse(RESULTCODE_FAILED,"delete chatHolidayInfo fail");
            }
            if (!messageDao.deleteChatMessageByTenantSpaceId(tenantSpaceId)) {
                return constructResponse(RESULTCODE_FAILED,"delete messageInfo fail");
            }
            if (!sessionRecordDao.deleteSessionRecordByTenantSpaceId(tenantSpaceId)) {
                return constructResponse(RESULTCODE_FAILED,"delete sessionrecord fail");
            }
            return constructResponse(RESULTCODE_SUCCESS, "delete AllccmessagingInfo success");
        }
        
        定时清理超期数据:
        // StartTask里启动
        // ...
        /**
         * 删除过期租间数据
         *
         * @return 返回值
         */
        @Override
        public boolean deleteExpiredTenantInfo() {
            // 查询所有租户列表
            JSONArray tenantRecordList = queryTenantRecordTimeList();
        
            if (CommonUtil.isEmpty(tenantRecordList)) {
                log.error("Not found any tenant info when delete expired tenant info.");
                return false;
            }
        
            Date currentDate = new Date();
            JSONObject params = new JSONObject();
            for (int index = 0;index < tenantRecordList.size();index++) {
                JSONObject tenantRecord = tenantRecordList.getJSONObject(index);
                if (!verifyParams(tenantRecord)) {
                    continue;
                }
                params.put("tenant_id", tenantRecord.get("tenant_id"));
                params.put("partdbId", tenantRecord.get("partdbId"));;
                JSONObject vcallCenterMap = tenantRecord.getJSONObject("VCallCenter");
                int expiredTime = Integer.parseInt(vcallCenterMap.getString("recordtime"));
                Date date = DateUtils.addMonths(currentDate, -expiredTime);
                Timestamp createTime = new Timestamp(date.getTime());
                params.put("createTime", createTime);
        
                messageDao.deleteExpiredChatMessage(params);
                sessionRecordDao.deleteExpiredSessionRecord(params);
            }
        
            return true;
        }
        /**
         * 向PROVISION查询租户ccInstId
         *
         * @return 返回值
         */
        @Override
        public JSONArray queryTenantRecordTimeList() {
            log.debug("queryTenantRecordTimeList begin");
            ApiAuthService apiAuthService = (ApiAuthService) ContextRegistry.getContextHolder()
                .getBean("base_ApiAuthService");
            String accesstoken = apiAuthService.getToken("ServiceCloudProvision");
            HttpHeaders headers = new HttpHeaders();
            headers.add("Accept", "application/json, */*");
            headers.add("Content-Type", "application/json");
            headers.add("X-Access-Token", accesstoken);
            RestTemplate restTemplate = RestTemplateBuilder.create();
        
            JSONObject body = new JSONObject();
            try {
                ResponseEntity<String> respEntity = restTemplate.exchange(QUERY_TENANT_PARTDBID_INFO, HttpMethod.POST,
                        new HttpEntity<Map<String,Object>>(body, headers), String.class);
                if (HttpStatus.OK.equals(respEntity.getStatusCode())) {
                    JSONArray tenantRecordList = JSONArray.parseArray(respEntity.getBody());
                    return tenantRecordList;
                }
            } catch (RestClientException e) {
                log.error("exception in v1/tenantspace/queryRecordTimeInfo");
            }
            return null;
        }
        
      • OBS上的多媒体数据设置90天的有效期。
        public void storage(String filePath, String objectKey) {
            PutObjectRequest putObjectRequest = new PutObjectRequest(obsBucketName, objectKey);
            putObjectRequest.setFile(UsFileUtils.getFile(filePath));
            putObjectRequest.setExpires(Integer.parseInt(expireTime));
            try {
                getObsClient().putObject(putObjectRequest);
            } catch (Exception e) {
                log.info("storage failed", e);
            }
        }
        

你可能感兴趣的:(security)