今天在研究如何防止Cookie篡改,
防止篡改Cookie,最好的方法当然是利用非对称加密,通过私钥加密这个cookie的值,进行一个数字签名的大动作,然后再将cookie的值是签名的内容+要被签名的内容+公钥组成,再次返回的时候验证,就验证这个签名的内容解出来之后是否与要被签名的内容相同,相同则这个cookie没有被篡改
[Signature 类]
public final boolean verify(byte[] signature) throws SignatureException {
if (state == VERIFY) {
return engineVerify(signature);
}
throw new SignatureException("object not initialized for " +
"verification");
}
protected boolean engineVerify(byte[] sigBytes)
throws SignatureException {
try {
byte[] out = cipher.doFinal(sigBytes);
byte[] dataBytes = data.toByteArray();
data.reset();
return MessageDigest.isEqual(out, dataBytes);
} catch (BadPaddingException e) {
// e.g. wrong public key used
// return false rather than throwing exception
return false;
} catch (IllegalBlockSizeException e) {
throw new SignatureException("doFinal() failed", e);
}
}
public static boolean isEqual(byte[] digesta, byte[] digestb) {
if (digesta == digestb) return true;
if (digesta == null || digestb == null) {
return false;
}
int lenA = digesta.length;
int lenB = digestb.length;
if (lenB == 0) {
return lenA == 0;
}
int result = 0;
result |= lenA - lenB;
// time-constant comparison
for (int i = 0; i < lenA; i++) {
// If i >= lenB, indexB is 0; otherwise, i.
int indexB = ((i - lenB) >>> 31) * i;
result |= digesta[i] ^ digestb[indexB];
}
return result == 0;
}
可以看到下面验签(也就是sigBytes与data进行比较,data就是传入的原内容)的过程其实就是逐位做异或,只要有1位为1,result 就为1,然后就匹配失败
(异或 :1^0 = 1 , 1^1 = 0 , 0^1 = 1 , 0^0 = 0)
Java支持数字签名的算法有三种:
RSA 将两个大素数相乘十分容易,但反过来想要对它们的乘积进行因式分解会比较困难
DSA 只用签名而不能加密或解密,比RSA要快,主要就是两个素数公开,这样,当使用别人的p和q时,即使不知道私钥,你也能确认它们是否是随机产生的,还是作了手脚。
ECDSA 椭圆曲线签名算法:椭圆曲线上的离散对数问题,主要就是知道基点(椭圆曲线上的点)和某个数的乘积 以及 基点 很难推出 这个 某个数 来,难度相较于上面两个都要难很多;除了一个个遍历没有更快的办法,但如果是大数就会需要遍历很久
那么我利用用户信息对这个Cookie加密,那岂不是就安全了?
然后当我乐呵乐呵地尝试去修改sessionId的生成方法时,发现我使用的是Spring-session-redis,而在RedisHttpSessionConfiguration 中有个RedisOperationSessionRepository 的session仓库,
而这个RedisOperationSessionRepository 就是存放各种session 的地方
内部有个方法是CreateSession,这里就是session 生成的地方
public RedisOperationsSessionRepository.RedisSession createSession() {
Duration maxInactiveInterval = Duration.ofSeconds(this.defaultMaxInactiveInterval != null ? (long)this.defaultMaxInactiveInterval : 1800L);
RedisOperationsSessionRepository.RedisSession session = new RedisOperationsSessionRepository.RedisSession(maxInactiveInterval);
session.flushImmediateIfNecessary();
return session;
}
关键是这里有个RedisSession
而这个RedisSession 就是这个仓库中的session,也就是Spring-session 用来替换原生的session 的session
再往下看会发现,这个RedisSession是final修饰的!
final class RedisSession implements Session {
private final MapSession cached;
这意味着什么? 是的!final修饰的类不可继承,final修饰的方法不可重写
看到上面还有个MapSession 是干啥的?它不仅是private 修饰的还是final修饰的,private意味着即使redissession是可继承的,你也拿不到它,private修饰的成员变量是让你可以拥有,只是可以拥有,final 修饰意味着它 ! 也!不 ! 可! 以! 改!写!
再往下看:这个就是RedisOperationSessionRepository createSession时提到的方法!
RedisSession(Duration maxInactiveInterval) {
this((MapSession)(new MapSession()));
this.cached.setMaxInactiveInterval(maxInactiveInterval);
this.delta.put("creationTime", this.getCreationTime().toEpochMilli());
this.delta.put("maxInactiveInterval", (int)this.getMaxInactiveInterval().getSeconds());
this.delta.put("lastAccessedTime", this.getLastAccessedTime().toEpochMilli());
this.isNew = true;
}
内部是不是有个MapSession的实例?
跳到MapSession里看看,这个到底和Redissession 有什么关系?
public final class MapSession implements Session, Serializable {
public MapSession() {
this(generateId());
}
public MapSession(String id) {
this.sessionAttrs = new HashMap();
this.creationTime = Instant.now();
this.lastAccessedTime = this.creationTime;
this.maxInactiveInterval = Duration.ofSeconds(1800L);
this.id = id;
this.originalId = id;
}
private static String generateId() {
return UUID.randomUUID().toString();
}
}
可以看到其实就是生成了一个Session,还包括SessionId的生成,也就是说这个RedisSession就是个大session,内部有个小Session,而这个session就是真正的Session,毕竟SessionID也就是在这生成的
最后我的修改SessionID的方法宣告失败~不过或许有其他可以修改SessionID的“曲线救国”的方式 还有待寻找
参考: