homebrew

[toc]

一、homebrew库的下载、更新

Homebrew 的安装见 终端常用命令

1、homebrew库的下载、更新

1.1、库的下载

创建以 homebrew-开头的工程,如https://github.com/dvlpCI/homebrew-tools

这样执行 brew tap 的时候,就可以只输入brew tap dvlpCI/tools,其会自动映射到dvlpCI/homebrew-tools

image-20230415222441966

如截图,你的远程库索引就会放到本地的以下路径

/usr/local/Homebrew/Library/Taps/dvlpci/homebrew-tools

image-20230415230921079

1.2、库的更新

使用以下命令更新

1
2
3
brew update

brew upgrade tools

更新后

image-20230416215241520

2、homebrew库中的命令

2.1、brew install

①、brew install的使用

brew install可安装上述 homebrew-tool中的子库

如可执行 brew install brew 也可以 brew helloworld

②、brew install 安装到目录

brew install软件会安装在/usr/local/Cellar/bjf/

image-20230416190602315

执行结果如下:

如果想要有如下lib/src,则需要在rb文件中

1
2
3
4
5
6
def install
# Install script to bin
# bin.install "helloworld.sh"
bin.install "bjf"
lib.install Dir["*"] # 记得添加lib目录,并将源码存放于lib里的src下
end

image-20230416190923562

目录/usr/local/Cellar/bjf/0.0.3/lib/src/

2.2、brew uninstall

验证方式,你brew uninstall的时候也会提示

下载下来的文件存放位置:

/Users/lichaoqian/Library/Caches/Homebrew

rb文件介绍

1、xxx.rb名字规范

1、文件名:

建议文件名都是小写;如果有多个词需连接,建议用-

2、文件内容

文件内容中的类名,必须和文件名一致,且第一个字母大小,其余都是小写。否则会出现如下错误。

image-20230416034029544

库中的内容

存放rb文件

二、brew库开发

1、正常的脚本开发

2、脚本加密

Shell高级加密可执行

3、

echo “hello world”

echo “1.1、创建 https://github.com/dvlpCI/script-branch-json-file.git"

echo “1.2、在上述创建的git下创建 helloworld.sh,并编写想要的脚本代码”

echo “1.3.1、shc -r -f helloworld.sh”
echo “1.3.2、修改上述生成的二进制可执行文件helloworld.sh.x名为helloworld(不填的话,你后续使用时候就不能输helloworld,而是要输helloworld.sh)”

echo “1.4、提交文件并打tag到远程”

echo “1.5、假如上述打的tag是0.0.1,则可以直接在浏览器里输入 https://github.com/dvlpCI/script-branch-json-file/archive/0.0.1.tar.gz 来回车下来该文件”

echo “1.6、shasum -a 256 0.0.1.tar.gz”

echo “hello world”

echo “hello world”

echo “2.1、创建 https://github.com/dvlpCI/homebrew-branch-json-file.git"

echo “2.2、在上述创建的git下添加 helloworld.rb 文件及其内容”

echo “hello world”

echo “hello world”

echo “tar -czvf v0.0.2.tar.gz helloworld.sh”

echo “mv v0.0.2.tar.gz archive/“

echo “hello world”

echo “hello world”

echo “brew tap dvlpCI/homebrew-branch-json-file”

echo “hello world”

echo “hello world”

ComfyUI入门

[toc]

一、安装与试运行

1、安装环境

安装教程见官网:https://github.com/comfyanonymous/ComfyUI?tab=readme-ov-file#installing

1.1、确认为 python3.12,若不是,则进行Mac下python 从3.13 降级到3.12

1
2
3
4
5
6
7
8
9
10
# 查看版本
python3 --version
python3.12 --version

# 安装 3.12 版本
brew install python@3.12

# 使用指定版本安装包和运行脚本
pip3.12 install -r requirements.txt
python3.12 main.py

1.2、安装 pytorch 相关库

按照 https://developer.apple.com/metal/pytorch/ 提示安装 torch 即可。

1
2
3
4
5
6
7
8
9
uname -m
# 如果输出为 x86_64,说明你的Mac使用的是Intel x86架构。
# 如果输出为 arm64,说明你的Mac使用的是Apple Silicon (ARM架构)

# Apple silicon
curl -O https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-arm64.sh
sh Miniconda3-latest-MacOSX-arm64.sh

python3.12 -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/nightly/cpu

1.3、 ComfyUI manual installation instructions

Note that some dependencies do not yet support python 3.13 so using 3.12 is recommended.

Git clone this repo.

Put your SD checkpoints (the huge ckpt/safetensors files) in: models/checkpoints

Put your VAE in: models/vae

1
2
3
4
# 进入 https://github.com/comfyanonymous/ComfyUI/releases 下载 Source code(zip) 并解压
# 下载 checkpoints https://civitai.com/models/1116?modelVersionId=124626
# 下载 vae https://civitai.com/models/296576/sdxl-vae
# 将 checkpoints 和 vae 分别放进 models 中的 checkpoints 和 vae 文件夹下

image-20241031040837228

1.4、ComfyUI dependencies.

1
2
3
# 进入 https://github.com/comfyanonymous/ComfyUI/releases 下载 Source code(zip) 并解压
cd Downloads/ComfyUI-0.2.6/
pip3.12 install -r requirements.txt

2、运行python3.12 main.py

运行 python3.12 main.py 提出如下错误

1
2
3
4
5
6
Traceback (most recent call last):
File "/Users/qian/Downloads/ComfyUI-0.2.6/main.py", line 66, in <module>
import utils.extra_config
File "/Users/qian/Downloads/ComfyUI-0.2.6/utils/extra_config.py", line 2, in <module>
import yaml
ModuleNotFoundError: No module named 'yaml'

2.1、yaml 问题

按照错误提示,进行 yaml 安装

1
pip3.12 install pyyaml

为了避免与系统环境冲突,您可以创建一个虚拟环境来安装 Python 包。以下是创建和激活虚拟环境的步骤:

1
2
3
4
5
6
7
8
# 创建虚拟环境
python3.12 -m venv path/to/venv

# 激活虚拟环境
source path/to/venv/bin/activate

# 在虚拟环境中安装 pyyaml
python3.12 -m pip install pyyaml

请将 path/to/venv 替换为您希望创建虚拟环境的路径。

image-20241031034115744

2.2、torch 问题

重新执行 python3.12 main.py,yaml 的错误解决了,但出现了 torch 问题

1
2
3
4
5
6
Traceback (most recent call last):
File "/Users/qian/Downloads/ComfyUI-0.2.6/main.py", line 88, in <module>
import comfy.utils
File "/Users/qian/Downloads/ComfyUI-0.2.6/comfy/utils.py", line 20, in <module>
import torch
ModuleNotFoundError: No module named 'torch'

按照 https://developer.apple.com/metal/pytorch/ 提示安装 torch 即可。

1
python3.12 -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/nightly/cpu

安装过程如下:

image-20241031034609679

2.3、numpy<2 问题image-20241031035434788

2.4、safetensors 等其他问题

再次运行 python3.12 main.py

1
2
3
4
5
6
Traceback (most recent call last):
File "/Users/qian/Downloads/ComfyUI-0.2.6/main.py", line 88, in <module>
import comfy.utils
File "/Users/qian/Downloads/ComfyUI-0.2.6/comfy/utils.py", line 24, in <module>
import safetensors.torch
ModuleNotFoundError: No module named 'safetensors'

