手把手教你使用自定义ClassLoader实现Java源代码加密!
你知道的越多,不知道的就越多,业余的像一棵小草!
你来,我们一起精进!你不来,我和你的竞争对手一起精进!
编辑:业余草
推荐:https://www.xttblog.com/?p=5277
前言
最近和朋友聊天,他接了个外包项目,他问我有没有办法让自己的源码不被反编译破解,我就跟他说可以对代码进行混淆和加密。今天我们就来聊聊如何通过对代码进行加密实现代码防反编译,至于混淆因为可以直接利用proguard-maven-plugin
进行配置实现,相对比较简单,就不在本文论述。
代码防编译整体套路
编写加密工具类
@Slf4j
public class EncryptUtils {
private static String secretKey = "test123456lyb-geek"+System.currentTimeMillis();
private EncryptUtils(){}
public static void encrypt(String classFileSrcPath,String classFileDestPath) {
System.out.println(secretKey);
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(classFileSrcPath);
fos = new FileOutputStream(classFileDestPath);
int len;
String[] arrs = secretKey.split("lyb-geek");
long key = Long.valueOf(arrs[1]);
System.out.println("key:"+key);
while((len = fis.read())!=-1){
byte data = (byte)(len + key + secretKey.length());
fos.write(data);
}
} catch (Exception e) {
log.error("encrypt fail:"+e.getMessage(),e);
}finally {
if(fis != null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fos != null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
对需要防止被反编译代码加密
public static void main(String[] args) {
String classFileSrcPath = classFileSrcPath("UserServiceImpl");
System.out.println("classFileSrcPath:--->"+classFileSrcPath);
String classFileDestDir = ServiceGenerate.class.getClassLoader().getResource("META-INF/services/").getPath();
System.out.println("classFileDestDir:--->"+classFileDestDir);
String classFileDestPath = classFileDestDir + "com.github.lybgeek.user.service.impl.UserServiceImpl.lyb";
EncryptUtils.encrypt(classFileSrcPath,classFileDestPath);
}
对加密代码进行反编译验证
打开反编译工具 jd-gui,把加密的代码拖入 jd-gui。
打不开,至少说明不能用 jd-gui 来反编译加密过的代码。
我们打开正常的编译的 class 文件,其内容形如下:
从内容我们大概还是能看出一些东西,比如包名啥的。而打开加密后的文件,其内容如下:
内容宛若天书!!!
思考一:代码都被加密了,那 jvm 如何识别?
「答案」:既然有加密,自然可以通过解密来使用。那这个解密得存放在什么地方进行解密?
如果对类加载有一定了解的朋友,就会知道 java 的 class 文件是通过类加载器把 class 加载入 jvm 内存中,因此我们可以考虑把解密放在类加载器中。常用的类加载有启动类加载器、扩展类加载器、系统类加载。我们正常 classpath 路径下的类都是通过系统类加载器进行加载。而不巧这三个 jdk 提供的加载器没法满足我们的需求。因此我们只能自己实现我们的类加载器。其自定义加载器代码如下:
@Slf4j
public class CustomClassLoader extends ClassLoader{
/**
* 授权码
*/
private String secretKey;
private String SECRETKEY_PREFIX = "lyb-geek";
/**
* class文件的根目录
*/
private String classRootDir = "META-INF/services/";
public CustomClassLoader(String secretKey) {
this.secretKey = secretKey;
}
public String getClassRootDir() {
return classRootDir;
}
public void setClassRootDir(String classRootDir) {
this.classRootDir = classRootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> clz = findLoadedClass(name);
//先查询有没有加载过这个类。如果已经加载,则直接返回加载好的类。如果没有,则加载新的类。
if(clz != null){
return clz;
}else{
ClassLoader parent = this.getParent();
clz = getaClass(name, clz, parent);
if(clz != null){
return clz;
}else{
clz = getaClass(name);
}
}
return clz;
}
private Class<?> getaClass(String name) throws ClassNotFoundException {
Class<?> clz;
byte[] classData = getClassData(name);
if(classData == null){
throw new ClassNotFoundException();
}else{
clz = defineClass(name, classData, 0,classData.length);
}
return clz;
}
private Class<?> getaClass(String name, Class<?> clz, ClassLoader parent) {
try {
//委派给父类加载
clz = parent.loadClass(name);
} catch (Exception e) {
//log.warn("parent load class fail:"+ e.getMessage(),e);
}
return clz;
}
private byte[] getClassData(String classname){
if(StringUtils.isEmpty(secretKey) || !secretKey.contains(SECRETKEY_PREFIX) || secretKey.split(SECRETKEY_PREFIX).length != 2){
throw new RuntimeException("secretKey is illegal");
}
String path = CustomClassLoader.class.getClassLoader().getResource("META-INF/services/").getPath() +"/"+ classname+".lyb";
InputStream is = null;
ByteArrayOutputStream bas = null;
try{
is = new FileInputStream(path);
bas = new ByteArrayOutputStream();
int len;
//解密
String[] arrs = secretKey.split(SECRETKEY_PREFIX);
long key = Long.valueOf(arrs[1]);
// System.out.println("key:"+key);
while((len = is.read())!=-1){
byte data = (byte)(len - key - secretKey.length());
bas.write(data);
}
return bas.toByteArray();
}catch(Exception e){
e.printStackTrace();
return null;
}finally{
try {
if(is!=null){
is.close();
}
} catch (IOException e) {
log.error("encrypt fail:"+e.getMessage(),e);
}
try {
if(bas!=null){
bas.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
通过如下方式进行调用
public static void main(String[] args) throws Exception{
CustomClassLoader customClassLoader = new CustomClassLoader("test123456lyb-geek1603895713759");
Class clz = customClassLoader.loadClass("com.github.lybgeek.user.service.impl.UserServiceImpl");
if(clz != null){
Method method = clz.getMethod("list", User.class);
method.invoke(clz.newInstance(),new User());
}
}
思考二:通过自定义加载器加载过的类如何整合进行 spring?
「答案:」 通过 spring 提供的扩展点进行 ioc 容器注入
❝1、编写 bean 定义,并注册注册 bean 定义。
❞
@Component
public class ServiceBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
Object secretKey = YmlUtils.getValue("lyb-geek.secretKey");
if(ObjectUtils.isEmpty(secretKey)){
throw new RuntimeException("secretKey can not be null,you maybe need to config in application.yml with key lyb-geek.secretKey");
}
registerBean(beanFactory,secretKey.toString());
// setClassLoader(beanFactory,secretKey.toString());
}
/**
* 如果项目中引入了>spring-boot-devtools,则默认加载器为org.springframework.boot.devtools.restart.classloader.RestartClassLoader
* 此时如果使用自定加载器,则需把bean的类加载器变更为AppClassLoader
* @param beanFactory
*/
private void setClassLoader(ConfigurableListableBeanFactory beanFactory,String secretKey) {
CustomClassLoader customClassLoader = new CustomClassLoader(secretKey);
beanFactory.setBeanClassLoader(customClassLoader.getParent());
}
private void registerBean(ConfigurableListableBeanFactory beanFactory,String secretKey){
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) beanFactory;
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(UserService.class);
GenericBeanDefinition definition = (GenericBeanDefinition) builder.getRawBeanDefinition();
definition.getPropertyValues().add("serviceClz",UserService.class);
definition.getPropertyValues().add("serviceImplClzName", "com.github.lybgeek.user.service.impl.UserServiceImpl");
definition.getPropertyValues().add("secretKey", secretKey);
definition.setBeanClass(ServiceFactoryBean.class);
definition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);
String beanId = StringUtils.uncapitalize(UserService.class.getSimpleName());
defaultListableBeanFactory.registerBeanDefinition(beanId, definition);
}
}
❝2、如果是接口注入,还需通过 FactoryBean 进行狸猫换太子。
❞
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ServiceFactoryBean <T> implements FactoryBean<T> , ApplicationContextAware, InitializingBean {
private ApplicationContext applicationContext;
private Class<T> serviceClz;
private String serviceImplClzName;
private String secretKey;
private Object targetObj;
@Override
public T getObject() throws Exception {
return (T) targetObj;
}
@Override
public Class<?> getObjectType() {
return serviceClz;
}
@Override
public void afterPropertiesSet() throws Exception {
targetObj = ServiceFactory.create(secretKey,serviceImplClzName,applicationContext);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
❝3、验证是否整合成功
❞
验证示例代码如下:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping(value = "/save")
public User save(User user){
User newUser = userService.save(user);
return newUser;
}
}
能够正常输出,说明整合成功。
总结
上述的例子只是提供一个思路,并不能完全杜绝代码被反编译。因为如果真想进行反编译,其实可以先通过反编译你自定义的类加载器,然后通过解密方式,去逆推加密算法,从而还原加密类。要杜绝代码被反编译的思路有如下:
提高反编译的成本,比如对自定义类加载再次加密,编写复杂的加密算法 编写让人没有欲望反编译的代码,比如写一堆垃圾代码
需要本文完整 demo 的,可以加我微信:codedq,免费获取!