我们在一部分场景下需要对用户的一些的参数,进行加密和解密,比如我们公司有两套服务,一套是专门对接银行/微信/支付宝的支付服务,那么这种服务不是任何请求都可以接受处理的。除了引入复杂的jwt雪花相关的加密包外,我们自己想实现一个轻量级加密解密算法的时候到了,在本地百万千万的单元测试中,效率可以达到100w/s,所以完全不用担心效率问题。不废话,上才艺:
工具包:
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.RandomUtil;
import org.apache.commons.lang3.StringUtils;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义简单的验签工具
* @author Liaoqian
* @version 3.0.0
* 加强了加密后字符串的稳定性,引入偏移量,权重值,随机值,长度校验,校验码校验,时间戳校验,分段校验等
*/
public class MySignUtil {
/**
* 过期时长(单位s)
*/
private static final int EXPIRATION_TIME = 60 * 30;
private static final String SIGN_REGEX = "^[a-zA-Z0-9|$]{7,}.[a-zA-Z0-9|$]{7,}.[a-zA-Z0-9|$]{7,}$";
private static final char[] CHECK_CODE = {'Q', 'w', 'E', 'r', 'T', 'y', 'U', 'i', 'O', 'p'};
private static final String LEGAL_CHAR_STR = "lwZyA07NXugBKfVI6MYUohPDnj3TsrH15JmeLzQtFv9d2qxGbCRac8WkSipOE4$";
private static final char[] LEGAL_CHAR_ARR = LEGAL_CHAR_STR.toCharArray();
private static final int LEGAL_CHAR_LEN = LEGAL_CHAR_STR.length();
private static final int[] WEIGHT_NUMBER = {7, 8, 2, 5, 0, 9, 4, 1, 3, 6};
/**
* 正向生成偏移量
*/
private static final int[] GENERATE_OFFSET = {17, 1, 4, 56, 28, 45, 61, 0, 11, 8, 20};
/**
* 反向解析偏移量
*/
private static final int[] PARSE_OFFSET = {46, 62, 59, 7, 35, 18, 2, 63, 52, 55, 43};
private static final int[] CHECK_INDEX = {1, 2, 0};
private static final int TEN = 10;
private static final int ZERO = 0;
private static final int ONE = 1;
private static final int TWO = 2;
private static final int THREE = 3;
private static final String SEPARATOR = ".";
private static final String SEPARATOR_REGEX = "\\.";
private static final String LENGTH_FORMAT = "%d$%s$%d";
private static final String LENGTH_SEPARATOR = "\\$";
private static final String LENGTH_SEP = "$";
private static Map<Character, Integer> charInedexMap = new HashMap<>();
static {
char[] chars = LEGAL_CHAR_STR.toCharArray();
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
charInedexMap.put(c, i);
}
}
/**
* 生成签名字符串
* 由user用户,role角色,date日期根据一定的规则生成一串乱码,
* 为了防止被破坏加密后的字符串因删减/移位/增位等一系列操作,引入位置权重,随机码,校验码,校验长度码等来保证加密后的稳定性
* 效率:解析速度约1000ns,即1/1000ms,即1s可解析100w条,效率是解析的两倍
*
* @param user
* @param role
* @param date
* @return
*/
public static String generateSign(String user, String role, Date date) {
if (StringUtils.isAnyBlank(user, role) || null == date) {
return null;
}
StringBuilder sb = new StringBuilder();
// 加密user信息
int sum = ZERO;
user = String.format(LENGTH_FORMAT, user.length(), user, user.length());
char[] chars = user.toCharArray();
for (int i = ZERO; i < chars.length; i++) {
char c = chars[i];
int index = charInedexMap.get(c);
sum += index * WEIGHT_NUMBER[i % TEN];
sb.append(LEGAL_CHAR_ARR[(index + GENERATE_OFFSET[i % TEN]) % LEGAL_CHAR_LEN]);
}
addCheckCode(sum, sb, ZERO, true);
// 加密role信息
sum = ZERO;
role = String.format(LENGTH_FORMAT, role.length(), role, role.length());
chars = role.toCharArray();
for (int i = ZERO; i < chars.length; i++) {
char c = chars[i];
int index = charInedexMap.get(c);
sum += index * WEIGHT_NUMBER[i % TEN];
sb.append(LEGAL_CHAR_ARR[(index + GENERATE_OFFSET[i % TEN]) % LEGAL_CHAR_LEN]);
}
addCheckCode(sum, sb, ONE, true);
// 加密time信息
String time = String.valueOf(date.getTime());
sum = ZERO;
time = String.format(LENGTH_FORMAT, time.length(), time, time.length());
chars = time.toCharArray();
for (int i = ZERO; i < chars.length; i++) {
char c = chars[i];
int index = charInedexMap.get(c);
sum += index * WEIGHT_NUMBER[i % TEN];
sb.append(LEGAL_CHAR_ARR[(index + GENERATE_OFFSET[i % TEN]) % LEGAL_CHAR_LEN]);
}
addCheckCode(sum, sb, TWO, false);
return sb.toString();
}
/**
* 添加校验码
* 由两位随机码和一位校验码组成,校验码位置是index
*
* @param sum
* @param sb
* @param index
* @param addSep
*/
private static void addCheckCode(int sum, StringBuilder sb, int index, boolean addSep) {
int checkIndex = sum % TEN;
String check = RandomUtil.randomString(LEGAL_CHAR_STR, THREE);
check = check.replace(check.charAt(CHECK_INDEX[index]), CHECK_CODE[checkIndex]);
sb.append(check);
if (addSep) {
sb.append(SEPARATOR);
}
}
/**
* 解析签名是否正确
*
* @param sign
* @return
*/
public static boolean parseSign(String sign) {
if (StringUtils.isBlank(sign)) {
return false;
}
if (!sign.matches(SIGN_REGEX)) {
return false;
}
MySign mySign = parseToMySign(sign);
return mySign.getCheck();
}
/**
* @param sign
* @return
*/
public static boolean signExpires(String sign) {
if (StringUtils.isBlank(sign)) {
return false;
}
if (!sign.matches(SIGN_REGEX)) {
return false;
}
MySign mySign = parseToMySign(sign);
if (!mySign.getCheck()) {
return false;
}
Long timeStamp = mySign.getTimeStamp();
if (DateUtil.offsetSecond(new Date(timeStamp), EXPIRATION_TIME).isBeforeOrEquals(DateUtil.date())) {
throw new BusinessException(BusinessExceptionEnum.SIGNATURE_EXPIRED);
}
return true;
}
/**
* 解析字符串签名,输出MySign解析结果
* 效率:解析速度约2000ns,即1/500ms,即1s可解析50w条
*
* @param sign 入参签名
* @return 解析完的MySign
*/
public static MySign parseToMySign(String sign) {
MySign mySign = new MySign();
String[] splits = sign.split(SEPARATOR_REGEX);
String user = splits[ZERO];
String role = splits[ONE];
String timeStamp = splits[TWO];
// user校验
StringBuilder sb = new StringBuilder();
int sum = ZERO;
char[] chars = user.toCharArray();
for (int i = ZERO; i < chars.length - THREE; i++) {
char c = chars[i];
int index = charInedexMap.get(c);
index = (index + PARSE_OFFSET[i % TEN]) % LEGAL_CHAR_LEN;
sb.append(LEGAL_CHAR_ARR[index]);
sum += index * WEIGHT_NUMBER[i % TEN];
}
int checkIndex = sum % TEN;
char userCheck = user.charAt(user.length() - CHECK_INDEX.length + CHECK_INDEX[ZERO]);
user = sb.toString();
if (!user.contains(LENGTH_SEP)) {
return mySign;
} else {
String[] split = user.split(LENGTH_SEPARATOR);
if (split.length != 3) {
return mySign;
}
if (!split[0].equals(split[2])) {
return mySign;
}
if (!String.valueOf(split[1].length()).equals(split[0])) {
return mySign;
}
user = split[1];
}
if (CHECK_CODE[checkIndex] != (userCheck)) {
return mySign;
}
// role校验
sb = new StringBuilder();
sum = ZERO;
chars = role.toCharArray();
for (int i = ZERO; i < chars.length - THREE; i++) {
char c = chars[i];
int index = charInedexMap.get(c);
index = (index + PARSE_OFFSET[i % TEN]) % LEGAL_CHAR_LEN;
sb.append(LEGAL_CHAR_ARR[index]);
sum += index * WEIGHT_NUMBER[i % TEN];
}
checkIndex = sum % TEN;
userCheck = role.charAt(role.length() - CHECK_INDEX.length + CHECK_INDEX[ONE]);
role = sb.toString();
if (!role.contains(LENGTH_SEP)) {
return mySign;
} else {
String[] split = role.split(LENGTH_SEPARATOR);
if (split.length != 3) {
return mySign;
}
if (!split[0].equals(split[2])) {
return mySign;
}
if (!String.valueOf(split[1].length()).equals(split[0])) {
return mySign;
}
role = split[1];
}
if (CHECK_CODE[checkIndex] != (userCheck)) {
return mySign;
}
// timeStamp校验
sb = new StringBuilder();
sum = ZERO;
chars = timeStamp.toCharArray();
for (int i = ZERO; i < chars.length - THREE; i++) {
char c = chars[i];
int index = charInedexMap.get(c);
index = (index + PARSE_OFFSET[i % TEN]) % LEGAL_CHAR_LEN;
sb.append(LEGAL_CHAR_ARR[index]);
sum += index * WEIGHT_NUMBER[i % TEN];
}
checkIndex = sum % TEN;
userCheck = timeStamp.charAt(timeStamp.length() - CHECK_INDEX.length + CHECK_INDEX[TWO]);
timeStamp = sb.toString();
if (!timeStamp.contains(LENGTH_SEP)) {
return mySign;
} else {
String[] split = timeStamp.split(LENGTH_SEPARATOR);
if (split.length != 3) {
return mySign;
}
if (!split[0].equals(split[2])) {
return mySign;
}
if (!String.valueOf(split[1].length()).equals(split[0])) {
return mySign;
}
timeStamp = split[1];
}
if (CHECK_CODE[checkIndex] != (userCheck)) {
return mySign;
}
// 校验通过
mySign.setCheck(true);
mySign.setUser(user);
mySign.setRole(role);
mySign.setTimeStamp(Long.parseLong(timeStamp));
return mySign;
}
}
模型:
import lombok.Data;
@Data
public class MySign {
/**
* 是否解析成功
*/
private boolean check;
private String user;
private String role;
private Long timeStamp;
public boolean getCheck() {
return check;
}
public void setCheck(boolean check) {
this.check = check;
}
}
public class Test1 {
@Test
public void t1() {
String s = MySignUtil.generateSign("cc", "adminx", new Date());
System.out.println(s);
System.out.println(MySignUtil.signExpires("4lSqT3HT2.JlkL$tPxgnixE.bTyn2dV3dRDTo4JdE1zTV7"));
}
}
加密:拼装-固定-偏移-随机-验证码
解密:分段-偏移-验证码-格式分析-提取-封装
解读起来不是很麻烦,在自我实现的过程忽视了很重要的一点,就是长度校验。这个可以固定加密后的字符串长度不能增删,改有校验码校验,另外还有随机码和校验码混合模式,分段校验通过,另外还加入过期时间的概念,让签名失效。目前来看没有什么太大问题