safetensors是所需的一个库,如果只是执行 pip3.12 install -r requirements.txt ,当再执行main.py的时候还会出现如下错误

image-20241031040225364

所以正确的是运行以下命令即可全部修复

1
pip3.12 install -r requirements.txt

3、运行成功

执行完之后,再执行main.py 即可看到如下成功提示。

image-20241031040437792

image-20241031042528504

二、ComfyUI 入门教程

ComfyUI 入门教程

End

框架设计模式-⑦组件化

[toc]

一、概念了解

1、组件化

就是“基础库”或者“基础组件”,意思是把代码重复的部分提炼出一个个组件供给功能使用。

​ 使用:Dialog,各种自定义的UI控件、能在项目或者不同项目重复应用的代码等等。

​ 目的:复用,解耦。

​ 依赖:组件之间低依赖,比较独立。

​ 架构定位:纵向分层(位于架构底层,被其他层所依赖)。

2、模块化

就是“业务框架”或者“业务模块”,也可以理解为“框架”,意思是把功能进行划分,将同一类型的代码整合在一起,所以模块的功能相对复杂,但都同属于一个业务。

​ 使用:按照项目功能需求划分成不同类型的业务框架(例如:注册、登录、外卖、直播…..)

​ 目的:隔离/封装 (高内聚)。

​ 依赖:模块之间有依赖的关系,可通过路由器进行模块之间的耦合问题。

​ 架构定位:横向分块(位于架构业务框架层)。

3、总结

​ 其实组件相当于,把一些能在项目里或者不同类型项目中可复用的代码进行工具性的封装。

​ 而模块相应于业务逻辑模块,把同一类型项目里的功能逻辑进行进行需求性的封装。

二、组件化的必要性/产生背景

1、产生背景:

所有模块代码都编写在一个项目中,在项目越来越大后,测试/使用某个模块或功能,需要编译运行整个项目,麻烦

2、组件化思路:

将每个模块作为一个组件,加一个中间层来协调各个模块间的调用,所有的模块间的调用都会经过中间层中转。只让其他模块对中间层产生耦合关系,中间层不对其他模块发生耦合。)

3、组件化好处:

业务划分更佳清晰,新人接手更佳容易,可以按组件分配开发任务。

项目可维护性更强,提高开发效率。

更好排查问题,某个组件出现问题,直接对组件进行处理。

开发测试过程中,可以只编译自己那部分代码,不需要编译整个项目代码。

方便集成,项目需要哪个模块直接通过CocoaPods集成即可。

三、组件化设计思路1:有管理中心

完整的项目:https://github.com/dvlproad/033-Data-Notification-iOS

1、核心思想

启动时候注册关系表,执行时候遍历关系表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 核心思想1:注册,以统一管理,后续调用时候,从注册表中查询并执行对应的操作
void register(url, handle) {
model.url = url;
model.handle = handle;
models.add(model); // 问题1:用什么管理所有数据
}

// 核心思想2:执行,遍历在注册表中的所有操作,找到对应的操作后进行执行
void exec(url) {
for (var model in models) {
if (model.url == url) {
model.handle();
break;
}
}
}

2、实现方案

2.1、方案1:Url+block 路由器

缺点:入参,取参不明显。

2.2、方法2:protocal - class

将要执行的方法统一放到类中,通过protocal或者类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 根据 protocal 查找 class
// 方法1(可一对多):为 protocal 添加监听者数组属性(通过runtime,形成类似于 protocal.listeners ),
NSHashTable *protocalListeners = objc_getAssociatedObject(protocol, CJ_BROADCAST_PROTOCOL_LISTENER); // 找到之前绑定到这个protocal上的监听者列表。
objc_setAssociatedObject(protocol, CJ_BROADCAST_PROTOCOL_LISTENER, protocalListeners, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

注册的时候 [protocalListeners addObject:listener];

// 方法2(只能一对一):以 protocal 为 key , 通过 protocal 存取 module 模块(模块自身遵循协议)
// 创建一个NSMapTable实例,其中键是强引用,值是弱引用
NSMapTable<NSString *, NSObject *> *weakRefHashTable = [NSMapTable strongToStrongObjectsMapTable];

// 向哈希表中添加一个键值对
NSString *key = @"someKey";
NSObject *value = [[NSObject alloc] init];
[weakRefHashTable setObject:value forKey:key];

模块协议拆分为多个子协议,用户

网络处理协议、数据管理协议(单独)、用户界面更新协议

改进思想

1
2
3
4
5
6
7
8
9
10
// 1、发起请求(LoginViewController)
id<OrderModulePublic> moduler = [[CJMoudleManager shared] modulerForProtocal:@protocal(OrderModulePublic)];
UIViewController *vc = [moduler orderViewControllerWithPageType:xxx];

// 2、实现请求(TSOrderModuleImpl)
[CJProtocolCenter addModule:self forProtocol:@protocol(OrderModulePublic)];
#pragma mark - OrderModulePublic
- (UIViewController *)orderViewControllerWithPageType:(int)pageType

}

上述代码还可以演变成实现通知的一种形式(当注入的类中,有多个类都遵循了(实际上不应该出现)模块协议时候)

1
2
3
4
5
6
7
8
9
10
11
12
// 1、发起请求(LoginViewController)
NSMutableSet<id<CJUserServiceProtocolForModule>> listener = [[CJMoudleManager shared] listenersForProtocal:@protocal(CJUserServiceProtocolForModule)];
for (id<CJUserServiceProtocolForModule> listener in listeners) {
[listener userNameUpadte:userName];
}

// 2、实现请求(OrderHomeViewController)
[CJProtocolCenter addModule:self forProtocol:@protocol(CJUserServiceProtocolForModule)];
#pragma mark - CJUserServiceProtocolForModule
- (void)userNameUpadte:(NSString *)userName

}

下列图片来源于 《架构分层.graffle》 中的【三、模块间设计】

跳转的使用示例:

modular_CJProtocalManager 跳转

通知的实现示例:

modular_CJProtocalManager 通知

跳转 + 数据变化后内部通知的实现示例:

modular_CJProtocalManager 跳转 + 数据变化后内部通知

四、组件化设计思路2:无管理中心

项目示例:CJStandardProject 中 CTMediator+CJDemoModuleMain.h

1、核心思想

核心思想:直接执行,让系统通过反射来找到要响应事件的类和方法

1
2
3
void exec(target, action, params) {

}

2、实现方案

2.1、方案1:CTMediator target+action+param

实现示例:

modular_CTMediator target+action+param

2.2、方案2:XXXService+Implemention

modular_XXXService+Implemention

五、组件化实施过程的思考

1、架构分层

整个APP架构上从上到下分为三层,独立于APP的通用层,通用业务层,业务层。

架构分层1

1.1、独立于APP的通用层

此层常为:一些Cocoapods公有库或者自己编写的独立于APP的库。

举例:MJExtension、CJNetwork、分享、CJOverlayKit(Toast、HUD、ActionSheet)、CJShareList

1.2、每个APP自己的通用层

此层常为:前面的独立于APP的通用层的二次封装。若公司内部引用了第三方库,按照依赖倒置的原则,建议封装一层之后放到Basic Specs供业务方使用。好处:跟外部环境有效隔离,第三方库发生问题,公司内部可控。

如:CQNetwork、CQOverlayKit、CQShareKit、网络库CQNetwork、数据库(FMDB/WCDB)、缓存库等

依赖倒置原则告诉我们要面向接口编程;通过抽象(接口或抽象类)使各个类或模块实现彼此独立,互不影响,实现模块间的松耦合。

