Java:实现SHA-1加密算法(附带源码)
一、项目背景详细介绍
在信息安全领域,数据完整性与身份认证至关重要。哈希算法(Hash Function)能够将任意长度的输入数据映射为固定长度的摘要,用于校验数据在传输或存储过程中的完整性。SHA-1(Secure Hash Algorithm 1)是美国国家安全局(NSA)设计并由美国国家标准与技术研究所(NIST)发布的加密哈希函数,输出 160 位(20 字节)的摘要。虽然 SHA-1 已被发现存在碰撞风险,但它曾是广泛使用的标准,并在许多老系统、数字签名和版本控制(如 Git 对象校验)中仍有应用。
通过在 Java 中实现 SHA-1,可以帮助学习者深入理解哈希算法的内部结构,如分组处理、消息填充、迭代压缩函数等;同时掌握 Java 原生 MessageDigest 的使用,并可根据需要定制轻量级纯 Java 版实现,用于教育或兼容性场景。
二、项目需求详细介绍
本项目旨在用 Java 语言实现 SHA-1 摘要计算,包括两种方案:调用 Java 原生库和手写纯 Java 算法,要求如下:
-
原生库方案
-
提供方法
String sha1WithJdk(String input),使用java.security.MessageDigest计算给定字符串的 SHA-1 值,并返回 40 位十六进制字符串; -
支持对文件流或字节数组计算,提供
byte[] sha1Bytes(byte[] data)方法。
-
-
纯 Java 手写方案
-
不依赖
MessageDigest,从消息填充、分组、迭代压缩函数到输出生成,手动实现完整 SHA-1 算法; -
提供方法
String sha1Manual(String input),返回十六进制字符串;
-
-
命令行工具模式
-
提供
Main类,支持参数:-
-s --string <text>:对输入字符串计算 SHA-1; -
-f --file <path>:对指定文件计算 SHA-1; -
--mode <jdk|manual>:选择实现方案,默认 jdk;
-
-
-
单元测试覆盖
-
使用 JUnit 5 对两种方案和边界情况(空串、长文本、二进制文件)进行测试,确保结果一致;
-
-
性能对比
-
在 README 中记录调用原生库与手写算法的性能差异,可选使用 JMH 简单基准测试;
-
-
工程化与可扩展性
-
将算法封装在
Sha1Util工具类,私有构造防止实例化; -
未来可通过接口
HashUtil扩展为 MD5、SHA-256 等多种算法。
-
三、相关技术详细介绍
为实现上述需求,将涉及以下技术栈与概念:
-
Java 安全包
-
java.security.MessageDigest:调用getInstance("SHA-1")、update、digest完成摘要; -
java.nio与java.io:高效读取文件并分块更新摘要。
-
-
二进制与位操作
-
手写方案需对 32 位字进行左旋位操作
((x << n) | (x >>> (32-n))); -
按大端序将消息字节转换为 32 位字。
-
-
消息填充(Padding)
-
按照 SHA-1 规范:先添 0x80,再补 0x00 若干,最后附加 64 位原始长度,确保消息长度为 512 的倍数。
-
-
分组与迭代压缩
-
将填充后的消息分为 512 位(16 个 32 位字)一组;
-
扩展为 80 个 32 位工作字;
-
在 80 轮循环中,根据轮数选择不同常量和逻辑函数,更新 5 个 32 位寄存器 A–E。
-
-
命令行解析
-
使用 Apache Commons CLI 解析用户输入参数,生成帮助提示。
-
-
单元测试与基准测试
-
JUnit 5 验证示例向量(如 RFC 3174 中给出的 SHA-1 结果);
-
如需性能对比,可使用 JMH 进行简单基准测试。
-
四、实现思路详细介绍
-
工具类
Sha1Util-
私有构造;
-
静态方法分两组:基于 JDK 的
sha1WithJdk/sha1Bytes,手写算法的sha1Manual; -
公用方法
toHexString(byte[] bytes)将字节数组转为十六进制字符串。
-
-
原生库实现
-
调用
MessageDigest.getInstance("SHA-1"),对输入 UTF-8 编码字节调用update,最后digest; -
直接复用
toHexString。
-
-
手写实现流程
-
将输入字符串转换为字节数组(UTF-8);
-
消息填充:计算补齐长度,拼接 0x80、0x00、长度值;
-
分组:每 64 字节(512 位)为一组,转换为 16 个 32 位大端整数;
-
消息扩展:创建 80 长度的
W数组,前 16 个直接赋值,后 64 个W[t] = leftRotate(W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16], 1); -
初始化五个寄存器
h0…h4为标准初始值; -
80 轮循环中根据
t选择常量K[t]和函数f(t, B, C, D),并更新A…E; -
最后将初始寄存器与当前寄存器相加并截断为 32 位;
-
拼接
h0…h4,转换为字节序列并返回十六进制。
-
-
命令行入口
Main-
定义选项
-s/--string、-f/--file、--mode; -
根据
mode调用两种方案,并打印结果; -
对文件处理使用流分块,调用
sha1Bytes或手写算法逐块填充再压缩。
-
五、完整实现代码
// 文件:src/main/java/com/example/util/HeapSortUtil.java
package com.example.util;
/**
* HeapSortUtil 提供大顶堆/小顶堆的构建、排序与动态操作
*/
public class HeapSortUtil {
private HeapSortUtil() {}
private static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
// —— 大顶堆 ——
public static void buildMaxHeap(int[] arr) {
int n = arr.length;
for (int i = n / 2 - 1; i >= 0; i--) {
heapifyMax(arr, n, i);
}
}
private static void heapifyMax(int[] arr, int heapSize, int root) {
int largest = root;
int l = 2 * root + 1, r = 2 * root + 2;
if (l < heapSize && arr[l] > arr[largest]) largest = l;
if (r < heapSize && arr[r] > arr[largest]) largest = r;
if (largest != root) {
swap(arr, root, largest);
heapifyMax(arr, heapSize, largest);
}
}
public static void heapSortAsc(int[] arr) {
buildMaxHeap(arr);
for (int i = arr.length - 1; i > 0; i--) {
swap(arr, 0, i);
heapifyMax(arr, i, 0);
}
}
// —— 小顶堆 ——
public static void buildMinHeap(int[] arr) {
int n = arr.length;
for (int i = n / 2 - 1; i >= 0; i--) {
heapifyMin(arr, n, i);
}
}
private static void heapifyMin(int[] arr, int heapSize, int root) {
int smallest = root;
int l = 2 * root + 1, r = 2 * root + 2;
if (l < heapSize && arr[l] < arr[smallest]) smallest = l;
if (r < heapSize && arr[r] < arr[smallest]) smallest = r;
if (smallest != root) {
swap(arr, root, smallest);
heapifyMin(arr, heapSize, smallest);
}
}
public static void heapSortDesc(int[] arr) {
buildMinHeap(arr);
for (int i = arr.length - 1; i > 0; i--) {
swap(arr, 0, i);
heapifyMin(arr, i, 0);
}
}
// —— 动态堆操作 ——
public static int maxHeapInsert(int[] heap, int size, int value) {
heap[size] = value;
int i = size;
while (i > 0) {
int p = (i - 1) / 2;
if (heap[p] < heap[i]) {
swap(heap, p, i);
i = p;
} else break;
}
return size + 1;
}
public static int maxHeapExtract(int[] heap, int size) {
swap(heap, 0, size - 1);
heapifyMax(heap, size - 1, 0);
return size - 1;
}
public static int minHeapInsert(int[] heap, int size, int value) {
heap[size] = value;
int i = size;
while (i > 0) {
int p = (i - 1) / 2;
if (heap[p] > heap[i]) {
swap(heap, p, i);
i = p;
} else break;
}
return size + 1;
}
public static int minHeapExtract(int[] heap, int size) {
swap(heap, 0, size - 1);
heapifyMin(heap, size - 1, 0);
return size - 1;
}
}
六、代码详细解读
-
swap
通用交换方法,将数组中索引i、j的元素互换。 -
buildMaxHeap / heapifyMax
从最后一个非叶子节点开始,调用heapifyMax构建大顶堆。heapifyMax比较根与左右子节点,找到最大者并交换后递归下沉。 -
heapSortAsc
先构建大顶堆,再将堆顶(最大值)与末尾交换,缩小堆大小并对新根调用heapifyMax,反复直至排序完成,得到升序数组。 -
buildMinHeap / heapifyMin / heapSortDesc
与大顶堆对称:保证根最小,排序过程中将最小值移至末尾,得到降序结果。 -
maxHeapInsert / minHeapInsert
插入新元素到“堆尾”后,进行向上“浮”调整:与父节点比较并交换,直至父节点满足堆序。 -
maxHeapExtract / minHeapExtract
弹出堆顶:将堆顶与尾元素交换、缩减堆大小,再对新根执行heapify恢复堆结构。
七、项目详细总结
本项目实现并封装了大顶堆与小顶堆的核心算法,涵盖:
-
原地堆构建:
buildMaxHeap、buildMinHeap,时间 O(n)。 -
堆排序:
heapSortAsc(升序)、heapSortDesc(降序),整体时间 O(n log n),空间 O(1)。 -
动态堆操作:插入与弹出方法模拟优先队列基本功能。
项目结构清晰、方法职责单一,可直接用于排序工具或优先队列实现。通过掌握堆在数组中的索引映射与调整逻辑,为实现更复杂的数据结构(如并行堆、外部堆)奠定基础。
八、项目常见问题及解答
Q1:为什么堆排序是原地排序?
堆排序仅需在输入数组上原地调整和交换,不依赖额外同规模数组,空间复杂度 O(1)。
Q2:heapify 从哪儿开始执行?
叶子节点无需调整,从最后一个非叶子节点 n/2−1 向前依次执行 heapify 即可覆盖所有子树。
Q3:递归 vs 迭代 heapify?
递归实现简洁但有函数调用开销,可重写为循环版本减少栈深度和调用成本。
Q4:数组容量不足如何动态扩容?
可配合 ArrayList 或手动在插入前检查容量并按需翻倍扩容。
Q5:何时使用小顶堆 vs 大顶堆?
大顶堆用于升序排序或实时最大值获取,小顶堆用于降序排序或实时最小值获取;优先队列常根据业务需求选择。
九、扩展方向与性能优化
-
迭代版
heapify
将递归改为循环,减少函数调用和栈开销。 -
并行堆构建
对巨型数组分块并行heapify,再合并堆顶,利用多核加速。 -
泛型支持
将算法泛型化,支持任意Comparable<T>对象数组。 -
基于文件的外部堆
面对超大数据无法全部加载内存,可设计外部堆或分块堆进行外存排序。 -
基准测试
使用 JMH 对不同数据规模、数据分布(随机、有序、逆序)进行性能测试,指导生产环境选择。 -
优先队列封装
在此基础上实现完整PriorityQueue,提供peek、poll、offer等 API。
更多推荐

所有评论(0)