编码算法与常见的哈希算法总结

编码算法  

        首先,我们一起来学习一下编码算法,要学习编码算法首先我们就要了解一下什么是编码?

        举例说明,ASCII码就是我们常见的一种编码,字母a的编码是十六进制的0x61,字母b是0x62,以此类推。

字母ASCII码

a

0x61
b0x62
c0x63
......

接下来我们开始学习一下编码算法。

        今天我们主要以URL编码和Base64编码为例,为大家讲解一下。

URL编码

        URL 编码是浏览器发送数据给服务器时使用的编码,它通常附加在URL的参数部分,例如:https://www.baidu.com/s?wd=%E4%B8%AD%E6%96%87。

        而Java标准库也提供了一个URLEncoder类来对任意字符串进行URL编码,如果服务器收到了URL编码的字符串,就可以对其进行解码,还原成原始字符串。

package com.zwd.demo01;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;

public class Test01 {
	public static void main(String[] args) throws UnsupportedEncodingException {
		String url = "http://www.baidu.com/s?wd=";
		String value = "西安";
		
		// 对URL中的中文进行编码
		String result = URLEncoder.encode(value, "utf-8");
		System.out.println(result);
		System.out.println(url + result);
		
		// 对URL中的中文进行解码
		String param = "https://www.baidu.com/s?wd=%E6%88%91%E6%9C%AC%E5%B0%86%E5%BF%83%E5%90%91%E6%98%8E%E6%9C%88";
		String content = URLDecoder.decode(param, "utf-8");
		System.out.println(content);
	}
}

Base64编码

        URL编码是对字符进行编码,表示成%xx的形式,而Base64编码编码是对二进制数据进行编码,表示成文本格式。

首先,我们可以读取图片。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;

public class Test02 {
	public static void main(String[] args) throws IOException {
		// 读取图片(字节数组)
		byte[] imageByteArray = Files.readAllBytes(Paths.get("D:\\net\\1.jpg"));
		
		// 将字节数组进行Base64编码,转换成"字符串形式"
		String imageDataStr = Base64.getEncoder().encodeToString(imageByteArray);
		
		// Base64解码
		byte[] imageResultByteArray = Base64.getDecoder().decode(imageDataStr);
		Files.write(Paths.get("D:\\http\\coder\\帅哥.jpg"), imageResultByteArray);
	}
}

也可以从文本文件中读取一张图片的Base64编码值

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.List;

public class Test03 {
	public static void main(String[] args) throws IOException {
		// 从文本文件中读取一张图片的Base64编码值
		List<String> lines = Files.readAllLines(Paths.get("D:\\net\\周杰伦.txt"));
		StringBuilder sb = new StringBuilder();
		for(String ln : lines) {
			sb.append(ln);
		}
		System.out.println(sb.length());
		
		// Base64解码
		byte[] imageByteArray = Base64.getDecoder().decode(sb.toString());
		Files.write(Paths.get("D:\\http\\coder\\jay.jpg"), imageByteArray);
	}
}

还有从文本文件中读取一首mp3的Base64编码值

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.List;

public class Test04 {
	public static void main(String[] args) throws IOException {
		// 从文本文件中读取一首mp3的Base64编码值
		List<String> lines = Files.readAllLines(Paths.get("D:\\http\\coder\\mojito.txt"));
		StringBuilder sb = new StringBuilder();
		for(String ln : lines) {
			sb.append(ln);
		}
		
		// Base64解码
		byte[] mp3ByteArray = Base64.getDecoder().decode(sb.toString());
		Files.write(Paths.get("D:\\music\\mojito.mp3"), mp3ByteArray);
	}
}

哈希算法

        哈希算法( Hash )又称摘要算法( Digest)。它的作用是:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。

        哈希算法的目的:推为了验证原始数据是否被篡改。

        哈希算法最重要的特点就是:相同的输入-定得到相同的输出;不同的输入大概率得到不同的输出。

        还有一个概念,我们需要了解,就是哈希碰撞。

        哈希碰撞就是指:两个不同的输入,得到了相同的输出,举例说明:

"AaAaAa".hashCode(); // 0x7460e8c0
"BBAaBB".hashCode(); // 0x7460e8c0

"通话".hashCode(); // 0x11ff03
"重地".hashCode(); // 0x11ff03

        有的人就会问了:哈希碰撞能不能避免?答案是:不可以。因为输出的字节长度是固定的,String的hasjCode()输出的是4字节的整数,它最多只有4294967296种输出结果,但是输入的数据长度是不固定的,有无数种的输入可能,所以它必然会产生哈希碰撞。

        哈希算法的安全性也是由哈希碰撞的概率高低决定的,一个安全的哈希算法必须满足:碰撞概率低;不能猜测输出。

不能猜测输出是指:输入的任意一个bit的变化会造成输出结果完全不同,这样就很难从输出反推输入。

常用的哈希算法

算法输出长度(位)输出长度(字节)
MD5128 bits

16 bytes