1.3、业务模块

业务层的模块应该按照模块化的设计思想,尽量做到高度的“高内聚,低耦合”。

因模块高度独立,且高频使用,若公司内部有多个App同时需要依赖,建议单独创建私有库Specs。

iOS端APP架构设计心得

END

日志系统

[toc]

前言:主要事项

1、异常数据的发现及补充、日志分类优化
2、日志数据的文件保存、滚动存储
3、日志文件上传
4、日志回捞

一、日志收集

1、日志分类

1.1、日志目标

序号 目标(Target)
1 app
2 sdk 三方库
3 h5 网页

1.2、日志类型

序号 类型 描述
1 api_app app中的网络请求
2 api_app_cache app中的网络请求的网络缓存请求
3 api_buriedPoint 埋点的网络请求
4 sdk_other sdk的各种事件(初始化等)
5 sdk_api sdk中的网络请求
6 dart 语法
7 widget 视图(布局像素越界等)
8 click_other 点击、
9 click_share 分享
10 native_route 路由/跳转
11 h5_route 与网页跳转有关
12 h5_js js交互
13 monitor_network 监控:网络类型变化
14 monitor_lifecycle 监控:生命周期变化
15 buriedPoint_other 埋点数据生成等
16 im IM
17 heartbeat 心跳
18 other 其他

2、日志等级

序号 目标(Level) 描述
1 Normal 正常信息(目前用于请求开始)
2 Success 成功信息(目前用于请求结束:成功)
3 Warning 警告信息(目前用于请求结束:报错)
4 Error 错误日志(目前用于请求结束:失败)
5 Dangerous 危险(处理白屏等) 一般会进行额外的埋点

3、日志整合归类

序号 列表 标志 包含
1 全部 all 所有
2 警告 warning 所有的警告
3 错误 error 所有的错误
4 接口 api api_app、api_cache(不包括sdk_api、api_buriedPoint)
5 点击 click click_share、click_other、h5_js
6 路由 route navite_route、h5_route
7 网页 H5 h5_route、h5_js
8 sdk sdk sdk_api、sdk_other
9 code code dart、widget
10 埋点 buriedPoint api_buriedPoint、buriedPoint_other
11 监控 monitor monitor_lifecycle、monitor_network
12 其他 other other
13 api结果 api_result type=api_app & level != Normal
14 im im 本地缓存消息、历史消息、收到的消息等
15 心跳 heartbeat

devtool_log_home_page

4、日志的补充收集及其策略

要收集的数据已在上述《1.2、日志类型》中说明。

4.1、页面跳转

详见上述 《3、日志整合归类》中的”路由”

4.2、截断的收集

序号 关键字 场景
1 break for 、 swtich
2 return

4.3、用户关键行为

序号 行为 用途的场景说明
1 用户的资源选择 判断选择的本地资源是否上传到错误的存储桶

二、日志数据的文件保存、滚动存储

1、数据的文件保存

写入xlog文件中。

2、数据的文件滚动存储

2.1、日志的过期清理

1、本地日志文件结构

1
2
3
4
5
6
7
-- Document
-- log
-- 2020-10-01
-- xlog_2020-10-01_1.xlog
-- xlog_2020-10-01_2.xlog
-- 2020-10-18
-- xlog_2020-10-18_1.xlog

2、日志保留清理的配置信息(摘自/同 下文的日志接口中的《日志文件配置信息》)

序号 说明 字段 示例
接口回值1 最多保留几个天的日志目录 maxDayDirCount 7
接口回值2 最多每天保留几个文件 maxDayFileCount 10
接口回值3 每个日志文件超过多少后创建新文件 perFileMaxMB 5

3、清理方案

清理时机:在切换前后台的时候,

  • 日志配置信息的更新
  • 根据最新的日志配置信息,延迟10s后(避免影响启动),进行清理判断及清理
    • 遍历总日志目录 Document/log 下的所有日志文件夹
    • 如果天日志文件夹个数超过保留个数(避免是用保留时间,导致保留时间内只有一个日志,也被删掉),则按顺序删除早期日志
      • 遍历天日志目录 Document/log/yyyy-MM-dd 下的所有日志文件夹
      • 如果天日志文件夹个数超过保留个数(避免是用保留时间,导致保留时间内只有一个日志,也被删掉),则按顺序删除早期日志

2.2、日志文件的滚动写入

要增加的日志记录,如果添加后会超过日志文件的大小,则使用新文件写入。

细节点
1、日志文件的大小判断,不要每次都去读取文件,而是记录住大小变化
2、日志不要有一条写一条,而是使用写入缓冲区+定时器,5s尝试写入一次

3、数据的加密、解密

1、加密:

写入日志字符串时候,对编码后的字节数据,额外增加一个三位随机值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// 使用 GZip 压缩算法将字符串进行压缩,并返回压缩后的字节数据(List<int> 类型)。
List<int> _gzipCompressString(String inputString) {
final codec = GZipCodec();

// 将输入字符串 inputString 编码为 UTF-8 格式的字节数据
final encoded = utf8.encode(inputString);

// 对编码后的字节数据进行压缩,并返回压缩后的字节数据。
final compressedData = codec.encode(encoded);

return compressedData;
}

/// 加密日志字符串
String _encryptLogString(String logString) {
final compressedData = _gzipCompressString(logString); // 压缩字符串,并返回压缩后的字节数据(List<int> 类型)
final randomNumber = Random().nextInt(800) + 100;
final confound = compressedData.toString().substring(0, compressedData.toString().length - 1) + ", $randomNumber]"; // 在压缩后的字符串尾部加上一个随机字符串

return confound;
}

2、解密:

使用python脚本对log文件进行解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import sys
import os
import gzip

# 读取.clog文件并解析数据
def parse_clog_file(input_file):
parsed_data = []

with open(input_file, 'r') as clog_file:
current_data = ""
for line in clog_file:
if current_data:
parsed_data.append(current_data)
binary_data = line[:-6] + ']' # 去除加密时候额外添加的三位随机数
byte_data = bytes(eval(binary_data))
decompressed_data = gzip.decompress(byte_data)
# current_data = byte_data.decode('utf-8')
print(decompressed_data)
current_data = decompressed_data.decode('utf-8')

# 添加最后一个数据条目
if current_data:
parsed_data.append(current_data)

return parsed_data

# 将解析后的数据写入与输入文件同名的.log文件
def write_to_log_file(parsed_data, input_file):
base_name = os.path.splitext(input_file)[0]
output_file = base_name + ".log"

with open(output_file, 'w') as log_file:
for data in parsed_data:
log_file.write(data + "\n")
log_file.write("\n")
log_file.write("=================================================================================================" + "\n")
log_file.write("\n")

if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python script.py <clogFileName>")
sys.exit(1)

input_clog_file = sys.argv[1]

# 解析.clog文件
parsed_data = parse_clog_file(input_clog_file)

# 将解析后的数据写入与输入文件同名的.log文件
write_to_log_file(parsed_data, input_clog_file)

print(f"Log data parsed from {input_clog_file} and written to {input_clog_file}.log.")

三、日志文件上传

1、日志文件命名及目录规范

以用户10000012020-01时候上传声音文件 beautiful_scenery.aac为例,其路径完整示例如下:

1
https://media.xxx.com/app1_test1/audio/1/1000001/2020-01/beautiful_scenery.aac

要上传的存储桶:

