2025-03-06 17:19:07
在开发过程中,我们经常使用 HMAC(散列消息认证码)对数据进行签名,以确保数据完整性和身份验证。
然而,不同编程语言在对签名数据进行编码时可能会有所不同,导致相同的 HMAC 计算在不同语言中产生不同的结果。
这篇文章也是因为我直接将 PHP 的签名算法扔给 ChatGPT 生成,并没有实际测试,导致客户反馈签名计算失败,测试后才发现的。
本文将以 Go 和 PHP 为例,探讨为什么直接对 HMAC 签名进行 Base64 编码与先转换为 16 进制字符串再编码的结果不同。
package main
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/hex"
"fmt"
)
func main() {
data := "hello"
password := "123456"
h := hmac.New(sha1.New, []byte(password))
h.Write([]byte(data))
signatureBytes := h.Sum(nil)
// 直接对 HMAC 结果进行 Base64 编码
base64Signature := base64.StdEncoding.EncodeToString(signatureBytes)
fmt.Println(base64Signature) // 输出:NYSQUfYBHG0EZ6pU+r+Iw4CvPIQ=
// 先转换成 16 进制字符串,再进行 Base64 编码
hexString := hex.EncodeToString(signatureBytes)
base64OfHex := base64.StdEncoding.EncodeToString([]byte(hexString))
fmt.Println(base64OfHex) // 输出:MzU4NDkwNTFmNjAxMWM2ZDA0NjdhYTU0ZmFiZjg4YzM4MGFmM2M4NA==
}
<?php
$data = "hello";
$password = "123456";
// 直接对 HMAC 结果进行 Base64 编码
echo base64_encode(hash_hmac('sha1', $data, $password, true));
// 输出:NYSQUfYBHG0EZ6pU+r+Iw4CvPIQ=
echo "\n";
// 先转换成 16 进制字符串,再进行 Base64 编码
echo base64_encode(hash_hmac('sha1', $data, $password));
// 输出:MzU4NDkwNTFmNjAxMWM2ZDA0NjdhYTU0ZmFiZjg4YzM4MGFmM2M4NA==
?>
表面上看,Go 和 PHP 代码的逻辑是相同的,但它们的 Base64 结果却不同。
其根本原因在于编码前的输入数据不同。
hash_hmac(
string $algo,
string $data,
#[\SensitiveParameter] string $key,
bool $binary = false
): string
PHP 手册中也提到了:当 binary
设置为 true
输出原始二进制数据,设置为 false
输出小写 16 进制字符串。
signatureBytes
是 HMAC 计算出的二进制数据。hash_hmac('sha1', $data, $password, true)
也返回二进制数据。hash_hmac('sha1', $data, $password)
默认返回 16 进制字符串,每个字节被转换成 2 个字符。hex.EncodeToString(signatureBytes)
也会将二进制数据转换为 16 进制字符串。Base64 编码的主要作用是将二进制数据转换为文本格式,便于在 URL 或 JSON 等环境中传输。
它不会改变数据的内容,而是按照固定的方式将每 3 个字节转换为 4 个可打印字符。
因此,输入数据的不同会直接影响最终的编码结果。
如果希望跨语言 HMAC 计算保持一致,建议:
hash_hmac('sha1', $data, $password, true)
以获取二进制结果。base64.StdEncoding.EncodeToString(signatureBytes)
,避免中间转换为 16 进制字符串。希望这篇文章能帮助你理解 HMAC 签名在不同语言中的编码差异,并在开发中避免类似的问题!
2025-02-27 14:42:30
在 DevOps 和开发流程中,如何安全高效地管理机密数据(如密码、API 密钥和认证信息)是一个重要话题。
Bitwarden 是一款开源密码管理工具,帮助用户存储、管理并共享敏感信息。Bitwarden 推出了新产品Secrets Manager,专为 DevOps 团队和开发人员提供简化的机密管理方案。
对于使用 GitHub Actions 等 CI/CD 工具的团队来说,Secrets 是一种存储机密信息的常见方式。
但是,GitHub Actions 中的 Secrets 一旦保存,就无法查看或修改,这使得本地保存机密变得繁琐且易出错。
而在团队环境中,个人和公司电脑之间的同步问题更是增加了额外的复杂性。
Bitwarden Secrets Manager 解决了这一难题,提供了安全、高效的机密存储与管理方式。
与传统的 Bitwarden 密码管理器类似,Secrets Manager 也支持 self-hosting,但需要授权才能进行。
为了简化流程(白嫖),也可以使用 Bitwarden 提供的在线服务,享受如下免费额度:
这些免费额度对于大多数小型或中型团队来说已足够使用。
Bitwarden Secrets Manager 便于与 GitHub Actions 等 CI/CD 服务进行集成。
以下是一个简单的示例,展示了如何在 GitHub Actions 中获取并使用存储在 Bitwarden 中的机密。
首先,在 GitHub Actions 的工作流 YAML 文件中,添加获取机密的步骤:
- name: Get Secrets
uses: bitwarden/sm-action@v2
with:
access_token: ${{ secrets.BW_ACCESS_TOKEN }}
base_url: https://vault.bitwarden.com
secrets: |
fc3a93f4-2a16-445b-b0c4-aeaf0102f0ff > SECRET_NAME_1
bdbb16bc-0b9b-472e-99fa-af4101309076 > SECRET_NAME_2
在上面的示例中,fc3a93f4-2a16-445b-b0c4-aeaf0102f0ff
和 bdbb16bc-0b9b-472e-99fa-af4101309076
是存储在 Bitwarden Secrets Manager 中的机密 ID,而 SECRET_NAME_1
和 SECRET_NAME_2
是引用机密的名称,用于在后续步骤中进行使用。
接着,在后续步骤中,使用这些机密值:
- name: Use Secret
run: SQLCMD -S MYSQLSERVER -U "$SECRET_NAME_1" -P "$SECRET_NAME_2"
完整示例:
- name: Get Secrets
uses: bitwarden/sm-action@v2
with:
access_token: ${{ secrets.BW_ACCESS_TOKEN }}
secrets: |
fc3a93f4-2a16-445b-b0c4-aeaf0102f0ff > GITHUB_GPG_PRIVATE_KEY
bdbb16bc-0b9b-472e-99fa-af4101309076 > GITHUB_GPG_PRIVATE_KEY_PASSPHRASE
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ env.GITHUB_GPG_PRIVATE_KEY }}
passphrase: ${{ env.GITHUB_GPG_PRIVATE_KEY_PASSPHRASE }}
git_user_signingkey: true
git_commit_gpgsign: true
更多集成方式请参考 官方文档。
为了便于在本地查询和管理机密,Bitwarden 提供了强大的 Secrets Manager CLI 工具。可以通过它来创建、删除、编辑和列出机密。
从 GitHub Releases 下载适合操作系统的可执行文件,运行以下命令以查看帮助信息:
bws --help
使用 Secrets Manager CLI 时,需先配置访问令牌(Access Token),然后运行如下命令列出机密:
# 设置环境变量
export BWS_ACCESS_TOKEN=xxxxxx
bws secret list
# 或者从命令行传入
bws secret list --access-token xxxxxx
为了进一步提高效率,写了一个小工具,帮助在 macOS 上快速查询并复制 Secrets。
以下是完整的 PHP 脚本示例:
<?php
$tokenName = !empty($argv[1]) ? trim($argv[1]) : '';
$accessToken = getenv('BWS_ACCESS_TOKEN');
$iconPngUrl = 'icon.png';
$json = `bws secret list -t '{$accessToken}'`;
if (!$json) {
echo json_encode(['items' => [['title' => 'Error: Failed to fetch secrets', 'valid' => false]]]);
exit;
}
$list = json_decode($json, true);
$items = [];
foreach ($list as $item) {
if (!empty($tokenName) && stripos($item['key'], $tokenName) === false) {
continue;
}
$items[] = [
'arg' => $item['value'],
'title' => $item['key'],
'subtitle' => $item['note'],
'icon' => ['path' => $iconPngUrl],
'valid' => true,
];
}
if (empty($items)) {
$items[] = [
'title' => 'No secrets found',
'valid' => false
];
}
echo json_encode(['items' => $items]);
exit;
Bitwarden Secrets Manager 为 DevOps 团队提供了一种更加安全、便捷的方式来管理和集成机密信息。
无论是与 GitHub Actions 集成,还是使用 CLI 工具进行本地管理,Bitwarden 都提供了简洁而强大的功能,帮助提升工作效率并确保敏感数据的安全。
2025-02-25 14:24:31
在 MySQL 数据库中,UTF-8 及其变体是最常用的字符集。
不同的 UTF-8 编码可能对大小写敏感性产生影响,主要包括以下几种:
utf8
:MySQL 早期的 UTF-8 实现,最多支持 3 字节,无法存储部分 Emoji 字符。utf8mb4
:MySQL 5.5+ 版本推荐使用的 UTF-8 编码,最多支持 4 字节,能够完整存储所有 Unicode 字符。MySQL 字符集搭配不同的排序规则(Collation)可能会影响查询的大小写敏感性。
常见的排序规则包括:
utf8_general_ci
/ utf8mb4_general_ci
:不区分大小写(Case Insensitive,ci
代表 Case Insensitive)。utf8_bin
/ utf8mb4_bin
:区分大小写(Binary,bin
代表按二进制存储,严格区分大小写)。utf8_unicode_ci
/ utf8mb4_unicode_ci
:更符合 Unicode 规范的排序方式,不区分大小写。默认情况下,utf8_general_ci
和 utf8mb4_general_ci
在搜索时是不区分大小写的。
当 MySQL 表的字符集设置为 utf8_general_ci
或 utf8mb4_general_ci
时,使用 LIKE
或 =
进行查询时,默认是不区分大小写的。
例如:
SELECT * FROM users WHERE username = 'admin';
如果数据库中存储了 Admin
、ADMIN
等,查询会返回这些所有匹配项。
如果需要执行区分大小写的查询,则需要:
ALTER TABLE users MODIFY username VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
这样查询就会严格区分 admin
和 Admin
。
SELECT * FROM users WHERE BINARY username = 'admin';
这样 admin
只会匹配完全相同的字符串,而不会匹配 Admin
、ADMIN
等。
whereRaw
进行原生查询在 ThinkPHP 框架中,默认的 where
方法不支持直接使用 BINARY
进行查询,但可以通过 whereRaw
方法执行 MySQL 原生查询。
$result = Db::table('users')
->whereRaw("BINARY username = ?", ['admin'])
->find();
$result = Db::table('users')
->whereRaw("BINARY username LIKE ?", ['%admin%'])
->select();
这种方法可以避免默认的大小写不敏感查询,让 MySQL 进行更严格的匹配。
utf8_general_ci
和 utf8mb4_general_ci
默认不区分大小写。BINARY
关键字。whereRaw
方法执行 MySQL 原生查询,确保大小写敏感匹配。这样,你就可以在 ThinkPHP 框架中更灵活地处理 MySQL 字符集大小写敏感的问题。
2025-01-15 18:09:21
在命令行界面(CLI)中输出带颜色的日志不仅能提升可读性,还能帮助开发人员在调试时迅速区分不同类型的日志信息。
通过使用 ANSI 转义序列,我们可以很方便地控制输出文本的颜色、样式和其他显示效果,如加粗、下划线、反显等。
本文将详细介绍如何使用这些序列输出带颜色的日志。
ANSI 转义序列是一种用于控制终端文本格式的字符序列。
它通常以 \033[
或 \e[
开头,后接不同的控制代码,最后以 m
结尾。
例如,\033[32m
表示设置文本颜色为绿色,\033[0m
用来重置样式。
利用 ANSI 转义序列,开发者可以灵活地在命令行中输出不同颜色和效果的文本。
\033[0m
:关闭所有属性,恢复为默认设置\033[1m
:设置高亮度(加深显示)\033[4m
:设置下划线\033[5m
:设置闪烁\033[7m
:反显(替换背景色和前景色)\033[8m
:消隐(隐藏文本)\033[30m
到 \033[37m
:设置前景色(字体颜色)\033[40m
到 \033[47m
:设置背景色\033[nA
:光标上移 n 行\033[nB
:光标下移 n 行\033[nC
:光标右移 n 行\033[nD
:光标左移 n 行\033[y;xH
:设置光标位置为 y 行 x 列\033[2J
:清屏\033[K
:清除从光标到行尾的内容\033[s
:保存光标位置\033[u
:恢复光标位置\033[?25l
:隐藏光标\033[?25h
:显示光标最简单的颜色控制是设置文本的前景色。例如,以下代码将输出绿色文本:
echo "\033[32m绿色\033[0m"
033[32m
设置文本为绿色,033[0m
用于重置所有属性,使后续输出恢复默认样式。
我们可以结合多种样式来增强文本的可读性。例如,下面的代码将输出一个带下划线的红色文本:
echo "\033[4;31m下划线红色\033[0m"
这里,4
表示下划线,31
表示红色。
除了颜色和样式,ANSI 转义序列还可以控制终端的其他行为,比如发出声音。
这行命令会在终端发出一声铃声,同时输出一段普通文本:
echo "\007发出'咚~'一声\033[0m"
请注意,在某些终端环境下,铃声可能不会响起,尤其是在没有扬声器的设备上。
你还可以同时设置文本的前景色和背景色。例如,以下代码将输出一个白色背景和红色前景的文本:
echo "\033[47;31m白底红字\033[0m"
47
是背景色(白色),31
是前景色(红色)。
通过组合多个效果,你可以创建更具视觉冲击力的输出。比如,以下代码将输出一个蓝色加粗下划线的文本:
echo "\033[1;4;34m蓝色加粗下划线\033[0m"
在这个示例中,1
表示加粗,4
表示下划线,34
表示蓝色。
ANSI 转义序列还允许控制光标的位置和终端屏幕的清理。例如:
echo "\033[2J" # 清屏
echo "\033[10;5H光标移动到第10行第5列\033[0m"
你还可以隐藏和显示光标:
echo "\033[?25l" # 隐藏光标
echo "\033[?25h" # 显示光标
通过使用 ANSI 转义序列,我们可以轻松地为命令行中的输出添加颜色和样式。
这不仅能让调试日志变得更加易读,还能增强命令行工具的用户体验。
你可以根据需求结合不同的颜色、效果和光标控制,创建自定义的命令行输出,通过这些技术,命令行的输出变得更加生动和富有表现力,有助于开发人员快速识别关键信息。
2025-01-02 12:05:37
今天在对接一个 API 的时候,发现需要生成高精度的时间戳,格式为yyyyMMddHH24mmssSSS
。
本文将介绍两种常见的实现方式,并讨论它们的优缺点。
格式 yyyyMMddHH24mmssSSS
的含义如下:
例如,时间 2025-01-02 11:30:45.123
的格式化结果为:20250102113045123
。
DateTime
类实现以下是使用 DateTime
类生成毫秒时间戳的代码示例:
<?php
$dateTime = new DateTime();
// 获取当前时间的微秒数并计算为毫秒
$milliseconds = intval($dateTime->format('u') / 1000);
// 格式化时间
$formattedTime = $dateTime->format("YmdHi") . $dateTime->format("s") . sprintf("%03d", $milliseconds);
echo $formattedTime;
$dateTime->format('u')
返回当前时间的微秒(6 位数,例如 123456
)。intval($dateTime->format('u') / 1000)
将微秒转换为毫秒(3 位数,例如 123
)。sprintf("%03d", $milliseconds)
确保毫秒部分始终为 3 位数(不足时补零)。假设当前时间为 2025-01-02 11:30:45.123456
,输出结果为:
20250102113045123
microtime
函数实现另一种方法是结合 microtime()
和 date()
函数:
<?php
$microtime = microtime(true);
// 格式化时间到秒
$formattedDate = date('YmdHis', floor($microtime));
// 获取毫秒部分
$milliseconds = sprintf('%03d', ($microtime - floor($microtime)) * 1000);
// 拼接毫秒
$formattedDate .= $milliseconds;
echo $formattedDate;
microtime(true)
返回当前 Unix 时间戳,包含秒和小数部分。floor($microtime)
获取整数秒部分。($microtime - floor($microtime)) * 1000
提取小数部分并转换为毫秒。假设当前时间为 2025-01-02 15:30:45.123456
,输出结果为:
20250102153045123
特性 | 使用 DateTime 类 |
使用 microtime()
|
---|---|---|
代码简洁性 | 更加现代化,语义清晰 | 较为传统,需要手动处理毫秒 |
精度 | 取决于系统支持的时间精度 | 依赖 microtime() 的实现 |
扩展性 | 更容易与其他 DateTime 操作结合 |
适合处理与 Unix 时间戳相关的逻辑 |
为了验证两种方法的输出是否一致,可以添加以下代码:
if ($formattedTime === $formattedDate) {
echo "两种方法的输出一致:$formattedTime\n";
} else {
echo "两种方法的输出不一致:\n第一种方法:$formattedTime\n第二种方法:$formattedDate\n";
}
DateTime
类,建议采用第一种方法,代码语义更加清晰。microtime()
或 Unix 时间戳直接交互,可以选择第二种方法。选择哪种方式主要取决于项目需求和代码风格偏好。希望本文对你在生成带毫秒的时间戳方面有所帮助!
2025-01-01 19:20:35
2024 年,对于我而言,是一段充满挑战与成长的旅程。
从工作中的挫折到家庭的变故,这一年让我更加体会到坚韧的意义,也在风雨中积累了成长的力量。
虽然充满波折,但我仍然选择在逆境中寻找方向,努力让自己变得更加强大。
在 2024 年中生活与工作的双重挑战交织而来,让我猝不及防。
其实在去年的年终总结中也提到了,自己已经内耗很久了,而且大部分同事其实也是,职业道路面临着严峻考验,加上大环境不好,工作和家庭多方面带来的压力并不小。
内耗了大概有快一年吧,今年最终也是爆发了:我和一位关系较好的同事一起离职了。
主要原因也是多方面,一是内耗这么久了,不想耗了,世界这么大我想去看看;二是自己的直性格和新同事的背刺;三是公司内部种种事件的发生,使得原本热爱的工作逐渐成为负担。
内耗严重,消耗了我的热情与精力。经过一段时间的挣扎,我决定离开这个让我不再快乐的环境。
离职后的日子可能并不轻松,但也让我重新掌握了生活的主动权,离职后也有听到有几位关系好的同事也先后离开了公司,活成了吃瓜群众...
因为房租还未到期,自己也选择了继续在青岛待上一段时间,跟关系好的同事们陆续告别,吃饭喝酒,侃侃而谈,大家也在聊着各自的看法和感受。
离职后,尝试让自己暂时放下压力,为未来的职业方向重新规划,也有疑问为什么不等到过年后再离职?答案还是上面提到的不想耗了,世界这么大我想去看看。
于是我也答应了跟我同一天离职的同事,帮她送兔子去北京,顺便溜达散心,从北京回来之后再起程回老家。
在北京我俩也是喝酒畅聊,还去了一趟雍和宫,在上班和上进之间选择了上香。时间仓促,第三天早上我便出发返程了,不料半路下雨,在出服务区时过了一个小水坑,结果车出现了问题(后来检查得知是电子转向机由于进水坏了),没办法只能一路坚持从高速开回了住处,找了地方维修车辆,避免耽误回老家。
由于转向机突然损坏,不得不承担一笔不小的维修费用,这让本不富裕的家庭雪上加霜,虽然看似是一次小意外,却让我深刻意识到,人生的计划往往会被突如其来的问题打断,我们能做的,是从容面对。
青岛的短暂停留给了我片刻喘息,与老同事的聚会让我感受到人情的温暖。然而,在房租到期后回到老家,奶奶的去世又一次让我面对人生的无常,同时也第一次在医院过夜。
这段时间,我更多地陪伴家人,感悟到亲情的重要性,也开始学会放慢脚步。
就在我努力整理心情,接受上海新工作的同时,命运再次掀起波澜:父亲突然离世,很急很急,我只能在视频中去医院之前见了最后一眼,没想到隔了不到几个小时就得到了人已经走了的消息。
这一噩耗让我深陷悲痛,当晚就请假第二天乘坐最早的飞机回家,奶奶去世时还有父亲在前面撑着,而父亲突然走了,让我手足无措,但我知道我不能垮掉,还有更多的事情在等着我处理。
这个家的生活还要继续,处理完父亲的身后事,直到三七结束,我也重返工作岗位。这段经历不仅考验了我的承受能力,也让我更加清楚地理解了责任的真正含义。
生活和工作的波折让我意识到,坚韧不仅仅是一种态度,更是一种能力。
面对工作中的失落,我学会了取舍,明白了有时候“止损”是对自己最好的成全;面对生活中的离别,我选择直面悲伤,在亲情的回忆中汲取前行的力量。
这一年,尽管前路坎坷,但我始终没有停下脚步。
在适应新的工作节奏中,我不断调整状态,力求用最好的表现迎接挑战。生活的种种考验让我学会了平衡,懂得了如何在压力中找到一丝安慰,让自己变得更坚强、更从容。
2024 年的经历让我深刻体会到,成长往往藏在不经意的每一个瞬间。
离职后的三个月是我重新审视自己的机会,除了本质工作以外,开源事业也是我发自内心热爱的方向。
Docsify 成为 Gitee 成为 GVP 的那一刻,我感受到努力被认可的喜悦,也让我更加坚定地投入到技术与开源社区中。
此外,成为 Apache Answer 的 PPMC Member 也是今年的一大收获。
这份荣誉不仅是一种认可,更是一份责任,促使我不断提升自己,为开源社区贡献更多价值。
无论是在工作还是生活中,我都在这些起起伏伏中找到了成长的契机。
从经历挫折到收获信心,这一年的每一步都让我离更好的自己更近了一些。
回顾 2024 年,我经历了从失望到解脱,从低谷到重生的全过程。
这些经历让我更加明白,生活并不会因为我们的疲惫而停下脚步,而我们能做的就是在每一次风雨中找到支撑自己的力量。
展望未来,我希望自己能更加沉着应对工作中的挑战,也会投入更多精力到开源社区中,用技术赋能更多人,在社区活动中认识更多人;在生活中,我会更加珍惜与家人、朋友的每一刻,用心感受生活带来的温暖与感动。
这一年让我更加坚信,无论面对什么,只要心中有光,终能走出阴霾。
2025 年,希望是一个充满希望和新机遇的年份,我期待着更多的可能性,也期待着与成长同行,继续前行。