1. 常用加密算法
这部分会分别介绍消息摘要算法、对称加密算法、非对称加密算法,关于具体的密码学原理不做说明,而是根据java提供的API看看如何使用这些加密算法。关于这些加密算法相信你的项目可能或多或少会用到,比如保存用户的密码肯定会用到,小点的公司至少会直接用到消息摘要算法。大公司可能专门提供加密功能的服务,应用服务器来调用该加密服务来对信息进行加密。
1.0. Base64/Hex
Base64编码 : 网上一搜Base64就会出现大量的Base64加密解密字眼,其实Base64只是一种编码方式,用以将字节数组转换为易读的字符串,并不是什么加密算法。只是Base64会经常配合加密算法一起使用,使用加密算法得到的都是字节数组,如果使用new String(bytes)十有八九会得到这样����р�j���8l[难以阅读和保存的字符串,如果使用Base64对字节数组进行编码就会得到这样的一串字符串XrY7u Ae7tCTyyK7j1rNww==, 推荐使用commons-codec库,依赖如下:
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.10</version>
</dependency>
private static String base64Encode(byte[] data) throws Exception {
String base64String = Base64.encodeBase64String(data);
return base64String; // 比如"hello world" -> aGVsbG8gd29ybGQ=
}
private static byte[] base64Decoder(String base64Data) throws Exception{
byte[] bytes = Base64.decodeBase64(base64Data);
return bytes;
}
// Hex是16进制编码方式,和Base64类似 6c91ad368187c02bbbddc2f09ff2a87e
private static String hex(byte[] data) throws Exception{
return Hex.encodeHexString(data);
}
1.1. 消息摘要
消息摘要算法使用HASH函数对消息进行加密后,会产生长度固定的加密串,称为摘要。接收者会对接收到的消息使用相同的HASH函数重新计算,将产生的新的摘要和原来的摘要进行对比,如果不一致说明消息在传递过程中被篡改了。
1.1.1. MD5/SHA-1
MD5即Message Digest Algorithm 5,为消息摘要的一种实现,摘要长度为128位,目前使用广泛; SHA-1, Secure Hash Algorithm,为消息摘要的一种实现,摘要长度为160位,相比MD5更安全,相对的计算速度更慢。
MD5/sha-1的使用:
public static String md5(byte[] data) throws Exception {
MessageDigest alg = MessageDigest.getInstance("md5"); // 或者传入sha-1
byte[] content = alg.digest(data);
return base64Encode(content);
}
一般来说MD5通过彩虹表(一张使用各种hash算法生成的明文密文对照表)还是可以破解的,通常会考虑“加盐(salt)”,这个salt可以看作是一个额外的“认证码”,同样的输入,不同的认证码,会产生不同的输出。因此,要验证输出的哈希,必须同时提供“认证码”,也不用自己手动去搞了,使用下面介绍的Hmac算法吧。
1.1.2. Hmac
Hmac全称为Hash-based Message Authentication Code, 就是一种基于密钥的消息认证码算法,也是一种更安全的消息摘要算法。Hmac算法总是和某种哈希算法配合起来用的。例如,我们使用MD5算法,对应的就是HmacMD5算法,它相当于“加盐”的MD5,同时生成密文的长度和使用源HASH算法生成的密文长度一样。比如还有:
HmacMD5/HmacSHA1/HmacSHA256/HmacSHA384/HmacSHA512。
private static String hmac = "HmacMD5";
public static String generateHmacSignSign(String textToSign, String key) throws Exception {
// 传入key和加密算法,也可以使用KeyGenerator生成(后续有使用)这样就不需要自己传入key了,具体得看业务需求
SecretKey secretKey = new SecretKeySpec(base64Decoder(key), hmac);
Mac mac = Mac.getInstance(hmac);
mac.init(secretKey);
byte[] data = mac.doFinal(textToSign.getBytes("utf-8"));
return Hex.encodeHexString(data);
}
1.2. 对称加密算法
数据发送方将明文和加密密钥一起经过特殊加密算法后,生成密文发送给接收方,接收方若想读取明文,则需要使用相同的密钥和逆算法去读取(要求双方事先知道密钥)。计算量小,加密速度快,加密效率高,但是密钥保护对加密信息安全很重要。
常见的有DES,3DES,AES,DES的密钥为64位(其实只要56位,8位位检验位),将明文按照64位进行分组,然后与密钥安位进行替代或交换形成密文;由于容易被破解,所以又有了3DES,使用3个长度为56的密钥与明文进行三次运算,为DES向AES的过渡算法;使用最为广泛的还是AES,设计有128/192/256位长度的密钥,相比前两种安全性更高。
下面为AES的使用,对于DES也是类似,只需要将对应的字符串aes换成des即可。
private static Key aesKey;
static {
try{
KeyGenerator keyGenerator = KeyGenerator.getInstance("aes"); //构造密钥生成器,指定为AES算法
keyGenerator.init(128); // 生成一个128位的随机源
SecretKey secretKey = keyGenerator.generateKey(); // 产生原始对称密钥
String encoded = base64Encode(secretKey.getEncoded()); // 通常会使用base64编码后保存起来
aesKey = new SecretKeySpec(base64Decoder(encoded), "aes");
}catch (Exception e) {
}
}
/**
* AES加密
*/
public static String aesEncrypt(String content) throws Exception {
byte[] bytes = content.getBytes("utf-8");
Cipher cipher = Cipher.getInstance("aes");
cipher.init(Cipher.ENCRYPT_MODE, aesKey); // 加密模式
byte[] result = cipher.doFinal(bytes);
return base64Encode(result);
}
/**
* AES解密
*/
public static String aesDecrypt(String content) throws Exception {
byte[] bytes = base64Decoder(content);
Cipher cipher = Cipher.getInstance("aes");
cipher.init(Cipher.DECRYPT_MODE, aesKey); // 解密模式
byte[] result = cipher.doFinal(bytes);
System.out.println(new String(result));
return base64Encode(result);
}
1.3. 非对称加密算法
甲方生成一对密钥,公钥向外公开,乙方使用明文和公钥加密产生密文,发送给乙方,乙方使用私钥进行解密。安全性高,但是复杂导致速度慢。
常用的实现为RSA算法
private static Map<String, String> map;
static {
try {
KeyPairGenerator generator = KeyPairGenerator.getInstance("rsa");
generator.initialize(512);
KeyPair keyPair = generator.generateKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
PublicKey publicKey = keyPair.getPublic();
String first = base64Encode(publicKey.getEncoded());
String second = base64Encode(privateKey.getEncoded());
map = new HashMap<>();
map.put("first", first);
map.put("second", second);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* RSA加密
*/
public static String rsaEncrypt(String content) throws Exception {
byte[] publicBytes = base64Decoder(map.get("first"));
KeyFactory keyFactory = KeyFactory.getInstance("rsa");
PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(publicBytes));
Cipher cipher = Cipher.getInstance("rsa");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] result = cipher.doFinal(content.getBytes("utf-8"));
return base64Encode(result);
}
/**
* RSA解密
*/
public static String rsaDecrypt(String content) throws Exception {
byte[] privateKeyByte = base64Decoder(map.get("second"));
KeyFactory keyFactory = KeyFactory.getInstance("rsa");
PrivateKey privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyByte));
Cipher cipher = Cipher.getInstance("rsa");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] result = cipher.doFinal(base64Decoder(content));
System.out.println(new String(result));
return base64Encode(result);
}
2. 利用加密算法实现一个可用的token令牌
需要进行身份验证的时候,目前比较流行的是服务端来下方令牌token,后续客户端带着这个令牌来请求服务端,服务端再进行身份验证。
Token有两个很重要的特性:
一个是随机性,不可预测,当然token会包含一些必要的信息,比如实例信息;另一个是存在过期时间;当然还有再刷新refresh功能,但是首要的是满足上面两个条件,下面就写一个可以实际使用的token。其中,createToken(T tokenBody, int minute)表示生成token,verify为检验token.。import com.googlemon.base.Joiner;
import com.googlemon.base.Splitter;
import com.google.gson.*;
import org.apachemonsdec.binary.Base64;
import org.apachemons.lang3.RandomStringUtils;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;
public class TokenUtil {
private static Key aesKey = getAESKey();
private static String hmacKey = "wSpJSBADgHgGIjdA9jvLzQ==";
private static class Status {
private int code;
private String message;
public Status(int code, String message) {
thisde = code;
thisssage = message;
}
@Override
public String toString() {
return "Status{"
"code=" code
", message='" message '\''
'}';
}
}
static class Student {
int id;
String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
}
/**
* 校验token,主要是校验两个方面
* 1、校验签名是否合法
* 2、校验token是否过期
* @param token
* @return
*/
public static Status verify(String token) {
List<String> list = Splitter.on('.').splitToList(token);
if (list.size() != 3) {
return new Status(5000, "token length is not valid");
}
String encryptJson = list.get(1);
String sign = list.get(2);
String headerJson = list.get(0);
try {
if (!verifySign(encryptJson, sign)) {
return new Status(5001, "invalid token");
}
if (!verifyExpire(headerJson)) {
return new Status(5002, "token has time out");
}
} catch (Exception e) {
e.printStackTrace();
}
return new Status(200, "success");
}
/**
* 校验token是否过期
* @param headerJson 请求头的base64编码
* @return
*/
private static boolean verifyExpire(String headerJson) {
byte[] headerBytes = base64Decoder(headerJson);
String json = new String(headerBytes);
JsonObject jsonObject = (JsonObject)new JsonParser().parse(json);
JsonElement element = jsonObject.get("expire");
long expireTime = element.getAsLong();
return expireTime > System.currentTimeMillis() / 1000;
}
/**
*校验签名 使用base64解码后的字节数组来生成签名,并与传递来的签名进行比较得出合法性
* @param encryptJson
* @param sign
* @return
* @throws Exception
*/
private static boolean verifySign(String encryptJson, String sign) throws Exception{
byte[] encryptBody = base64Decoder(encryptJson);
byte[] bytes = generateHmacSign(encryptBody);
return StringUtils.equals(sign, base64Encoder(bytes));
}
private static byte[] AESDecryptBody(byte[] encryptBody) throws Exception{
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, aesKey);
return cipher.doFinal(encryptBody);
}
/**
*
* @param tokenBody 实例对象,通常为bean
* @param minute 过期时间 单位:min
* @param <T>
* @return
*/
public static <T> String createToken(T tokenBody, int minute) {
long now = System.currentTimeMillis() / 1000;
Gson gson = new Gson();
JsonObject jsonBody = new JsonObject();
jsonBody.addProperty("body", gson.toJson(tokenBody));
String randomAlphabetic = RandomStringUtils.randomAlphabetic(3);
JsonObject jsonHeader = new JsonObject();
jsonHeader.addProperty("now", now);
jsonHeader.addProperty("rand_num", randomAlphabetic);
jsonHeader.addProperty("expire", (now minute * 60));
String token = null;
try {
byte[] encryptContent = generateEncryptBody(jsonHeader.toString(), jsonHeader.toString());
byte[] signWithEncrypt = generateSignWithEncrypt(encryptContent);
token = Joiner.on(".").join(new String[]{base64Encoder(
jsonHeader.toString().getBytes("utf-8")),
base64Encoder(encryptContent),
base64Encoder(signWithEncrypt)}
);
} catch (Exception e) {
e.printStackTrace();
}
return token;
}
private static byte[] generateEncryptBody(String header, String body) throws Exception{
byte[] headerBytes = header.getBytes("utf-8");
byte[] bodyBytes = body.getBytes("utf-8");
byte[] content = xor(headerBytes, bodyBytes);
byte[] encryptContent = AESEncrypt(content);
return encryptContent;
}
private static byte[] generateSignWithEncrypt(byte[] encryptContent) throws Exception{
byte[] result = generateHmacSign(encryptContent);
return result;
}
private static byte[] generateHmacSign(byte[] content) throws Exception {
SecretKeySpec secretKey = new SecretKeySpec(hmacKey.getBytes("utf-8"), "HmacMD5");
Mac mac = Mac.getInstance("HmacMD5");
mac.init(secretKey);
return mac.doFinal(content);
}
private static String base64Encoder(byte[] data) {
return Base64.encodeBase64String(data);
}
private static byte[] base64Decoder(String content) {
return Base64.decodeBase64(content);
}
private static byte[] xor(byte[] header, byte[] body) {
byte[] xorData = new byte[body.length];
for (int i = 0; i < body.length; i ) {
int idx = i % header.length;
xorData[i] = (byte)(body[i] ^ header[idx]);
}
return xorData;
}
private static Key getAESKey() {
Key key = null;
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(new SecureRandom());
byte[] encodedKey = keyGenerator.generateKey().getEncoded();
key = new SecretKeySpec(encodedKey, "AES");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return key;
}
private static byte[] AESEncrypt(byte[] content) throws Exception {
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, aesKey);
return cipher.doFinal(content);
}
private static byte[] AESDecrypt(byte[] content) throws Exception {
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, aesKey);
return cipher.doFinal(content);
}
public static void main(String[] args) throws InterruptedException {
String token = createToken(new Student(1, "tom"), 1);
System.out.println(token);
Status status = verify(token);
System.out.println(status);
System.out.println(verify(token "ab"));
Thread.sleep(70000);
System.out.println(verify(token));
}
}
来验证一下输出:
eyJub3ciOjE1ODU3MTg4NTEsInJhbmRfbnVtIjoia3hIIiwiZXhwaXJlIjoxNTg1NzE4OTExfQ==.bPvihryiPud0Vq0A8spQl2z74oa8oj7ndFatAPLKUJds KGvKI 53RWrQDyylCXcUvGGQsG4bLHYxHBdn/wbA==.pN4kTCFtceRiPOaxaLlfAQ==
Status{code=200, message='success'}
Status{code=5001, message='invalid token'}
Status{code=5002, message='token has time out'}
当然,在生产环境中key这些固定的东西肯定是离不开配置文件,或者有专门的key相关服务。