序号 说明 参数 约定值示例
1 存储桶region
regionGetFunction
网络环境 ap-shanghai
2 存储桶bucket
bucketGetFunction
网络环境 xxx-pro-image-1302324914
3 上传成功后,桶的映射路径值
cosFileUrlPrefixGetFunction
1、图片桶:https://images.xxx.com/
2、媒体桶:https://media.xxx.com/
3、日志桶:https://static.xxx.com/
4 要上传到桶的哪个相对路径下
cosFileRelativePathGetFunction
上传成功后完整路径=以上桶值+此相对路径
见下文 见下文
eg:app1_test1/audio/1/1000001/2020-01/beautiful_scenery.aac

《要上传到桶的哪个相对路径下 cosFileRelativePathGetFunction 》的文件路径分批说明:

序号 层次说明 层次值算法 层次值描述
1.1 存储桶里相对路径的前缀 app标志_环境标志
eg:app1_pro、app1_test1、app1_dev1
文件区分
1.2 是否在存储桶里进行进一步路径区分(可选) 根据场景决定是否对桶进行进一步分类。
if ( mediaType == xxx ) {
if(mediaScene == yyy) {

}
}

1、都是图片,但自拍图片要求独立出来
2、都是多媒体,但音视频要区分开
2.1 用户1级 int.parse(uid) % 1000; // 取余数 用户名取余,有效减少同层上文件夹个数
2.2 用户2级 uid 用户名
3 年月 DateTime.now().toString().substring(0, 7); 年-月
4 文件名 ${fileOriginName}_
${DateTime.now().microsecondsSinceEpoch}
.$fileExtensionType
文件名

附1:MediaType 媒体类型

序号 UploadMediaType 媒体类型 描述
1 unkonw 未知(默认值)
2 image 图片
3 audio 音频
4 video 视频
5 xlog 日志文件

附2:MediaScene场景 说明

序号 UploadMediaScene场景 描述
1 unkonw 未知(默认值)
2 selfie 自拍(安全等级较多)
3 im 会话聊天
4 live 直播

四、日志接口

1、日志记录接口(区别于回捞接口,需要在回捞前就有日志信息)

1、是否记录日志的开关及其配置信息

序号 说明 字段 示例
网页配置1 是否开启日志记录 isLogOn 1:开启、其他:关
网页配置2 要开启日志的用户(灰度上线)
(当且仅当开启上述日志记录时有效)
📢:未配置但开关为开时为全记录
logUserids [“101”, “102”]
网页配置3 要开启日志的设备(灰度上线)
(当且仅当开启上述日志记录时有效)
📢:未配置但开关为开时为全记录
logDeviceIds [“udid001”, “udid002”]
接口回值1 是否进行日志记录(灰度上线) needLog 1:记录、其他:不记录

2、不需要记录什么日志信息

序号 说明 字段 示例
接口回值1 不需要记录的日志类型 ignoreLogLevel [“Normal”, “Success”]
接口回值2 不需要记录的日志等级 ignoreLogType [“api_buriedPoint”, “buriedPoint_other”]

3、日志文件配置信息

序号 说明 字段 示例
接口回值1 最多保留几个天的日志目录 maxDayDirCount 7
接口回值2 最多每天保留几个文件 maxDayFileCount 10
接口回值3 每个日志文件超过多少后创建新文件 perFileMaxMB 5

4、日志文件问题回滚

序号 说明 字段 示例
网页配置1 强制删除什么时间前的日志 removeLogBeforeDate 2020-12-31
网页配置2 强制删除哪个用户的日志 removeUserids [“101”, “102”]
网页配置2 强制删除哪个设备的日志 logDeviceIds [“udid001”, “udid002”]
接口回值1 强制删除什么时间前的日志(不删除时空) removeLogBeforeDate 2020-12-31 或 空值

接口名定义:

1
function getLoggerConfig(userid, deviceId)

接口结果的json示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"logConfig": {
"needLog": 1,
"ignoreLogLevel": ["Normal", "Success"],
"ignoreLogType": ["api_buriedPoint", "buriedPoint_other"],
"maxDayDirCount": 7,
"maxDayFileCount": 10,
"perFileMaxMB": 5
},
"logRevert": {
"removeLogBeforeDate": "2020-12-31"
}
}

2、日志回捞及问题回滚接口

1、日志回捞

序号 说明 字段 示例
网页配置1 要回捞日志的用户 salvageUserids [“101”, “102”]
网页配置2 要回捞日志的设备 salvageDeviceIds [“udid001”, “udid002”]
网页配置3 日志回捞的开始时间 salvageStartDate 2020-01-01
网页配置4 日志回捞的结束时间 salvageEndDate 2020-01-07
接口回值1 此用户此设备日志回捞的开始时间 salvageStartDate 2020-01-01
接口回值1 此用户此设备日志回捞的结束时间 salvageEndDate 2020-01-07

2、日志文件问题回滚

序号 说明 字段 示例
网页配置1 强制删除什么时间前的日志 removeLogBeforeDate 2020-12-31
网页配置2 强制删除哪个用户的日志 removeUserids [“101”, “102”]
网页配置2 强制删除哪个设备的日志 logDeviceIds [“udid001”, “udid002”]
接口回值1 强制删除什么时间前的日志(不删除时空) removeLogBeforeDate 2020-12-31 或 空值

接口名定义:

1
function getLoggerSalvageAndRevert(salvageUserids, salvageDeviceIds)

接口结果的json示例如下:

1
2
3
4
5
6
{
"salvageId": "1001",
"salvageStartDate": "2020-01-01",
"salvageEndDate": "2020-01-07",
"removeLogBeforeDate": "2020-12-31"
}

3、日志文件上传到cos后同步给后台

用途:日志文件上传到cos后同步给后台,后台数据库记录每个用户,每个设备都捞到了什么数据。

接口名定义:

1
function addLoggerFile(userid, deviceId)

接口结果的json示例如下:

1
2
3
{

}

四、日志回捞

1、后台通过接口返回要回捞的信息。

2、前端用户在下次使用,收到后台信息时候,进行本地日志的上传。在腾云存储桶能收到对应日志,即代表成功。

五、方案实施

灰度方案:请参照 灰度系统 。(附:日志记录开关里目前已有灰度策略。)

目的:避免功能异常,出现集体性问题。

End

总目录

[toc]

Architecture架构

安全与破解

管理相关

混编

数据结构

算法与数学

iOS部分

Android

Flutter部分

React Native 部分

React 部分

H5-APP

HTML

Weex

上架相关

Script

阿里云

腾讯云

实用工具部分

代码管理

电脑使用

常识类

编程工具部分

AI

随笔

专利申请

iOS swiftui布局

[toc]

可以在position之后设置overlay吗?那可以在offset之后设置overlay吗?

在 SwiftUI 中,position 和 offset 对布局的影响不同,因此在它们之后设置 overlay 会导致不同的行为。下面具体分析两种情况,以及是否可以在这些修饰符之后使用 overlay:

1. 在 position 之后设置 overlay

​ • 行为

​ • position 改变了视图的实际布局坐标系,指定的是视图的中心点

​ • overlay 的绘制依赖于视图的边界框(frame),而 position 不会改变视图的原始边界框的大小,只改变其位置。

​ • 结果

​ • overlay 将基于视图的原始边界框绘制,而不是移动后的显示位置。

​ • 如果你在 position 之后设置 overlay,你会发现 overlay 的位置不符合预期。

​ • 示例问题代码