SHA-1160 bits20 bytes
RipeMD-160160 bits20 bytes
SHA-256256 bits32 bytes
SHA-512512 bits64 bytes

哈希算法的输出长度越长,就越难产生碰撞,也就越安全。

MD5算法

我们首先以MD5为例,一起来看如何对输入计算哈希:

        首先,我们需要创建一个MessageDigest对象,这是为了获取基于MD5加密算法的工具对象。

        然后,使用updata()方法,对原始数据进行更新。

        接着,创建一个字节数组用于接收加密后的数据。因为digest()方法的返回值是一个字节数组。

        最后,我们可以得到加密后的数据,把它转换成十六进制的字符串。

我们可以看到,只要输入的内容一致,不管是怎样输入,输出结果都是一样的。

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

public class Test01 {
	public static void main(String[] args) {
		try {
			// 获取基于MD5加密算法的工具对象
			MessageDigest md5 = MessageDigest.getInstance("MD5");
			
			// 更新原始数据
			md5.update("hello".getBytes());
			md5.update("world".getBytes());
			
			// 获取加密后的结果
			byte[] resultByteArray = md5.digest();
			System.out.println(Arrays.toString(resultByteArray));
			
			// 只要内容相同,加密的结果相同
			MessageDigest tempMd5 = MessageDigest.getInstance("MD5");
			tempMd5.update("helloworld".getBytes());
			byte[] tempResultByteArray = tempMd5.digest();
			System.out.println(Arrays.toString(tempResultByteArray));
			
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
	}
}

输出结果:

