今天在研究如何防止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的“曲线救国”的方式 还有待寻找
参考:
奇妙的安全旅行之DSA算法 - 知乎
ECDSA(椭圆曲线数字签名算法) - 简书