1
2
3
4
5
6
7
Text("Hello, SwiftUI!")
.frame(width: 100, height: 50)
.position(x: 150, y: 150) // 移动中心点
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.red, lineWidth: 2)
) // 边框不在预期位置

​ • 解决方法

​ • 不要在 position 之后设置 overlay。

​ • 把 overlay 放在 position 之前,或者使用 ZStack 来包装视图和装饰内容。

1
2
3
4
5
6
7
8
ZStack {
RoundedRectangle(cornerRadius: 10)
.stroke(Color.red, lineWidth: 2)
.frame(width: 100, height: 50)
Text("Hello, SwiftUI!")
.frame(width: 100, height: 50)
}
.position(x: 150, y: 150)

2. 在 offset 之后设置 overlay

​ • 行为

​ • offset 仅影响视图的视觉显示位置,不会改变视图的布局边界框(frame)。

​ • 因此,overlay 仍然会基于视图的原始边界框绘制,而不是移动后的显示位置。

​ • 结果

​ • overlay 不会随着 offset 的视觉位置变化,导致错位。

​ • 示例问题代码

1
2
3
4
5
6
7
Text("Hello, SwiftUI!")
.frame(width: 100, height: 50)
.offset(x: 50, y: 100) // 视觉位置偏移
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.red, lineWidth: 2)
) // 边框位置基于原始frame,不正确

​ • 解决方法

​ • 把 overlay 放在 offset 之前。

1
2
3
4
5
6
7
Text("Hello, SwiftUI!")
.frame(width: 100, height: 50)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.red, lineWidth: 2)
)
.offset(x: 50, y: 100) // 最后移动

​ • 如果必须在 offset 后设置 overlay,需要手动调整 overlay 的偏移量:

1
2
3
4
5
6
7
8
Text("Hello, SwiftUI!")
.frame(width: 100, height: 50)
.offset(x: 50, y: 100)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.red, lineWidth: 2)
.offset(x: 50, y: 100) // 手动同步偏移
)

总结

在 position 之后设置 overlay:

​ • 不推荐,position 改变了视图的坐标系,但不会改变边界框,导致 overlay 错位。

​ • 解决方法:使用 ZStack 或将 overlay 放在 position 之前。

在 offset 之后设置 overlay:

​ • 可以使用,但 overlay 基于原始边界框绘制,可能导致错位。

​ • 解决方法:将 overlay 放在 offset 之前,或者手动调整 overlay 的偏移量。

最佳实践

避免在 position 或 offset 之后设置 overlay,优先通过 ZStack 或调整修饰符顺序解决问题。

网络接口数据安全的【攻与防大全】

[toc]

前言

网络最简单的安全,防抓包。其次是数据加密。

Charles抓包原理:简单来说 Charles 是做了一个中间人的角色。

客户端和 Charles 建立 SSL 连接,然后 Charles 和服务端建立 SSL 连接。

客户端会验证 Charles 发来的证书(因为我们在手机上将 Charles 的根证书加入到信任区之后,Charles 签发的所有证书都会被客户端认为是可信的,则客户端就不会对 Charles 返回的数据进行怀疑,直接使用,这也是我们在 Charles 上修改 HTTPS 返回数据的理论基础。),而 Charles 会验证 SSL 服务端发来的证书。

iOS 禁用抓包实践

1、禁止代理抓包/路由直连:通过设置connectionProxyDictionary为空字典,可以禁止代理抓包工具如Charles和Fiddler等

2、检查是否有代理:缺点使用正常的VPN也会被禁止。

一、对数据加密的概述

1、安全的数据传输是什么样的

数据传输:都是使用密文(客户端提供加密的参数,服务端提供加密的结果)。

【客户端提供加密的参数】:需要在客户端加密后,上传到服务端,并在服务端对其进行解密,以确认传过来的参数。

【服务端提供加密的结果】:需要在服务端加密后,返回给客户端,并在客户端对其进行解密,以确认传过来的结果。

数据安全传输(AES)

若要了解加密方式可查看下文【附1:加密的方式有哪些

2、一端的AESKey怎么【安全的】传给另一端 / 为什么要对AES的密钥进行RSA公钥加密后传输??

答:为了安全,一端的AESKey必须先加密,然后才能传给另一端,这样才能防止用户从请求中抓取到AESKey。同时另一端需要对传过来的数据进行解密得到AESKey原值,后续两端才能都通过AESkey原值进行两边数据的对称加解密。即数据加密采用AES,而把AES的加密秘钥用RSA加密。(附:为什么AESKey不是使用md5方式加密?答:因为另一端要解密来使用原值。)。

问题:为什么要对AES的密钥进行RSA公钥加密后传输?

AES是对称加密算法,加密和解密的密钥都是同一个,为了防止被别人恶意获取到该密钥,然后对我们的业务请求参数进行解密,我们需要将AES密钥进行非对称加密后再进行传输。

2.1、密钥来源于客户端(类似Https)

数据安全传输(AES+RSA)

上图和Https原理有点相像。

2.1.1、客户端密钥保存的安全问题

如果密钥是来源于客户端,那如何在客户端安全的保存密钥?请查看下文中的【附2:如何在客户端安全的保存密钥AESKey

2.2、密钥来源于服务端(为了密钥能够像版本那样进行控制)

实际过程中,可能你的AESKey为了能够像版本那样进行控制,可能你的AESKey是由服务端分发的,则其过程可能如下:

数据安全传输(AES+RSA)

空 ==> getServicePublicKey ==> servicePublicKey

clientPublicKey(使用ServicePublicKey加密) ==> sendClientPublicKey ==> 使用servicePrivateKey解密得到clientPublicKey

空 ==> getServiceAESKey ==> aesKey(使用clientPublicKey加密)

参考文章:

最终网络数据传输,可能就只剩下类似下面一样,只有一个参数和

1
2
3
4
5
只剩一个参数:
encryptedParametersString: xxxx

只剩一个返回:
encryptedResultString: yyyy

二、被破解后,篡改参数值,进行请求攻击,怎么办?(二次加密:签名)

如果上面的加密过程被破解了,用户就能自己还原原始参数和原始返回值了。这时候如果他篡改参数值进行请求攻击怎么办?

1、请求参数值被篡改的场景

购物车多商品结算时候,本需共100美元的商品。在生成订单的时候,携带了所要支付的各个商品的id,以及总费用。结果总费用被篡改成以0.01美元的价格生成了订单Id。而后服务端却没有校验这些商品是不是0.01美元,从而导致平台损失。

以购物车结算为例,传统的传给服务器的参数一般是如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
goods: [
{
id: xxx
title:
price:
},
{
id: yyy
title:
price:
}
],
totoalPrice: 100

当用户破解了我们之前的加密算法后,其就能还原原始参数结果,并篡改参数值,然后再利用破解所得的加密算法得到一个新的加密参数如下:

1
2
3
4
5
篡改前所传参数:
encryptedParametersString: xxxx1

篡改后所传参数:
encryptedParametersString: xxxx2

之后,如果服务器接收和处理的就是被修改过的值了。

那这种情况下怎么办法呢?答:再加一层加密算法,即签名。

2、请求参数值被篡改的解决/防篡改方案

为了防篡改,通常有以下三个步骤:

1、我们通过上述原始参数计算得出一个签名值。然后将签名值添加一起传给服务端。

1
2
3
4
5
6
7
8
9
篡改前所传参数:
encryptedParametersString: xxxx1
signatureValue: xxxx1的签名值
signatureMethod: xxx1的签名方式(如果客户端有md5、sha256等多种加密方式)

