首页 > 其他分享 >维护真实时间:应对系统时间篡改的技巧

维护真实时间:应对系统时间篡改的技巧

时间:2023-12-23 23:36:56浏览次数:33  
标签:技巧 isSpite 系统 DateTime 获取 CacheUtil 时间 篡改


引言

在App使用中,由于系统时间用户可以随意更改,在某些特殊情况下会导致获取到的系统时间不正确问题。本篇代码使用dart语言进行相关描述。

1.问题分析:
手机系统时间 ≠ 真实时间,当我们做一些需要对时间精度和准确性要求较高的软件时,如果只通过调用系统API,获取到的时间不一定是真实的,那么就需要我们单独去维护一个真实的时间,下面主要分析了连网情况下和断网情况下两种时间维护方案。

2.方案一:连网情况下获取网络时间:
既然连网了,从网络获取时间就行了,获取到的时间与本地保存的时间同步,当然响应数据有一定的延时,如果想要特别精确再单独加上响应时间就行了。
Dart代码示例如下:

///获取网络时间
  Future<DateTime?> _getNetworkTime() async {
    try {
      final response = await HttpUtil.getInstance().get(apiUrl);

      if (response.statusCode == 200) {
        final Map<String, dynamic> responseData = json.decode(response.data);
        final String dateTimeString = responseData['datetime'];
        final DateTime networkTime = DateTime.parse(dateTimeString).toLocal();
        return networkTime;
      } else {
        print('Handle the response error here');
        return null;
      }
    } catch (e) {
      print('Handle any exceptions that may occur  $e');
      return null;
    }
  }

3.方案二:断网情况下本地时间维护:(重点)
既然断网了,方案一就一点用没了,此时有两种方法获取时间,一种是调用系统api,一种是获取本地维护的时间,我们知道系统时间是可以修改的,所以你获取系统时间的话,得到的不一定是正确的时间,那么只能从本地我们自己维护的时间去拿,那么问题来了本地时间要怎么去维护呢?
我们可以在应用启动的时候起个定时器1秒钟循环一次,对本地时间进行累加,不受系统时间影响,前提是本地第一次保存是时间必须是真实的时间系统时间不可靠,那么就把第一次从网络上获取的时间去保存下来供后续使用。需要注意的是这种情况下难免会有误差!

time2 = Timer.periodic(const Duration(seconds: 1), (timer) {
      if (!isSpite) {
        CacheUtil.saveRealTime(CacheUtil.getLastRealTime() + 1000);
      }
    });

上面的代码是定时器一秒钟循环一次,每次对上一次保存的时间进行累加,也就是我们自己维护的时间,这里的1000写死了,细微误差暂不考虑在内。
当然这只是开始,如果在你应用使用中突然断网了,并且系统时间还被修改了,上面方法还可以维护一个真实的时间,那么如果断网后,你的程序退出了,而且系统时间还被修改了,该去怎么去保证时间的真实性呢?

4.逻辑设计:
这里我使用的方法是,在上面saveRealTime 中我们每次保存的不单是真实时间,还保存的一个系统时间的毫秒数。
///保存一次真实时间及一次系统时间(两次时间不一定相同)

static void saveRealTime(int timeMillis) {
    SpUtil.putInt('timeMillis', timeMillis);
    SpUtil.putInt('systemTimeMillis', DateTime.now().millisecondsSinceEpoch);
  }

当程序退出时,systemTimeMillis 保存了上次退出时的系统时间,当下次启动应用时,获取当前系统时间,与上次保存的系统时间之差就是这段空缺的时间段,然后继续与本地的真实时间进行累加以达到获取维护真实时间的目的。

if (currTime - CacheUtil.getLastSystemTime() > 2000) {
          //当前系统时间 大于上次保存的时间(断网退出app时 保存的一次时间)
          //此处有个问题,用于修改时间大于当前时间(此时不用考虑)
          isSpite = false;
          var timeDiff = currTime - CacheUtil.getLastSystemTime();
          CacheUtil.saveRealTime(CacheUtil.getLastRealTime() + timeDiff);
        }