 我们也可以将最后得到的加密数据使用StirngBulider将其拼接起来。

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class Test02 {
	public static void main(String[] args) {
		String password = "wbjxxmynhmyzgq";
		
		try {
			// 根据当前算法,获取加密工具对象(摘要)
			MessageDigest digest = MessageDigest.getInstance("MD5");
			
			// 更新原始数据
			digest.update(password.getBytes());
			
			// 加密后的字节数组,转换字符串
			byte[] resultByteArray = digest.digest();
			StringBuilder result = new StringBuilder();
			
			for(byte bite : resultByteArray) {
				result.append(String.format("%02x", bite));
			}
			System.out.println(result);
			System.out.println(result.length());
			
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
				
	}
}

输出结果:

        在 使用哈希口令时,还要注意防止彩虹表攻击。

        什么是彩虹表呢?上面讲到了,如果只拿到MD5,从MD5反推明文口令,只能使用暴力穷举的方法。然而黑客并不笨,暴力穷举会消耗大量的算力和时间。但是,如果有一个预先计算好的常用口令和它们的MD5的对照表,这个表就是彩虹表。如果用户使用了常用口令,黑客从MD5一下就能反查到原始口令。

        对于这种情况,我们可以选择对MD5进行加盐(salt),来抵御彩虹表攻击。

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.UUID;

public class Test04 {
	public static void main(String[] args) {
		String password = "wbjxxmynhmyzgq";
		String salt = UUID.randomUUID().toString().substring(0, 5);
		System.out.println(salt);
		
		try {
			// 获取MD5算法的工具对象
			MessageDigest digest = MessageDigest.getInstance("MD5");
			digest.update(password.getBytes());
			digest.update(salt.getBytes());
			
			byte[] reaultByteArray = digest.digest();
			
			System.out.println(Arrays.toString(reaultByteArray));
			System.out.println(reaultByteArray);
			
			StringBuilder result = new StringBuilder();
			for(byte b : reaultByteArray) {
				result.append(String.format("%02x", b));
			}
			System.out.println(result);
			
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
	}
}

 SHA-1算法

        在Java中,SHA-1的使用方法和MD5完全一样,只需要把算法的名称改为"SHA-1"即可。

public class demo {
	public static void main(String[] args)  {
		// 创建一个MessageDigest实例:
        MessageDigest md = MessageDigest.getInstance("SHA-1");
       
        // 反复调用update输入数据:
        md.update("Hello".getBytes("UTF-8"));
        md.update("World".getBytes("UTF-8"));
        
        // 20 bytes: db8ac1c259eb89d4a131b253bacfca5f319d54f2
        byte[] results = md.digest(); 

        StringBuilder sb = new StringBuilder();
        for(byte bite : results) {
        	sb.append(String.format("%02x", bite));
        }
        
        System.out.println(sb.toString());
	}
}

Hamc算法

        接着,我们再了解一下Hamc算法。我们先回顾一下上面提到的哈希算法:(digest = hash(input)),为了抵御彩虹表攻击,我们会选择加盐,其目的就是为了能让输出有所变化:(digest = hash(salt + input))。

        而Hamc算法就是一种基于密钥的消息认证码算法,它的全称是:Hash-based Message Authentication Code,是一种更安全的消息摘要算法。

        Hmac算法总是以某种算法配合起来使用的,我们就以MD5为例。这样就可以看成HmacMD5,它可以看作带有一个安全的key的MD5。相比较于加salt的MD5它的好处有:

        ·HmacMD5使用的key长度是64字节,更加安全。

        ·Hmac是标准算法,同样适用于SHA-1等其他哈希算法。

        ·Hmac输出和原有的哈希算法长度一致。

第一:通过名称HmacMD5获取KeyGenerator实例。

第二:通过KeyGenerator创建一个SecretKey实例。

第三:通过名称HmacMD5获取Mac实例。

第四:用SecretKey初始化Mac实例。

第五:对Mac实例反复调用updata()方法输入数据。(此方法需传入字节数组)

第六:调用Mac实例的doFinal()获取最终的哈希值。

以下提供HmacMD5的参考代码:

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;

public class Test06 {
	public static void main(String[] args) {
		String password = "wbjxxmynrmyzgq";
		try {
			// 1. 生成密钥
			// 密钥生成器KeyGenerator
			KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5");
			
			// 生成密钥
			SecretKey key = keyGen.generateKey();
			
			// 获取密钥key的字节数组(64)
			byte[] keyByteArray = key.getEncoded();
			System.out.println("密钥长度:" + keyByteArray.length + "字节");
			
			StringBuilder keyStr = new StringBuilder();
			for(byte b : keyByteArray) {
				keyStr.append(String.format("%02x", b));
			}
			System.out.println("密钥内容:" + keyStr);
			System.out.println("密钥内容的长度:" + keyStr.length());
			
			// 2.使用密钥,进行加密
			// 获取算法对象
			Mac mac = Mac.getInstance("HmacMD5");
			
			// 初始化密钥
			mac.init(key);
			
			// 更新加密内容
			mac.update(password.getBytes());
			
			// 加密
			byte[] resultByteArray = mac.doFinal();
			
			System.out.println("加密结果:" + resultByteArray.length + "字节");
			
			StringBuilder resultStr = new StringBuilder();
			for(byte b : resultByteArray) {
				resultStr.append(String.format("%02x", b));
			}
			System.out.println("加密结果:" + resultStr);
			System.out.println("加密结果的长度:" + resultStr.length());
			
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			e.printStackTrace();
		}
	}
}

通过Hmac计算了哈希和SecretKey,如果我们想要验证该怎么办呢?

这时SecretKey不能从KeyGenerator生成了,而是从一个byte[]数组恢复:

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class Test07 {
	public static void main(String[] args) {
		// 原始密码
		String password = "nhmyzgq";
		
		try {
			// 通过"密钥的字节数组",恢复密钥
			byte[] keyByteArray = {126, 49, 110, 126, -79, -5, 66, 34, -122, 123, 107, -63, 106, 100, -28, 67, 19, 23, 1, 23, 47, 63, 47, 109, 123, -111, -27, -121, 103, -11, 106, -26, 110, -27, 107, 40, 19, -8, 57, 20, -46, -98, -82, 102, -104, 96, 87, -16, 93, -107, 25, -56, -113, 12, -49, 96, 6, -78, -31, -17, 100, 19, -61, -58};
			SecretKey key = new SecretKeySpec(keyByteArray, "HmacMD5");
			
			// 加密
			Mac mac = Mac.getInstance("HmacMD5");
			mac.init(key);
			mac.update(password.getBytes());
			byte[] resultByteArray = mac.doFinal();
			
			StringBuilder resultStr = new StringBuilder();
			for(byte b : resultByteArray) {
				resultStr.append(String.format("%02x", b));
			}
			System.out.println(resultStr);
			
		} catch (InvalidKeyException e) {
			e.printStackTrace();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (IllegalStateException e) {
			e.printStackTrace();
		}
	}
}

恢复SecretKey的语句就是new SecretKeySpec(keyByteArray, "HmacMD5");

RipeMD160算法

        最后,我们再浅浅的说一下RipeMD160算法。RipeMD160算法是一种基于Merkel-Damgard结构的加密哈希函数。 RipeMD160算法在java标准库中并没有提供,所以我们需要使用BouncyCastle提供的第三方开源jar包手动导入,我们可以从官方网站下载jar包。

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.Arrays;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

public class Main01 {
	public static void main(String[] args) {
		try {
			// 注册BouncyCastle提供的通知类对象BouncyCastleProvider
			Security.addProvider(new BouncyCastleProvider());
			
			// 获取RipeMD160算法的"消息摘要类"(加密对象)
			MessageDigest digest = MessageDigest.getInstance("RipeMD160");
			
			// 更新原始数据
			digest.update("helloWorld".getBytes());
			
			// 获取消息摘要(加密)
			byte[] result = digest.digest();
			
			// 消息摘要的字节长度和内容
			System.out.println(result.length); // 160位 = 20字节
			System.out.println(Arrays.toString(result));
			
			// 16进制内容字符串
			String hex = new BigInteger(1, result).toString(16);
			System.out.println(hex.length()); // 20字节= 40字符
			System.out.println(hex);
			
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
	}
}

  • 24
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值