篡改后所传参数:
encryptedParametersString: xxxx2
signatureValue: xxxx1的签名值(用户不知道此签名值的计算方法)
signatureMethod: xxx1的签名方式(如果客户端有md5、sha256等多种加密方式)

由于用户不知道签名值的计算方法,所以即使其篡改了原原始参数中的部分参数值并生成加密参数,其也仍需要再破解出这个签名值的计算方法,如果其不修改签名值,而是把签名值直接上传到服务端,则服务端会进行以下操作。

2、然后服务端接收到所传的参数(含签名值)后,也按照和客户端一样的方式对除签名值外的其他参数再计算一遍签名值。

3、如果所得的签名值和客户端传过来的签名值不一样,则是参数被篡改了,服务端放弃该请求。(注:为了保证正常情况下计算出的客户端和服务端的签名一样,我们会对请求参数进行字典排序后再签名。

所以,防篡改的解决办法是客户端加签,服务端验签。

2.1、加签的方法

对上述的 encryptedParametersString: xxxx1 进行md5、sha256或其他加密算法。

2.1.1、签名算法的防猜测

如果签名的加密算法被猜到是用md5进行的签名。则用户可以也跟着对encryptedParametersString: xxxx1 进行md5加密即可得到签名会被服务端认证通过的请求了。那是不是md5不能用呢?如果用怎么用?答:md5加盐,进一步增加破解的难度。

为什么加盐?
避免用户直接使用参数值进行md5加密,发现签名值就是由参数值md5加密而来。所以为了让MD5码更加安全 ,我们现在都采用加盐,盐要越长越乱。

1
2
3
4
5
加盐前:
digest = hash(input)

加盐后:用户拿参数值直接md5发现和签名值匹配不上,则一般就不会发现是用md5加密了。
digest = hash(salt + input)

这样加盐后,当用户再拿参数值直接进行md5后,发现和签名值匹配不上,则一般就不会想到算法是用md5加密了。

这个salt可以看作是一个额外的“认证码”,可以是固定的值,也可以是动态的值,如每个用户自己的userid。

更多MD5及其加盐可查看下文中的【附3:签名 –> md5(加盐))】

2.1.2、签名算法关注的内容

除了请求参数外,签名算法还关注请求的接口和方法,即其一般是对含参数在内、外加其他值(如URL、Method)一起进行签名。

参考文章:

三、不破解,但进行二次请求/API重放攻击(Replay Attacks)怎么办?

加密后,破解难度增加了,但是如果用户不破解,而是通过API重放攻击你的服务器呢,怎么办?

1、重放攻击(Replay Attacks)概述及危害介绍

API重放攻击(Replay Attacks)又称为重播攻击、回放攻击。它的原理就是把之前窃听到的数据原封不动的重新发送给接收方。

1.1、即使有幂等性情况下,重放请求的危害

追问:服务端不是应该有做幂等性了吗,为什么还需要处理防重放问题?

答:虽然如果一个系统操作是幂等的,即使攻击者重放了一个请求,系统也不会因为重复执行而产生不同的结果或状态。在一定程度上可以减少重放攻击的潜在损害(例如,一个幂等的HTTP GET请求,无论请求多少次,服务器都会返回相同的结果,而不会对服务器的状态产生影响)。但是即使请求本身是幂等的,也需要防止恶意用户发送大量重放请求来耗尽系统资源,从而实现拒绝服务攻击(DoS)。

若要了解幂等性知识,可查看

1.2、没有幂等性情况下,重放请求的危害

如果服务端没有幂等性,则假设有一个在线投票系统,用于选举或表决,每个用户只能投票一次。那么攻击者可能会尝试重放投票请求,进行重复投票,以增加他们支持的候选人或选项的票数。

2、重放攻击(Replay Attacks)的解决方案

防止重放攻击必须要保证请求仅一次有效。需要通过在请求体中携带当前请求的唯一标识,并且进行签名防止被篡改。所以,防止重放攻击需要建立在添加签名且防止签名被篡改的基础之上。

2.1、措施一:添加请求的时效性参数 Timestamp

即某个请求,其请求时间戳Timestamp,和服务端的当前时间在规定时间内(如1分钟内)则为合法请求,反之,则视为无效请求。

服务端时间戳的获取时机和服务端的RSA公钥一致,你可理解为网络库的初始化阶段(虽然这是真正初始化之后的请求的”二次初始化”)。

数据安全传输_防重放_1加timestamp

客户端与服务端的时间戳差值 diff = 服务器返回的时间戳 - 客户端请求结束的时间戳

校准服务端后的客户端的时间戳 = 当前客户端时间戳 + 客户端与服务端的时间戳差值 diff

2.2、措施二:添加一个唯一的随机数Nonce

2.2.1、Nonce的核心思路:

调用者每次调用时:

​ A:调用者生成并带上一个随机数Nonce(可以考虑用用户id+当前时间戳+接口url)

​ B:服务端该随机数是否已出现,有则拒绝,无则存储该随机数并放过请求。

则请求参数变为如下:

1
2
3
4
5
6
7
8
9
10
11
未加 timestamp 和 nonce 前:
encryptedParametersString: xxxx1
signatureValue: xxxx1的签名值
signatureMethod: xxx1的签名方式(如果客户端有md5、sha256等多种加密方式)

加了 timestamp 和 nonce 后:
encryptedParametersString: xxxx1
signatureValue: xxxx1的签名值,📢:要把timestamp和nonce也包括进去,避免它们被篡改
signatureMethod: xxx1的签名方式(如果客户端有md5、sha256等多种加密方式)
nonce: xxx1的nonce
timestamp: xxx1的timestamp

这里注意:计算xxxx1的签名值要把timestamp和nonce也包括进去,避免它们被篡改。

2.2、Nonce的优化思路:

这里服务端要保证Nonce唯一,就得存储已经用过的Nonce,但长期保持会带来两个问题

​ (1)存储成本增加,日积月累,这里要存储的Nonce会越来越多,需要的存储空间就越大

​ (2)碰撞概率增加,正常服务被拒绝概率增大;这里随着生成Nonce值越来越多,碰撞的概率一定越来越大,若通过增加Nonce值的长度,有增加存储成本。

​ 那么,另一个可行的办法,就是调用者每次请求时带上当前请求时间点Timestamp,然后由服务端限制请求的时效性。

​ 如此,上面提到的Nonce值存储成本可能比较大的问题,在结合Timestamp后,可大大降低存储成本,如Timestamp=1min,则仅需存储1min内的请求Nonce值即可,大大减少存储的量级。

至此,客户端发送的参数为:

1
2
3
4
5
encryptedParametersString: xxxx1
signatureValue: xxxx1的签名值,📢:要把timestamp和nonce也包括进去,避免它们被篡改
signatureMethod: xxx1的签名方式(如果客户端有md5、sha256等多种加密方式)
nonce: xxx1的nonce
timestamp: xxx1的timestamp

3、扩展:客户端自己如何防止接口重复请求?

1、界面添加 loading

2、进行节流(最后执行)和防抖(立即执行)中的防抖。

3、请求队列:类似SDWebImage加载相同地址的图片。

如果重复请求的话,客户端的优化:

1、缓存

其他可参考:功能问题:如何防止接口重复请求?


##

附1:加密的方式有哪些

对称加密 非对称加密
概念 加密密钥和解密密钥相同 加密密钥和解密密钥不相同
算法 DES、AES、RC2、RC4、RC5 等 RSA、ECC 等
缺点 速度快,安全性低。使用同一个密钥,如果一方密钥泄露,数据就不安全了 非对称加密的安全性更高,但加密速度较慢。