上面方法只适用 系统时间被修改一次的情况,至于为什么要大于2000毫秒,是因为上面还有个一秒循环一次的定时器在维护时间,只有程序退出了这两数之差才可能大于2000,因为这两个定时器是并行的,为了避免小概率事件大于1000有时可能会有问题!
当用户在程序退出后再断网,然后修改系统时间,此时上述方法就无力回天了,只能禁用与时间相关的所有功能,待时间恢复正常(连网…)再恢复相关功能使用。

if (currTime - CacheUtil.getLastSystemTime() < 0) {
          //当前时间小于上次保存的时间,代表时间被恶意修改,禁用所有与时间相关的功能,
          // 直到连网或者修改系统时间大于当前时间
          isSpite = true;
        }

下面是完整代码示例:

import 'dart:async';
import 'dart:convert';

import '../../http/httpUtil.dart';
import '../cache.dart';

///
/// 此类内部维护一个真实时间,由于系统时间可被修改,所以系统时间仅供参考
///
/// 用来获取当前真实时间
///
const String apiUrl = 'https://worldtimeapi.org/api/ip';

class RealTimeUtil {
  late Timer time;
  late Timer time2;

  //是否可以使用系统时间
  bool canUseSystem = false;

  //时间是否被恶意篡改
  bool isSpite = false;

  // 使用静态变量_instance来存储单例实例
  static RealTimeUtil? _instance;

  // 私有构造函数,防止外部实例化
  RealTimeUtil._() {
    init();
  }

  // 工厂构造函数,用于返回单例实例
  factory RealTimeUtil() {
    // 如果实例尚未初始化,则创建一个新实例
    _instance ??= RealTimeUtil._();
    return _instance!;
  }

  void init() async {
    _task();
    time = Timer.periodic(const Duration(seconds: 10), (timer) {
      _task();
    });
    time2 = Timer.periodic(const Duration(seconds: 1), (timer) {
      if (!isSpite) {
        CacheUtil.saveRealTime(CacheUtil.getLastRealTime() + 1000);
      }
    });
  }

  _task() {
    _getNetworkTime().then((DateTime? dateTime) {
      if (dateTime != null) {
        //获取网络时间成功
        isSpite = false;
        if ((dateTime.millisecondsSinceEpoch - DateTime.now().millisecondsSinceEpoch).abs() > 5000) {
          //网络时间大于系统时间5s 代表本地时间不正确, 5s是个阈值,视实际情况而定
          canUseSystem = false;
          CacheUtil.saveRealTime(dateTime.millisecondsSinceEpoch);
        } else {
          canUseSystem = true;
          CacheUtil.saveRealTime(DateTime.now().millisecondsSinceEpoch);
        }
      } else {
        //获取网络时间失败
        var currTime = DateTime.now().millisecondsSinceEpoch;

        print(
            'test 获取网络时间失败  currTime  = ${currTime - CacheUtil.getLastSystemTime()}  isSpite = $isSpite');

        if (currTime - CacheUtil.getLastSystemTime() > 2000) {
          //当前系统时间 大于上次保存的时间(断网退出app时 保存的一次时间)
          //此处有个问题,用于修改时间大于当前时间(此时不用考虑)
          isSpite = false;
          var timeDiff = currTime - CacheUtil.getLastSystemTime();
          CacheUtil.saveRealTime(CacheUtil.getLastRealTime() + timeDiff);
        } else if (currTime - CacheUtil.getLastSystemTime() < 0) {
          //当前时间小于上次保存的时间,代表时间被恶意修改,禁用所有与时间相关的功能,
          // 直到连网或者修改系统时间大于当前时间
          isSpite = true;
        }
      }
    });
  }

  ///获取真实时间的唯一路径
  DateTime getRealTime() {
    int lastRealTimeMillis = CacheUtil.getLastRealTime();

    // 创建一个DateTime对象,需要将时间戳转换为DateTime
    DateTime realTime = DateTime.fromMillisecondsSinceEpoch(lastRealTimeMillis);
    return realTime;
  }

  ///获取网络时间
  Future<DateTime?> _getNetworkTime() async {
    try {
      final response = await HttpUtil.getInstance().get(apiUrl);

      if (response.statusCode == 200) {
        final Map<String, dynamic> responseData = json.decode(response.data);
        final String dateTimeString = responseData['datetime'];
        final DateTime networkTime = DateTime.parse(dateTimeString).toLocal();
        return networkTime;
      } else {
        print('Handle the response error here');
        return null;
      }
    } catch (e) {
      print('Handle any exceptions that may occur  $e');
      return null;
    }
  }
}

