==
在项目中,为了防止别人窥视我们的密码通常我们会采取一些加密方式。这里简单介绍一下MD5 加盐加密方法,MD5叫做信息-摘要算法,严格来说不是加密方式,而是信息摘要。
对于可以接触到数据库的dba来说,常常可以通过数据库看到用户的具体信息,如果有人非法盗取别人账号密码登录系统更改别人信息,这将是一个极大的损失。因此在数据库保存用户密码的时候通常会采用加密算法,这样即使dba在数据库中看到的也是一串的字符。md5不能反向解密,即使获得了一串字符也不易通过反向解密得到用户密码,保障了用户密码安全性。
== 加盐的目是: ==
即使数据被拖库,攻击者也无法从中破解出用户的密码。
即使数据被拖库,攻击者也无法伪造登录请求通过验证。
即使数据被拖库,攻击者劫持了用户的请求数据,也无法破解出用户的密码。
(1)第一步:设计数据库时候给一个salt字段用来存储盐值信息,一个password字段来存储(用户密码和盐经过算法处理过的)数据。
(2)第二步:后台写注册的代码时候,可以随机生成一段字符串来做盐值salt,然后获取前端用户注册输入密码password。
(3)第三步:盐值+前端获取用户密码生成新的密码经过算法处理得到一个新的字符串(String newPassword = salt+password)
(4)第四步:数据库存储的时候,salt字段就存储salt的值,password就存储newPassword处理后的值。好了到这里MD5加盐加密处理已经over
(1)第一步:获取用户登录账号,通过用户账号从数据库查出用户信息(包含:用户基本信息,salt、password)
(2)第二步:获取用户登录密码,用户登录密码和查询出的盐值生成新的密码字符串(String password = user.getSalt()+loginpassword)
(3)第三步:用与注册相同的算法处理password生成新的字符串newPasswod
(4)第四步:判断newPassword与user.getPassword()是否相等,如果相等,则登录成功,不相等则登录失败
package com.dangdang.xql.util;
import java.security.MessageDigest;
import java.util.Random;
import org.apache.commons.codec.binary.Hex;
/**
* @category 加密工具类
* @author 许清磊
*
*/
public class MD5Util {
/**
* @category 获取盐值
* @return
*/
public static String getSalt(){
// 生成一个16位的随机数
char[] code = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".toCharArray();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 16; i++) {
sb.append(code[new Random().nextInt(code.length)]);
}
// 生成最终的加密盐
String Salt = sb.toString();
return Salt;
}
/**
* @category 加盐MD5加密
* @param password
* @param Salt
* @return
*/
public static String getSaltMD5(String password,String Salt) {
password = md5Hex(password + Salt);
char[] cs = new char[48];
for (int i = 0; i < 48; i += 3) {
cs[i] = password.charAt(i / 3 * 2);
char c = Salt.charAt(i / 3);
cs[i + 1] = c;
cs[i + 2] = password.charAt(i / 3 * 2 + 1);
}
return String.valueOf(cs);
}
/**
* @category 使用Apache的Hex类实现Hex(16进制字符串和)和字节数组的互转
* @param str
* @return
*/
private static String md5Hex(String str) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(str.getBytes());
return new String(new Hex().encode(digest));
} catch (Exception e) {
e.printStackTrace();
System.out.println(e.toString());
return "";
}
}
/**
* @category 判断密码是否一致
* @param password 密码
* @param md5str 加密过的密码
* @return
*/
public static boolean getSaltverifyMD5(String password,String md5str,String Salt) {
return md5str.equals(getSaltMD5(password,Salt));
}
public static void main(String[] args) {
int nextInt = new Random().nextInt(1);
System.out.println(nextInt);
String salt =getSalt();
System.out.println(salt);
System.out.println(getSaltMD5("123",salt));
}
}
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.10</version>
</dependency>
token 值: 登录令牌.利用 token 值来判断用户的登录状态.类似于 MD5 加密之后的长字符串.
用户登录成功之后,在后端(服务器端)会根据用户信息生成一个唯一的值.这个值就是 token 值.
基本使用:
在服务器端(数据库)会保存这个 token 值,以后利用这个 token 值来检索对应的用户信息,并且判断用户的登录状态.
用户登录成功之后,服务器会将生成的 token 值返回给 客户端,在客户端也会保存这个 token 值.(一般可以保存在 cookie 中,也可以自己手动确定保存位置(比如偏好设置.)).
以后客户端在发送新的网络请求的时候,会默认自动附带这个 token 值(作为一个参数传递给服务器.).服务器拿到客户端传递的 token 值跟保存在 数据库中的 token 值做对比,以此来判断用户身份和登录状态.
判断登录状态:
如果客户端没有这个 token 值,意味着没有登录成功过,提示用户登录.
如果客户端有 token 值,一般会认为登录成功.不需要用户再次登录(输入账号和密码信息).
token 值扩展:
token 值有失效时间:
一般的 app ,token值得失效时间都在 1 年以上.
特殊的 app :银行类 app /支付类 app :token值失效时间 15 分钟左右.
一旦用户信息改变(密码改变),会在服务器生成新的 token 值,原来的 token值就会失效.需要再次输入账号和密码,以得到生成的新的 token 值.
唯一性判断: 每次登录,都会生成一个新的token值.原来的 token 值就会失效.利用时间来判断登录的差异性.
网页的做法通常是存到session里一个用户ID,
小程序存不了session
小程序每次请求会变
所以多出来个token的概念。
Token在WEB系统中相当于临时令牌的作用,一般作为验证使用。JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案。Token的出现主要解决了两方面问题。
Session会话机制,可以对用户信息进行存储,方便用户再次进入系统,或者进行相应的业务操作。但是,Session是存储于服务器的,会在一定的时间内消耗服务器的内存,同时,Session储存于固定的服务器中,因此,跨域问题就成了一个大麻烦。
Token将用户信息、加密算法、签名哈希进行加密发给客户端,让客户端进行存储,又有点类似于cookie机制了,但token可比cookie安全多了。
JWT的结构
JWT,即Json Web Token(以下简称jwt)即就是一种token的实现方式。一般来说,一个完整的jwt需要分为三个部分,token头、有效载荷、哈希签名 。(jwt官方网站: https://jwt.io/)
{
"alg": "HS256", 签名使用的算法
"typ": "JWT" 令牌类型,这里就是jwt
}
将这个对象进行BASE64URL加密就是jwt的头。
{
"sub": "1234567890", 主题
"name": "John Doe", 自定义字段
"iat": 1516239022 发布时间
}
PAYLOAD有效载荷中既可以存放一些已经定义过的字段,也可以自定义字段。
~~payload预定义的一些字段~~
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
将这个对象同样使用BASE64URL加密并且与jwt头以点号( . )隔开。
三、VERIFY SIGNATURE 哈希签名
对jwt头和载荷信息使用指定算法进行哈希签名,确保数据的完整性。
在签名算法中需要指定一个私钥,这个私钥只存储于服务器中,不能向用户公开。
将生成的哈希签名作为第三部分与前两部分以点号隔开,就生成了一个完整的token。
JWT的存储
服务器在验证用户合法性后会生成token,并将这个token发送给客户端,由客户端(即浏览器)存储在cookie或者local storage中。
客户再次进入系统时,客户端将携带token发给服务器作验证,这时候,token一般位于HTTP请求的HEADER AUTHORITON字段中,有时,也会放在post请求的数据主体中。
JWT的优缺点
1、JWT默认不加密,但可以加密。生成原始令牌后,可以使用改令牌再次对其进行加密。
2、当JWT未加密方法是,一些私密数据无法通过JWT传输。
3、JWT不仅可用于认证,还可用于信息交换。善用JWT有助于减少服务器请求数据库的次数。
4、JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限。也就是说,一旦JWT签发,在有效期内将会一直有效。
5、JWT本身包含认证信息,因此一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用,JWT的有效期不宜设置太长。对于某些重要操作,用户在使用时应该每次都进行进行身份验证。
6、为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输。
JWT的实现
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.3.0</version>
</dependency>
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class AuthTools {
public static String sign(String userName, String userId){
try {
Date date = new Date(System.currentTimeMillis() + Constant.EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(Constant.TOKEN);
//设置头部信息
Map<String, Object> header = new HashMap<>(2);
header.put("typ","jwt");
header.put("alg","HS256");
return JWT.create()
.withHeader(header)
//设置自定义载荷信息
.withClaim("userName",userName)
.withClaim("userId", userId)
//设置token过期时间
.withExpiresAt(date)
.sign(algorithm);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
public static boolean verify(String token){
try {
Algorithm algorithm = Algorithm.HMAC256(Constant.TOKEN);
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (UnsupportedEncodingException e) {
return false;
}
}
}
//一些用到的常量
public class Constant {
//设置过期时间为15分钟
public static final long EXPIRE_TIME = 15*60*1000;
//token私钥
public static final String TOKEN = "cabsycbiabiebciubadiugi";
}
import ch.qos.logback.core.net.SyslogOutputStream;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.test.travel.tools.AuthTools;
import com.test.travel.tools.Constant;
import java.io.UnsupportedEncodingException;
public class TestMain {
public static void main(String[] args) {
String token = AuthTools.sign("username","1");
System.out.println("用户是否合法: " + AuthTools.verify(token));
Algorithm algorithm = null;
try {
algorithm = Algorithm.HMAC256(Constant.TOKEN);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(token);
System.out.println("header: " + jwt.getHeader());
System.out.println("payload:" + jwt.getPayload());
System.out.println("signature: " + jwt.getSignature());
System.out.println("token: " + jwt.getToken());
System.out.println("userName: " + jwt.getClaim("userName").asString());
System.out.println("userId: " + jwt.getClaim("userId").asString());
System.out.println("算法: " + jwt.getAlgorithm());
}
}
##执行结果
用户是否合法: true
header: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
payload:eyJ1c2VyTmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTU1NTkzMDMwLCJ1c2VySWQiOiIxIn0
signature: M7b7w1Zd4jeVCGii9VJRlfTQgjD_cCtQYWyDQhCeU2s
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyTmFtZSI6InVzZXJuYW1lIiwiZXhwIjoxNTU1NTkzMDMwLCJ1c2VySWQiOiIxIn0.M7b7w1Zd4jeVCGii9VJRlfTQgjD_cCtQYWyDQhCeU2s
userName: username
userId: 1
算法: HS256
####### MD5和身份令牌结合实例
参考:::https://blog.csdn.net/qq_38438756/article/details/79676197
<!-- https://mvnrepository.com/artifact/javax.activation/activation -->
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-email -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-email</artifactId>
<version>1.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.mail/mail -->
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.4.1</version>
</dependency>
package com.dangdang.xql.util;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.apache.commons.mail.EmailException;
import org.apache.commons.mail.SimpleEmail;
public class EmailUtil {
// 随机验证码
public static String achieveCode() { //由于数字1 和0 和字母 O,l 有时分不清,所有,没有字母1 、0
String[] beforeShuffle= new String[] { "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F",
"G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a",
"b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v",
"w", "x", "y", "z" };
List<String> list = Arrays.asList(beforeShuffle);//将数组转换为集合
Collections.shuffle(list); //打乱集合顺序
StringBuilder sb = new StringBuilder();
for (int i = 0; i < list.size(); i++) {
sb.append(list.get(i)); //将集合转化为字符串
}
return sb.toString().substring(3, 8); //截取字符串第4到8
}
public static void sendAuthCodeEmail(String email, String authCode) {
try {
SimpleEmail mail = new SimpleEmail();
mail.setHostName("smtp.qq.com");//发送邮件的服务器(smtp.qq.com)smtp.163.com
//mail.setAuthentication("邮箱账号", "平台授权码");//登录邮箱的密码,是开启SMTP的密码
mail.setAuthentication("邮箱账号", "授权码");
mail.setFrom("发送人邮箱账号","皮皮磊"); //发送邮件的邮箱和发件人
mail.setSSLOnConnect(true); //使用安全链接
mail.addTo(email);//接收的邮箱
mail.setSubject("皮皮磊平台注册码");//设置邮件的主题
mail.setMsg("尊敬的用户:你好!\n 皮皮平台登陆验证码为:" + authCode+"\n"+" (有效期为一分钟)");//设置邮件的内容
mail.send();//发送
} catch (EmailException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
sendAuthCodeEmail("[email protected]",achieveCode());
}
}
这里是发送邮件服务器平台
例如我们使用163.com服务器:smtp.163.com
qq为:smtp.qq.com
这里是平台账号的平台所开启的授权码中心
当你写完上面两部运行测试发现报错
这个时候需要去平台开启POP3与SMTP服务器
在设置里面 点击设置
点击账户
开启的时候记录授权码 上面代码要使用
//发送验证码,并使用session存储设置值
public void vcode(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException, SQLException{
String email = request.getParameter("email");
String code = new EmailUtil().achieveCode(); //生成随机密码
try {
new EmailUtil().sendAuthCodeEmail(email,code);
HttpSession session = request.getSession();
session.setAttribute("Vcode", code);
session.setMaxInactiveInterval(60); //设置session有效期 60秒
response.getWriter().write("{\"exist\":"+true+"}");
} catch (Exception e) {
response.getWriter().write("{\"exist\":"+false+"}");
e.printStackTrace();
}
}
$("#send").click(function(){
var email =$("#email").val(); //获取用户输入的邮箱
var myreg = /^([\.a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-])+/;
if(email.length==0 || !myreg.test(email)){ //验证邮箱是否正确
return false;
}
$.post(
"UsrServlet/vcode", //进行发送验证码处理的方法
{"email":email}, //以json格式传递参数
function(responseData) { //访问成功后的回调函数,responseData:响应参数
if (responseData.exist) {
new invokeSettime("#send"); //发送正确后,调用方法,开始倒计时
$("#showTooltips").attr("href","javascript:");
$("#showTooltips").css("background-color","rgba(26,173,25,0.5)");
} else {
console.log("验证码发送失败"); //控制台打印调试
}
},
"json" //响应数据为json格式
);
});
1、 背景
分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。
有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。
而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移到Cassandra,因为Cassandra没有顺序ID生成机制,为了满足Twitter每秒上万条消息的请求,每条消息都必须分配一条唯一的id,这些id还需要一些大致的顺序(方便客户端排序),并且在分布式系统中不同机器产生的id必须不同,所以twitter开发了这样一套全局唯一ID生成服务。
2、Snowflake算法核心
SnowFlake的结构如下(每部分用-分开): 把时间戳,工作机器id,序列号组合在一起。
1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0
*41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截) 后得到的值,这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
10位的数据机器位,可以部署在1024个节点,包括10位workerId
12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号
加起来刚好64位,为一个Long型。
SnowFlake的原始版本是scala版,用于生成分布式ID(纯数字,时间顺序),订单编号等。
其结构为: 时间戳 + 工作机器id + 数据中心ID + 序列号组合在一起。
SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
SnowFlake适用于数据敏感场景,例如商品订单类和分布式场景。
package com.grid.service;
public class SnowflakeIdWorker {
/**
* 雪花算法解析 结构 snowflake的结构如下(每部分用-分开):
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
* 第一位为未使用,接下来的41位为毫秒级时间(41位的长度可以使用69年),然后是5位datacenterId和5位workerId(10
* 位的长度最多支持部署1024个节点) ,最后12位是毫秒内的计数(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号)
*
* 一共加起来刚好64位,为一个Long型。(转换成字符串长度为18)
*
*/
// ==============================Fields===========================================
/** 开始时间截 (2015-01-01) */
private final long twepoch = 1489111610226L;
/** 机器id所占的位数 */
private final long workerIdBits = 5L;
/** 数据标识id所占的位数 */
private final long dataCenterIdBits = 5L;
/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/** 支持的最大数据标识id,结果是31 */
private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);
/** 序列在id中占的位数 */
private final long sequenceBits = 12L;
/** 机器ID向左移12位 */
private final long workerIdShift = sequenceBits;
/** 数据标识id向左移17位(12+5) */
private final long dataCenterIdShift = sequenceBits + workerIdBits;
/** 时间截向左移22位(5+5+12) */
private final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;
/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/** 工作机器ID(0~31) */
private long workerId;
/** 数据中心ID(0~31) */
private long dataCenterId;
/** 毫秒内序列(0~4095) */
private long sequence = 0L;
/** 上次生成ID的时间截 */
private long lastTimestamp = -1L;
// ==============================Constructors=====================================
/**
* 构造函数
* @param workerId 工作ID (0~31)
* @param dataCenterId 数据中心ID (0~31)
*/
public SnowflakeIdWorker(long workerId, long dataCenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("workerId can't be greater than %d or less than 0", maxWorkerId));
}
if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
throw new IllegalArgumentException(String.format("dataCenterId can't be greater than %d or less than 0", maxDataCenterId));
}
this.workerId = workerId;
this.dataCenterId = dataCenterId;
}
// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
// 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
// 如果是同一时间生成的,则进行毫秒内序列
// sequenceMask 为啥是4095 2^12 = 4096
if (lastTimestamp == timestamp) {
// 每次+1
sequence = (sequence + 1) & sequenceMask;
// 毫秒内序列溢出
if (sequence == 0) {
// 阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
// 时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
// 上次生成ID的时间截
lastTimestamp = timestamp;
// 移位并通过或运算拼到一起组成64位的ID
// 为啥时间戳减法向左移动22 位 因为 5位datacenterid
// 为啥 datCenterID向左移动17位 因为 前面有5位workid 还有12位序列号 就是17位
//为啥 workerId向左移动12位 因为 前面有12位序列号 就是12位
System.out.println(((timestamp - twepoch) << timestampLeftShift) //
| (dataCenterId << dataCenterIdShift) //
| (workerId << workerIdShift) //
| sequence);
return ((timestamp - twepoch) << timestampLeftShift) //
| (dataCenterId << dataCenterIdShift) //
| (workerId << workerIdShift) //
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
// ==============================Test=============================================
/** 测试 */
public static void main(String[] args) {
System.out.println(System.currentTimeMillis());
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(1, 1);
long startTime = System.nanoTime();
for (int i = 0; i < 50000; i++) {
long id = idWorker.nextId();
System.out.println(id);
}
System.out.println((System.nanoTime() - startTime) / 1000000 + "ms");
}
}
分布式ID生成系统 UUID与雪花(snowflake)算法
Leaf——美团点评分布式ID生成系统 -
https://tech.meituan.com/MT_Leaf.html
网游服务器中的GUID(唯一标识码)实现-基于snowflake算法-云栖社区-阿里云
https://yq.aliyun.com/articles/229420
UUID_STRING — Snowflake Documentation
https://docs.snowflake.net/manuals/sql-reference/functions/uuid_string.html
Twitter的分布式自增ID算法snowflake (Java版) - relucent - 博客园
https://www.cnblogs.com/relucent/p/4955340.html
雪花算法(snowflake) - 明月阁 - CSDN博客
https://blog.csdn.net/u011499747/article/details/78254990