注意:非对称加密的安全性更高,但加密速度较慢。一般我们在项目中采用的是 ”结合对称和非对称加密,数据加密采用AES,而把AES的加密秘钥用RSA加密,这样兼顾速度及安全性“https://juejin.cn/post/7002058547395559438

1.1、对称加密

加密方式_对称

1、李雷想要给韩梅梅发送消息,他们约定使用对称加密的方式把消息进行加密

2、李雷用密钥把消息加密然后发送给韩梅梅

3、韩梅梅用同一个密钥解密,然后看到李雷发送给自己的消息

1.2、非对称加密

加密方式_非对称

1、李雷想要给韩梅梅发送消息,他们约定使用非对称加密的方式把消息进行加密

2、李雷首先要得到韩梅梅的公钥

2、李雷用韩梅梅的公钥把消息加密然后发送给韩梅梅

3、韩梅梅用自己的私钥解密,然后看到李雷发送给自己的消息

韩梅梅给李雷发送消息同理。

附2:如何在客户端安全的保存密钥AESKey

如果密钥是来源于客户端,那如何在客户端安全的保存密钥?

1、密钥在代码上(内部)

1.1、存放密码的常见的代码

1
2
3
4
#define kSecret "abcd1234"

// 或者
const char* kSecret = "abcd1234";

这是非常危险的,因为常量会被直接编译到可执行文件的data段,只要对生成的可执行文件使用stringsotool等命令就可以dump出原始字符串。

1.2、对密码加密

仍然存

1、类的代码层面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 方法1:密码在可执行文件的data段,且是明文
#define kSecret "abcd1234"
void getSecret {
return kSecret
}

// 方法2:密码在可执行文件的data段,虽是明文,但取到的值要再经过一层你自己知道的方法解密
#define kSecret "\x7e\x77\x64\x3c\xa7\xd4\x6d\x46\x29\x8b\xe3\x23\x9f\x1a\x5c\xdb"; // 使用aes加密后的
#define kAESKey "abcdefgh12345678";
char* getSecret() {
return decryptSecretByAESKey(kSecret, kAESKey);
}
// 方法2的优化:内部代码使用额外包在宏(kAppSecret)上,由于宏没有明确的入口,使得静态分析相比直接调用函数形式的getSecret更加不容易被破解。
#define kAppSecret \
({ \
char* buf = decryptSecretByAESKey(kSecret, kAESKey); \
[NSString stringWithUTF8String:buf]; \
})

附:带返回值的宏的写法 见宏定义的两个技巧:

1
2
3
4
#define SOME_MACRO \ 
({ \
expression; \
})

最后一个表达式的值就是宏的返回值,使用时更像函数的返回值。

2、类本身(可包含类中的函数名)

因为objc代码的动态性,编译器会在binary中留下类名、函数名等信息,这些信息是可以被class-dump-z等工具提取的,友好的命名让程序猿更方便,但同时也方便了破解者。对安全相关的重要模块类,可以故意混淆类名,让人不容易轻易联想到该的真实目的。

PS、更多混淆知识可看我写的另一篇文章:iOS APP安全加固方案(一、代码混淆CodeObfuscation)

2、密钥保存在外部

2.1、密钥保存在Keychain

密钥保存在Keychain并不安全,iOS越狱后可以导出Keychain的内容。应该尽量避免存放重要信息(如:token、用户名、密码等)在Keychain中,即使要存放,也一定要加密后存放。参考:iOS安全攻防(九):使用Keychain-Dumper导出keychain数据

2.2、密钥保存在文件

保存在app bundle、plist等配置文件更不安全,但可以使用隐写术等方式迷惑hackers。

2.2.1、隐写术

隐写术的载体:相比视频、音乐等文件,通常选择使用图片这种占用空间小的文件,但因为Xcode打包时会对png图片做特殊处理,如果将密码携带在png中,可能会在使用的时候无法复原。所以若非确定的隐写方法,则建议用bmp图片。

本节点【如何防止客户端被破解/密钥的保护】部分内容摘自/参考:如何防止客户端被破解

LSB算法:LSB全称为 Least Significant Bit(最低有效位),是一种常被用做图片隐写的算法。为了避免我们对图片进行的处理可能会在压缩的过程中被破坏,建议在无压缩的bmp图片上实现。(png图片是无损压缩,jpg图片是有损压缩。)

steganography_lbs图片来源于:《网络加密.graffle》中的【隐写术】

bmp的隐写,相比lsb隐写,bmp简单的多,并且达到了无损隐写。

bmp了解:BMP位图隐写

ctf杂项中图片隐写拿到一张图片常见的解题方法

附3:签名 –> md5(加盐)

md5是哈希函数。SHA-256或SHA-3也是哈希函数,相比md5更安全。

Mac:在终端上实现将abcd1234进行MD5(Message Digest algorithm 5 ,信息摘要算法)加密。

1
2
3
echo -n 'abcd1234' | md5

得到结果:e19d5cd5af0378da05f63f891c7467af

使用MD5就是将一串字符串通过某特定的算法来将其变成另一种形式,这样子就在外观上起到了加密的效果,但是由于背后的算法(md5是一种常见的hash算法)是固定的,所以每一个字符串都有固定的MD5格式。(反过来,一个hash值是否只能是一个值才可能?答:不是)

因为每一个字符串就只有一种特定的MD5格式,所以md5的破解(从c=hash(m)逆向得到原始明文m)容易想到的方式有:

方法 优缺点 推荐程度
暴力破解法 时间成本太高 ⭐️
字典法 提前构建一个“明文->密文”对应关系的一个大型数据库,破解时通过密文直接反查明文。但存储一个这样的数据库,空间成本是惊人的 ⭐️⭐️⭐️
构建彩虹表 在字典法的基础上改进,以时间换空间。是现在破解哈希常用的办法。
彩虹表:彩虹表记录了几乎所有字符串的MD5对照表。
⭐️⭐️⭐️⭐️⭐️

md5在线解密网站

md5在线解密

因为已经有了类似彩虹表的破解方法,所以如果不‘加盐’,则当用户知道是 e19d5cd5af0378da05f63f891c7467af 时候,就很容易知道原值是 ‘abcd1234’了。

所以Md5加密方式的进化之路小结:

密码存储的种方式
1 直接存储密码明文m 明文存储,无安全性可言。
2 存储密码明文的哈希值hash(m) 虽然是入侵者得到的是hash值,但由于彩虹表的存在,也很容易批量还原出密码明文来。
3 存储密码明文的加盐哈希 hash(m+salt)。
这里的salt可以是用户名,手机号等,但必须保证每个用户的salt都不一样才是安全的。
相对前两种安全

网路本身的安全

HTTPS是应用层的安全协议。TCP是传输层的协议,但是它不安全,因为它是明文传输的,所以SSL的诞生就是给TCP加了一层保险,使HTTPS和TCP之间使用加密传输。而TLS只是SSL的升级版,他们的作用是一样的。

以下内容摘自HTTPS、SSL/TLS、TCP之间错综复杂的情感纠葛

SSL(Secure Sockets Layer) 安全套接层:为网络通信提供安全及数据完整性的一种安全协议。

TLS(Transport Layer Security)安全传输层协议:用于在两个通信应用程序之间提供保密性和数据完整性。该协议由两层组成: TLS 记录协议(TLS Record)和 TLS 握手协议(TLS Handshake),是更新、更安全的SSL版本。