5.总结:
当然上述方法,是以系统时间为参考进行的补救措施,如果使用的是系统启动时间,而不是系统时间为参考,上面问题就迎刃而解了。这里没有尝试该方法,我主要是给大家提供一个思路,而且博主能力有限,很多细节可能未考虑在内,大家有好的方法也可以提出来,共同学习,一起进步。


标签:技巧,isSpite,系统,DateTime,获取,CacheUtil,时间,篡改
From: https://blog.51cto.com/xaye/8947863

相关文章

  • Shiro 框架中如何更新Redis的超时登录时间?
    在Shiro框架中,可以通过实现SessionDAO接口来将会话信息保存到Redis中,并且可以通过实现SessionValidationScheduler接口来定期检查会话是否过期。因此,要更新Redis中的超时登录时间,可以按照以下步骤进行操作:实现SessionDAO接口,将会话信息保存到Redis中。在实现SessionDAO接口时,可以使......
  • linux命令find使用技巧汇总
    linux命令find是一个强大的工具,它可以在指定的目录下查找文件和目录,还可以根据不同的条件进行过滤和限制,甚至可以对查找到的文件执行操作。......
  • 几种高级的git技巧
    加快gitcommit的速度使用git保存代码快照时,通常的流程是:gitadd.gitcommit-m"whatwasthat"gitpushoriginmaster但是commit命令的-a选项可以省略掉gitadd这一步,即:gitcommit-m"whatwasthat"-agitpushoriginmaster并且,利用git提供的别......
  • 【Linux】正则匹配SQL里面的时间 TIMESTAMP
    在使用plsql或者dbeaver的insertsql导出的时候通常日期格式的会导出为以下形式,我们通常将这些日期需要更新为sysdate或者to_char(sysdate,'YYYYMMDD')的形式,此时可以使用正则匹配来替换,以下列举了常见的两种时间场景:1.匹配TIMESTAMP'2023-12-2318:00:01:000000'通常创建时......
  • 原地堆化技巧
    将数组以\(O(n)\)的时间复杂度和\(O(1)\)的空间复杂度构造为堆的trick。想象我们把数组随意地填充进一棵完全二叉树(尚不满足堆的性质),然后通过交换节点等操作把二叉树变成堆。因为完全二叉树的节点个数性质,我们直接从\(\dfrac{n}{2}\)到\(1\)倒序遍历(相当于从下到上遍历......
  • Java第十三课_常用时间类和集合
    1.常用时间类Calendar类publicstaticvoidmain(String[]args){//JDK1.1开始//Calendar类是一个抽象类,//它提供了在特定时刻和一组日历字段(如YEAR、MONTH、DAY_of_MONTH、HOUR等)之间进行转换的方法,以及操作日历字段(例如获取下一周的日期......
  • “做开源犹如养护花朵,花开需要时间”|2023年度总结
    你好,我是Kagol。2023年已经接近尾声,OpenTiny从一颗种子......
  • R语言经济学:动态模型平均(DMA)、动态模型选择(DMS)预测原油价格时间序列
    原文链接:http://tecdat.cn/?p=22458 原文出处:拓端数据部落公众号 简介本文提供了一个经济案例。着重于原油市场的例子。简要地提供了在经济学中使用模型平均和贝叶斯方法的论据,使用了动态模型平均法(DMA),并与ARIMA、TVP等方法进行比较。希望对经济和金融领域的从业人员和研究......
  • 【收藏】法律人办案必备检索网站最新汇总!附检索技巧
    为什么要进行法律检索?无论你擅长的是做诉讼还是非诉讼业务,法律检索都是必备技能之一。只有做好法律检索才能制定出更加完备的策略报告,才能提供更加充实、可行、准确的方案。一、数据库检索1、alpha数据库https://www.icourt.cc 已经用了3年的大数据库,听说最近降价了。有相当多......
  • 在线时间戳是什么?
    在线时间戳是基于国际标准结合PKI密码技术及数字签名技术,对电子数据产生的精确时间进行固证,为电子数据提供时间证明的一种在线服务。时间戳技术使用数字签名技术对包含原始文件信息、签名参数、签名时间等信息构成的对象进行数字签名。1、时间戳有什么作用?在电子合同签署、电子签名......