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

引言

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

你可能感兴趣的:(其他,时间)