背景
作为一个纯粹的Java Boy,一生致力于使用spring
和maven
来开发JAVA WEB
项目,我们入门业务的第一课都是登录注册,完成注册后,我们去完成登录时,往往会生成一个令牌token
来标识这个用户的身份
ACL
作为一个专业的CURD仔,为了开发效率我们都会奉行拿来主义,在实现登录验证时,我们一般采用采用Spring Security
来实现,采用Spring Security
的好处有他提供了token
生成和验证的方法,为了便利我们一般会使用JWT去生成token
JWT
JWT作为一种开放标准(RFC7519)有以下特点参考来自
- 无需服务器存储状态:传统会话认证机制需要服务器在会话中存储用户的状态信息,使用 JWT,服务器无需存储任何会话状态信息,所有的认证和授权信息都包含在 JWT 中。
- 跨域支持:由于 JWT 包含了完整的认证和授权信息,可以轻松地在多个域之间进行传递和使用实现跨域授权。
- 适应微服务架构:在微服务架构中,很多服务是独立部署并且可以横向扩展的,这就需要保证认证和授权的无状态性。使用 JWT 可以满足这种需求,每次请求携带 JWT 即可实现认证和授权。
- 自包含:JWT 包含了认证和授权信息,以及其他自定义的声明,这些信息都被编码在 JWT 中,在服务端解码后使用。JWT 的自包含性减少了对服务端资源的依赖,并提供了统一的安全机制。
- 扩展性:JWT 可以被扩展和定制,可以按照需求添加自定义的声明和数据,灵活性更高。
But
但是因为他的自包性和跨域支持,让我们可以在拿到token后可以在别的地方通过其标准的协议来进行解密,从而拿到body体和签名算法,例如
这样来看,我们在body就只能存放可以明着看的数据,比如id
、隐匿部分数据的用户账号
、用户昵称
等数据,但是在一些我们需要存储or使用他们用户其他的用户数据时,就只能通过用户id再去进行一次查询,这操作显然会消耗更多的rpc资源尤其是针对C端应用时
曲线救国
我们知道,token其实就是对一串字符串进行加密后形成一串字符串,又为了提升利用效率我们都尽可能的把有用的信息塞进token中,但是又为了安全我们不能使用jwt的开放协议,我们可以设计一套自己的token加密逻辑
生成token
我们常用的用户信息有用户的账号id
、昵称
、账号
、用户组id
、权限列表
、签发时间
、有效时间
、签发来源
等等,我们可以来定义一下用户信息的数据结构
public class UserInfo{
//账号id 8bit
private Long userId;
//昵称 dynamic
private String nickName;
//账号 8bit
private Long account;
//用户组id 8bit
private Long userGroupId;
//权限列表 8bit * N
private Long[] aclList;
//签发时间 8bit
private Long generateTime;
//有效时间 4bit
private Integer validTime;
//有效时间计算单位 2bit
private Short validUnit;
//签发来源 2bit
private Short source;
}
有了这个用户信息的容器,我们可以来将他转换成byte数组,从而通过加密算法对其进行加密,通过包装类型的,我们可以知道Long类型
占用64个比特,也就是8字节,而Integer类型
占用32比特,也就是4字节,Short类型
占用16比特,也就是2字节。
我们知道的,加密算法对数据进行加密前,需要对数据位数进行对齐,我们以RSA算法为例,在使用1024位的密钥,我们可以一次加密一块117
字节的数据,而2048位密钥能加密245
字节的数据,4096位密钥则是512
字节。
根据加密算法规则,我们可以根据消息体长度去选择对应的算法,在本文中,我们设定的用户信息基础信息占用了8+动态长度+8+8+8*N++4+2+2=32+动态长度+8*N
,假设我们的昵称长度限制为15个中文+长度位置一个Short
也就是30+2=32字节,加上权限列表,也就是我们通过117字节进行计算就够,如若还需使用其他数据可以选择其他的算法,并对用户信息进行调整,我们这里设定权限容量N为6,其中0为无效,为此,我们改造一下UserInfo类
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class UserInfo{
//账号id 8bit
private Long userId;
//昵称 dynamic
private String nickName;
//账号 8bit
private Long account;
//用户组id 8bit
private Long userGroupId;
//权限列表 8bit * N
private Long[] aclList;
//签发时间 8bit
private Long generateTime;
//有效时间 4bit
private Integer validTime;
//有效时间计算单位 2bit
private Short validUnit;
//签发来源 2bit
private Short source;
/**
* 把数据解析成bytes
*/
private byte[] bean2byts(){
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
byteBuffer.putLong(userId);
//限制长度不超过30字节
short length=Integer.valueOf(nickName.length()).shortValue();
byteBuffer.putShort(length);
for(int strIdx=0;strIdx<length;strIdx++){
byteBuffer.putChar(nickName.charAt(strIdx));
}
byteBuffer.putLong(account);
byteBuffer.putLong(userGroupId);
for(int aclIdx=0;aclIdx<8;aclIdx++){
if(aclIdx>=aclList.length){
byteBuffer.putLong(0L);
}else {
byteBuffer.putLong(aclList[aclIdx]);
}
}
byteBuffer.putLong(generateTime);
byteBuffer.putInt(validTime);
byteBuffer.putShort(validUnit);
byteBuffer.putShort(source);
return byteBuffer.array();
}
/**
* 进行加密
*/
public String encode(PublicKey publicKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, ShortBufferException {
byte[] bodys=bean2byts();
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE,publicKey);
byte[] res=cipher.doFinal(bodys,0, 117);
return Base64.getEncoder().encodeToString(res);
}
}
在这我们实现了生成token,那如何进行解析呢,我们可以把token进行解base64变成bytes后再通过RSA解密后填充回UserInfo
private UserInfo bytes2Bean(byte[] body){
ByteBuffer byteBuffer = ByteBuffer.wrap(body);
userId=byteBuffer.getLong();
short length=byteBuffer.getShort();
StringBuilder nickNameStrBuilder=new StringBuilder();
for(int strIdx=0;strIdx<length;strIdx++){
nickNameStrBuilder.append(byteBuffer.getChar());
}
nickName=nickNameStrBuilder.toString();
account=byteBuffer.getLong();
userGroupId=byteBuffer.getLong();
for(int aclIdx=0;aclIdx<6;aclIdx++){
aclList[aclIdx]=byteBuffer.getLong();
}
generateTime=byteBuffer.getLong();
validTime=byteBuffer.getInt();
validUnit=byteBuffer.getShort();
source=byteBuffer.getShort();
return this;
}
public UserInfo decode(String token, PrivateKey privateKey) throws InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException {
byte[] body=Base64.getDecoder().decode(token);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE,privateKey);
byte[] afterRsaDecode=cipher.doFinal(body);
return bytes2Bean(afterRsaDecode);
}
加盐
是不是以为这时候就可以用了?确实是可以用了,但是这是直接对byte进行加密,加密后的bytes直接转base64就返回了,这可能会被别人hook出结果,这时候我们还需要对其加盐进行混淆,让base64无法直接进行解析,我们可以进行以下操作
//获取随机字符串
private String getRandomString(int length){
SecureRandom random = new SecureRandom();
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
int randomInt = random.nextInt(62);
char randomChar;
if (randomInt < 10) {
randomChar = (char) ('0' + randomInt);
} else if (randomInt < 36) {
randomChar = (char) ('A' + randomInt - 10);
} else {
randomChar = (char) ('a' + randomInt - 36);
}
sb.append(randomChar);
}
return sb.toString();
}
//获取转移后的字符串
public String getToken(String realToken){
StringBuilder tokenBuilder=new StringBuilder();
tokenBuilder.append("!1hassan");//可以自定义字符集
//随机计算出插入颜的位置,采用2位16进制,所以max=FF,但是FF<字符串长度
SecureRandom random = new SecureRandom();
int randomIndex=random.nextInt(Math.min(realToken.length(), 0xFF));
tokenBuilder.append(Integer.toHexString(randomIndex));
//计算随机长度,采用1位16进制,0~F
int randomLength=random.nextInt(16);
tokenBuilder.append(Integer.toHexString(randomLength));
String randomSalt=getRandomString(randomLength);
if(randomIndex>0){
tokenBuilder.append(realToken.substring(0,randomIndex));
tokenBuilder.append(randomSalt);
tokenBuilder.append(realToken.substring(randomIndex));
}else {
tokenBuilder.append(randomSalt);
tokenBuilder.append(realToken);
}
return tokenBuilder.toString();
}
然后在解析之前去除这些数据
public String getRealToken(String token){
token=token.substring("!1hassan".length());
int randomIndex=Integer.parseInt(token.substring(0,2),16);
int randomLength=Integer.parseInt(token.substring(2,3),16);
token=token.substring(3);
String realToken="";
if(randomIndex==0){
realToken=token.substring(randomLength);
}else {
realToken=token.substring(0,randomIndex)+token.substring(randomIndex+randomLength);
}
return realToken;
}
这时候,我们使用加盐后的base64字符串去解密时,就会出现以下异常
最后附上完整的UserInfo对象代码,不过需要你注意的是,这里采用的是1024位的rsa密钥
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class UserInfo{
//账号id 8bit
private Long userId;
//昵称 dynamic
private String nickName;
//账号 8bit
private Long account;
//用户组id 8bit
private Long userGroupId;
//权限列表 8bit * N
private Long[] aclList;
//签发时间 8bit
private Long generateTime;
//有效时间 4bit
private Integer validTime;
//有效时间计算单位 2bit 0 分钟 1小时 2 天数 3 周 4 天
private Short validUnit;
//签发来源 2bit
private Short source;
//javaBean转bytes
private byte[] bean2byts(){
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
byteBuffer.putLong(userId);
//限制长度不超过30字节
short length=Integer.valueOf(nickName.length()).shortValue();
byteBuffer.putShort(length);
for(int strIdx=0;strIdx<length;strIdx++){
byteBuffer.putChar(nickName.charAt(strIdx));
}
byteBuffer.putLong(account);
byteBuffer.putLong(userGroupId);
for(int aclIdx=0;aclIdx<6;aclIdx++){
if(aclIdx>=aclList.length){
byteBuffer.putLong(0L);
}else {
byteBuffer.putLong(aclList[aclIdx]);
}
}
byteBuffer.putLong(generateTime);
byteBuffer.putInt(validTime);
byteBuffer.putShort(validUnit);
byteBuffer.putShort(source);
return byteBuffer.array();
}
//进行加密
public String encode(PublicKey publicKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, ShortBufferException {
byte[] bodys= bean2byts();
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE,publicKey);
byte[] res=cipher.doFinal(bodys,0, 117);
return Base64.getEncoder().encodeToString(res);
}
//获取随机字符串
private String getRandomString(int length){
SecureRandom random = new SecureRandom();
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
int randomInt = random.nextInt(62);
char randomChar;
if (randomInt < 10) {
randomChar = (char) ('0' + randomInt);
} else if (randomInt < 36) {
randomChar = (char) ('A' + randomInt - 10);
} else {
randomChar = (char) ('a' + randomInt - 36);
}
sb.append(randomChar);
}
return sb.toString();
}
//获取转移后的字符串
public String getToken(String realToken){
StringBuilder tokenBuilder=new StringBuilder();
tokenBuilder.append("!1hassan");//可以自定义字符集
//随机计算出插入颜的位置,采用2位16进制,所以max=FF,但是FF<字符串长度
SecureRandom random = new SecureRandom();
int randomIndex=random.nextInt(Math.min(realToken.length(), 0xFF));
tokenBuilder.append(Integer.toHexString(randomIndex));
//计算随机长度,采用1位16进制,0~F
int randomLength=random.nextInt(16);
tokenBuilder.append(Integer.toHexString(randomLength));
String randomSalt=getRandomString(randomLength);
if(randomIndex>0){
tokenBuilder.append(realToken.substring(0,randomIndex));
tokenBuilder.append(randomSalt);
tokenBuilder.append(realToken.substring(randomIndex));
}else {
tokenBuilder.append(randomSalt);
tokenBuilder.append(realToken);
}
return tokenBuilder.toString();
}
//bytes转成javaBean
private UserInfo bytes2Bean(byte[] body){
ByteBuffer byteBuffer = ByteBuffer.wrap(body);
userId=byteBuffer.getLong();
short length=byteBuffer.getShort();
StringBuilder nickNameStrBuilder=new StringBuilder();
for(int strIdx=0;strIdx<length;strIdx++){
nickNameStrBuilder.append(byteBuffer.getChar());
}
nickName=nickNameStrBuilder.toString();
account=byteBuffer.getLong();
userGroupId=byteBuffer.getLong();
for(int aclIdx=0;aclIdx<6;aclIdx++){
aclList[aclIdx]=byteBuffer.getLong();
}
generateTime=byteBuffer.getLong();
validTime=byteBuffer.getInt();
validUnit=byteBuffer.getShort();
source=byteBuffer.getShort();
return this;
}
//RSA解密
public UserInfo decode(String token, PrivateKey privateKey) throws InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException {
byte[] body=Base64.getDecoder().decode(token);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE,privateKey);
byte[] afterRsaDecode=cipher.doFinal(body);
return bytes2Bean(afterRsaDecode);
}
//获取真实的token
public String getRealToken(String token){
token=token.substring("!1hassan".length());
int randomIndex=Integer.parseInt(token.substring(0,2),16);
int randomLength=Integer.parseInt(token.substring(2,3),16);
token=token.substring(3);
String realToken="";
if(randomIndex==0){
realToken=token.substring(randomLength);
}else {
realToken=token.substring(0,randomIndex)+token.substring(randomIndex+randomLength);
}
return realToken;
}
}
测试main
方法
public static void main(String[] args) {
try {
String publicKeyStr = "公钥";
String privateKeyStr = "私钥";
Long[] aclList=new Long[]{1L,2L,3L,4L,5L,6L};
UserInfo userInfo=UserInfo.builder().userId(1L).nickName("昵称").account(180200220L).userGroupId(1L).aclList(aclList)
.generateTime(System.currentTimeMillis()/1000L).validTime(100).validUnit((short) 2).source((short) 1)
.build();
byte[] decodedKey = org.apache.commons.codec.binary.Base64.decodeBase64(publicKeyStr.getBytes());
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decodedKey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey= keyFactory.generatePublic(keySpec);
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(org.apache.commons.codec.binary.Base64.decodeBase64( privateKeyStr.getBytes()));
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
String token=userInfo.encode(publicKey);
System.out.println(token);
String returnToken = userInfo.getToken(token);
System.out.println(returnToken);
String realToken=userInfo.getRealToken(returnToken);
System.out.println(realToken);
UserInfo userInfoDecode=userInfo.decode(realToken,privateKey);
System.out.println(JSONObject.toJSONString(userInfoDecode));
}catch (Exception e){
//TODO must deal witch exception
e.printStackTrace();
}
}