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

引言

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/209687.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SQL命令---修改数据库的编码

介绍 使用sql命令修改数据库的编码&#xff0c;修改为utf8mb4编码。 命令 alter database 数据库名称 default character set utf8mb4;

垃圾收集算法和各种垃圾收集器的实现

深入理解Jvm虚拟机第三章 二、对象已死&#xff1f;3.2.1 引用计数算法3.2.2 可达性分析算法3.2.3 再谈引用3.2.4 生存还是死亡3.2.5 回收方法区 三、垃圾收集算法3.3.1 分代收集理论3.3.2 标记-清除算法3.3.3 标记-复制算法3.3.4 标记-整理算法 四、HotSpot的算法细节实现3.4.…

单片机中的printf思考

问题: 1. printf自带的库编译出来的大小比较大(flash吃紧) 2. printf是一个不定长参数, 意味着函数无法知道传入的长度. 解决这个问题有2中方法:1.设置足够大小的数组作为参数存储; 2. 使用动态内存分配的方式来做(应该使用的是这个方式).(内存吃紧) 问题解释: 1. 之前写裸…

C# WPF上位机开发(串口界面设计)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 如果只是把上位机看成是纯软件开发&#xff0c;本身不和硬件打交道的话&#xff0c;那么这就把上位机的操作范围给限定死了。事实上&#xff0c;上…

多线程的使用

进程与线程 进程&#xff1a; 1、进程是指运行中的程序&#xff0c;比如我们使用QQ&#xff0c;就启动了一个进程&#xff0c;操作系统就会为该进程分配内存空间。当我们使用微信&#xff0c;又启动了一个进程,操作系统将为其分配新的内存空间。 2、进程是程序的一次执行过程…

数据库系统概论期末经典大题讲解(范式提升、求闭包、求主码)

上一次我们介绍了数据库中关系代数查询&#xff0c;从选择、投影到连接等操作符&#xff0c;探索了数据库查询 大家可以移步我的文章&#xff1a;数据库系统概论期末经典大题讲解&#xff08;用关系代数进行查询&#xff09;-CSDN博客 今天&#xff0c;我们将继续沿着数据库系统…

《python每天一小段》--12 数据可视化《1》

欢迎阅读《Python每天一小段》系列&#xff01;在本篇中&#xff0c;将使用Python Matplotlib实现数据可视化的简单图形。 一、概念 Matplotlib是一个流行的Python数据可视化库&#xff0c;它提供了丰富的绘图功能&#xff0c;可以创建各种类型的图表&#xff0c;包括折线图、…

Spring框架学习:Bean生命周期

目录 SpringBean的生命周期 Bean实例属性填充 三级缓存 常用的Aware接口 Spring IoC容器实例化Bean总结 SpringBean的生命周期 Spring Bean的生命周期是从 Bean 实例化之后&#xff0c;即通过反射创建出对象之后&#xff0c;到Bean成为一个完整对象&#xff0c;最终存储到…

【MyBatis系列】MyBatis字符串问题

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

SpringBoot + Spring Cloud Alibaba + Nacos实现服务管理

1、参考文档 Spring Cloud Alibaba参考文档 https://spring-cloud-alibaba-group.github.io/github-pages/hoxton/zh-cn/index.html Spring Cloud Alibaba官方文档 https://github.com/alibaba/spring-cloud-alibaba/wiki/ 2、引入 Alibaba 依赖 每个 SpringBoot 都有对应的…

css中2D和3D的区别

CSS中2D和3D的主要区别在于&#xff1a; 维度不同&#xff1a;2D是二维平面&#xff0c;3D是三维空间。可视角度不同&#xff1a;2D只能从一个平面角度看&#xff0c;而3D可以在多个角度上观察。技术难度不同&#xff1a;3D效果需要更复杂的技术支持&#xff0c;如矩阵变换&am…

javascript实现Stack(栈)数据结构

上一篇文章我们理解了List这种数据结构&#xff0c;知道了它的特点和一些使用场景&#xff0c;这篇文章我们就来看一下栈这种数据结构&#xff0c;这里的栈可不是客栈哦&#xff0c;哈哈 栈其实和List非常像&#xff0c;使用javascript实现都是基于数组来实现 尝试理解Stack …

6种常见的JS模块打包器

前言 JS模块打包器是一种工具&#xff0c;它可以将多个JS文件或模块合并成一个或多个输出文件&#xff0c;以便在浏览器或其他环境中使用。 JS模块打包器的作用有&#xff1a; 优化代码&#xff1a;通过压缩、混淆、删除无用代码等方式&#xff0c;减少代码的体积和复杂度&…

windows系统和虚拟机上ubuntu系统通过虚拟串口进行通信

本文的目的是实现windows系统和虚拟机上安装的ubuntu通过串口进行通信。为了直观观测串口收发数据的内容&#xff0c;需要在windows系统和ubuntu系统使用串口助手来进行监听。windows系统端用的监听工具是串口助手SSCOM&#xff0c;ubuntu系统端使用的串口助手是CuteCom。 ubu…

OpenCL学习笔记(一)开发环境搭建(win10+vs2019)

前言 异构编程开发&#xff0c;在高性能编程中有重要的&#xff0c;笔者本次只简单介绍下&#xff0c;如何搭建简单的开发环境&#xff0c;可以供有需要的小伙伴们开发测试使用 一、获取opencl的sdk库 1.使用cuda库 若本机有Nvidia的显卡&#xff0c;在安装cuda库后&#x…

如何提高大模型在超长上下文的表现?Claude实验表明加一句prompt立即提升效果~

本文来自DataLearnerAI官方网站&#xff1a;如何提高大模型在超长上下文的表现&#xff1f;Claude实验表明加一句prompt立即提升效果~ | 数据学习者官方网站(Datalearner)https://www.datalearner.com/blog/1051701947131881 Claude 2.1版本的模型上下文长度最高拓展到200K&am…

【Flink系列四】Window及Watermark

3.1、window 在 Flink 中 Window 可以将无限流切分成有限流&#xff0c;是处理有限流的核心组件&#xff0c;现在 Flink 中 Window 可以是时间驱动的&#xff08;Time Window&#xff09;&#xff0c;也可以是数据驱动的&#xff08;Count Window&#xff09;。 Flink中的窗口…

c jpeg YUV图片帧分割成 8*8 块 ,与逆向把8*8还原为帧

1. 正向分割为若干8*8 块 下面的程序为通用程序&#xff0c;可以分割任意块 #include <stdlib.h> #include <string.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h>…

如果微软20年前开发.net core,JAVA会不会和IE一样倒下了

可以跨平台&#xff0c;大量类库&#xff0c;微软亲自操刀&#xff0c;性能一流&#xff0c;因为没有做跨平台&#xff0c;.NET被 python,javascript等抢了一半以上市场。 如果微软早早的推出类似.net core这样的跨平台语言&#xff0c;.net程序猿还会出在这样的尴尬局面吗众所…

Java基础-开发流程以及HelloWorld程序

目录 1. Java的开发流程2. HelloWorld 1. Java的开发流程 开发Java程序&#xff0c;需要三个步骤&#xff1a;编写代码&#xff0c;编译代码&#xff0c;运行代码 2. HelloWorld 编写代码 public class HelloWorld {public static void main(String[] args) {System.out.pri…