SSL/TLS协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。==> 类似 RSA

但是,这里有两个问题。

(1)如何保证公钥不被篡改?

解决方法:将公钥放在数字证书中。只要证书是可信的,公钥就是可信的。

(2)公钥加密计算量太大,如何减少耗用的时间?

解决方法:每一次对话(session),客户端和服务器端都生成一个”对话密钥”(session key),用它来加密信息。由于”对话密钥”是对称加密,所以运算速度非常快,而服务器公钥只用于加密”对话密钥”本身,这样就减少了加密运算的消耗时间。 ==> 类似 AES

HTTPS(超文本传输安全协议)是建立在HTTP(超文本传输协议)之上,通过SSL/TLS(安全套接字层/传输层安全协议)进行加密的安全协议。在HTTPS的握手过程中,SSL/TLS握手会先于HTTP握手发生。以下是HTTPS连接建立的基本步骤:

  1. TCP握手:客户端首先通过TCP(传输控制协议)与服务器建立一个普通的连接。这个过程包括三次握手,确保双方都准备好进行数据传输。
  2. SSL/TLS握手:一旦TCP连接建立,SSL/TLS握手就开始了。这个过程包括:
  3. 应用层协议:一旦SSL/TLS握手完成,客户端和服务器就可以使用这个安全的连接来传输加密的数据。这时,HTTP请求和响应就会在SSL/TLS层之上进行传输。

因此,HTTPS的连接建立过程中,是先进行TCP握手,然后进行SSL/TLS握手,最后才开始HTTP通信。

其他参考文章

End

灰度系统

[toc]

目的:避免功能异常,出现集体性问题。

一、灰度接口

1、灰度开关接口

1、是否进行功能的灰度使用的开关

序号 说明 字段 示例
网页配置1 是否开启功能 isOpen 1:开启、其他:关
网页配置2 要开启功能的用户(灰度上线)
(当且仅当开启上述功能时有效)
openUserids [“101”, “102”]
网页配置3 要开启功能的设备(灰度上线)
(当且仅当开启上述功能时有效)
未配置但开关为开时为全记录
openDeviceIds [“udid001”, “udid002”]
接口回值1 是否进行功能的使用(灰度上线) needOpen 1:使用、其他:不使用

接口名定义:

1
function getGrayscaleSwitchConfig(userid, deviceId)

接口结果的json示例如下:

1
2
3
4
5
6
7
8
{
"logFuture": {
"needOpen": 1,
},
"cacheOptimize": {
"needOpen": 1
}
}

End

请求规范

[toc]

一、JS请求结果结构规范(JSResponseModel)

json举例

1
2
3
4
5
{
"resultCode": 0,
"message": "成功",
"result": map 或 array
}

点击跳转到附文查看:JSResponseModel 代码示例

二、JS调用规范

1、规范标准

1、js需要回传值时候,回传值方法统一由h5的callbackMethod告知app(而非app写死)

2、js需要回传值时候,一定要有回传值,且回传值结果必须如《本文》中的【一、JS请求结果结构】所示

2、JS调用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/// 调起微信小程序去支付
static JavascriptChannel pay(
BuildContext context, {
required WebViewController? Function() webViewControllerGetBlock,
}) {
return BJJavascriptChannel(
name: 'h5CallBridgeAction_pay',
onMessageReceived: (JavaScriptMessage message) async {
Map<String, dynamic>? map = json.decode(message.message.toString());
if (map == null) {
return;
}

Map<String, dynamic> jsCallbackMap = JSResponseModel.initMap();
if (map['payType'] == null || map['payType'].isEmpty) {
jsCallbackMap = JSResponseModel.errorMap(message: '缺少 payType 参数');
} else {
MiniProgramPayBean payParams = MiniProgramPayBean.fromJson(map);
bool isSuccess = false;
if (map['payType'] == 'wx_mp') {
isSuccess = await WXMPPay.jumpWechatMP2Pay(payParams);
} else {
isSuccess = await WXMPPay.jumpWechatMP2Pay(payParams);
}
jsCallbackMap = JSResponseModel.successMap(isSuccess: isSuccess);
}

String? jsMethodName = map["callbackMethod"];
if (jsMethodName == null) {
WebViewController? webViewController = webViewControllerGetBlock();
WebViewWays.runJavaScriptMap(
jsMethodName!,
params: jsCallbackMap,
controller: webViewController,
);
}
},
);
}

三、测试JS的JS接口

描述 JS方法 其他
测试 h5 调用 app 方法,并将返回值回调给 h5 h5CallBridgeAction_test_h5CallAppAndCallBackToH5
测试浏览器中的链接显示,且链接可打开app的_原生页面 h5CallBridgeAction_test_openBrowser
原生调用h5提供的显示回传值的方法 bridgeCallH5Action_showCallbackJsonString

四、公共的JS接口

1、app设备相关

描述 JS方法 其他
获取app的公共信息,并将返回值回调给 h5 h5CallBridgeAction_getFixedAppInfo 改用user-agent
获取埋点monitor的公共信息,并将返回值回调给 h5 h5CallBridgeAction_getFixedMonitorInfo
更新状态栏颜色 h5CallBridgeAction_updateAppStatusBarStyle
是否是模拟器 h5CallBridgeAction_isSimulator

2、用户相关

描述 JS方法 其他
获取用户token(用于安全的告知h5用户信息),并将返回值回调给 h5 h5CallBridgeAction_getCurrentUserToken
退出登录 h5CallBridgeAction_logout

3、基础提示相关

描述 JS方法 其他
显示app的toast样式 h5CallBridgeAction_showAppToast

4、路由相关

描述 JS方法 其他
跳转到app指定页面_Url h5CallBridgeAction_jumpAppPageUrl
跳转到app指定页面_Name h5CallBridgeAction_jumpAppPageName

5、分享相关

描述 JS方法 其他
分享内容到指定的分享方式(微信聊天页面等) h5CallBridgeAction_share

6、网页相关

描述 JS方法 其他
刷新网页(到新地址) h5CallBridgeAction_reloadAppWebView
关闭webview h5CallBridgeAction_closeWebView

五、业务相关的JS接口

入参 JS方法 其他
发送IM的各种信息 h5CallBridgeAction_sendIMText

六、JS安全

附1:JSResponseModel代码示例

class JSResponseModel 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// h5CallBridge_responseModel.dart
class JSResponseModel {
final int resultCode;
final String? message;
final dynamic result;

JSResponseModel({
this.resultCode = 0,
this.message,
this.result,
});

// js默认的map(默认是成功)
static Map<String, dynamic> initMap({
String? message,
}) {
return JSResponseModel(
resultCode: 0,
).toMap();
}
// js失败时候的map
static Map<String, dynamic> errorMap({
String? message,
}) {
return JSResponseModel(resultCode: 1, message: message).toMap();
}
// js成功时候的map
static Map<String, dynamic> successMap({
required bool isSuccess,
dynamic result,
}) {
return JSResponseModel(
resultCode: isSuccess ? 1 : 0,
result: result,
).toMap();
}

// Model 转 map 以回传结果给h5
Map<String, dynamic> toMap() {
Map<String, dynamic> responseMap = {};

responseMap.addAll({"resultCode": resultCode});
if (message != null) {
responseMap.addAll({"message": message});
}
if (result != null) {
responseMap.addAll({"result": result});
}
return responseMap;
}

bool get isSuccess => resultCode == 0;
}

End