ProGuard加密混淆SpringBoot应用代码

背景

我们的项目是基于SpringCloud架构的微服务应用,采用Docker离线部署方式交付客户,通过授权证书来控制应用的许可功能模块和使用时间。我们已经在代码层已经实现:

  • 基于多维度硬件指纹的绑定验证,cpu id、mac地址、磁盘序列、系统时钟、应用初始时间等
  • 双重时间验证机制(系统时间+硬件时钟)
  • 安全续期机制支持离线更新
  • 防调试/防篡改保护

来解决离线容器化部署Java应用程序授权问题。
整体流程如下:
ProGuard加密混淆SpringBoot应用代码_第1张图片
该解决方案已基本能解决离线容器化部署Java应用程序授权问题,为了进一步加强安全防止通过反编译代码破解授权证书,我们决定对代码进行加密混淆。

Proguard

ProGuard 是一款开源 Java 类文件压缩器、优化器、混淆器和预验证器。因此,ProGuard 处理的应用程序和库更小、速度更快。

  • 缩减步骤检测并删除未使用的类、字段、方法和属性。
  • 优化器步骤优化字节码并删除未使用的指令。
  • 名称混淆步骤使用简短而无意义的名称重命名剩余的类、字段和方法。

Maven插件

我们的项目是SpringBoot 2.2.9 + jdk1.8,基于Maven构建,因此我们使用Proguard的Maven插件:proguard-maven-plugin来进行自动化代码混淆。下面是SpringBoot项目下基本的proguard-maven-plugin插件配置:

<build>
        <finalName>appfinalName>
        <plugins>
            
            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-compiler-pluginartifactId>
                <version>3.8.1version> 
                <configuration>
                    <source>1.8source>
                    <target>1.8target>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombokgroupId>
                            <artifactId>lombokartifactId>
                            <version>1.18.24version> 
                        path>
                        <path>
                            <groupId>org.mapstructgroupId>
                            <artifactId>mapstruct-processorartifactId>
                            <version>${org.mapstruct.version}version>
                        path>
                    annotationProcessorPaths>
                    <compilerArgs>
                        <arg>-Aprojectlombok.classpath=${project.build.outputDirectory}arg>
                    compilerArgs>
                    <excludes>
                        <exclude>>com/hka/business/uaaserver/license/crypto/LicenseGenerator.javaexclude>
                    excludes>
                configuration>
            plugin>
            
            <plugin>
                <groupId>com.github.wvengengroupId>
                <artifactId>proguard-maven-pluginartifactId>
                <version>2.6.0version>

                <executions>
                    
                    <execution>
                        <phase>packagephase>
                        <goals>
                            <goal>proguardgoal>
                        goals>
                    execution>
                executions>
                <configuration>
                    <proguardVersion>6.2.2proguardVersion>
                    
                    <injar>${project.build.finalName}.jarinjar>
                    
                    <outjar>${project.build.finalName}.jaroutjar>
                    
                    <obfuscate>trueobfuscate>
                    
                    <proguardInclude>${project.basedir}/proguard.cfgproguardInclude>
                    
                    <putLibraryJarsInTempDir>trueputLibraryJarsInTempDir>
                    <libs>
                        <lib>${java.home}/lib/rt.jarlib>
                        <lib>${java.home}/lib/jce.jarlib>
                        <lib>${java.home}/lib/jsse.jarlib>
                    libs>
                    
                    <inLibsFilter>!META-INF/**,!META-INF/versions/9/**.classinLibsFilter>
                    
                    <outputDirectory>${project.basedir}/targetoutputDirectory>
                    
                    <options>
                        
                    options>
                configuration>
                <dependencies>
                    <dependency>
                        <groupId>net.sf.proguardgroupId>
                        <artifactId>proguard-baseartifactId>
                        <version>6.2.2version>
                    dependency>
                dependencies>
            plugin>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
                <version>${spring-boot-dependencies.version}version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackagegoal>
                        goals>
                    execution>
                executions>
            plugin>
        plugins>
    build>

这里需要重点注意的是proguard-maven-plugin插件配置必须在Maven插件之后(先编译后混淆)。

proguard.cfg配置如下:

#指定Java的版本
-target 1.8
# 保留Spring Boot启动类
-keep class com.hka.business.uaaserver.UaaCenterApplication { *;}
-keepclassmembers class com.hka.business.uaaserver.UaaCenterApplication {
    @* *;
}

# 保留Spring相关注解
-keep @org.springframework.stereotype.Service class *
-keep @org.springframework.stereotype.Component class *
-keep @org.springframework.stereotype.Repository class *
-keep @org.springframework.stereotype.Controller class *
-keep @javax.annotation.PostConstruct class *
-keep @lombok.RequiredArgsConstructor class *
-keep @lombok.extern.slf4j.Slf4j class *
-keep @lombok.Data class *
-keep @lombok.AllArgsConstructor class *

# 保留MyBatis Mapper接口
-keep @org.apache.ibatis.annotations.Mapper class *
-keepclassmembers class * {
    @org.apache.ibatis.annotations.* *;
}

# 保留Nacos相关配置
-keep class com.alibaba.nacos.** { *; }

# 保留JAXB注解(Spring Boot可能需要)
-keepclassmembers class * {
    @javax.xml.bind.annotation.XmlElement *;
    @javax.xml.bind.annotation.XmlRootElement *;
}

# 保留包及其类上的注解
-keep class com.hka.business.uaaserver.message.**,com.hka.business.uaaserver.bi.** { *; }
-keepclassmembers class com.hka.business.uaaserver.message.**,com.hka.business.uaaserver.bi.** {
    @* *;
}

-keep class com.hka.business.uaaserver.application.** { *; }
-keepclassmembers class com.hka.business.uaaserver.application.** {
    @* *;
}

-keep class com.hka.business.uaaserver.config.** { *; }
-keepclassmembers class com.hka.business.uaaserver.config.** {
    @* *;
}

-keep class com.hka.business.uaaserver.infrastructure.** { *; }
-keepclassmembers class com.hka.business.uaaserver.infrastructure.** {
    @* *;
}

-keep class com.hka.business.uaaserver.interfaces.** { *; }
-keepclassmembers class com.hka.business.uaaserver.interfaces.** {
    @* *;
}

# 强制混淆的License包
-keep class !com.hka.business.uaaserver.license.** {
    *;
}

# 处理Lambda表达式
-keepclassmembers class * {
    private static synthetic java.lang.Object $deserializeLambda$(java.lang.invoke.SerializedLambda);
}

# 保留枚举类
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# 保留序列化相关
-keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    static final java.io.ObjectStreamField[] serialPersistentFields;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}

# 忽略 javax.activation 包中的类
-dontwarn javax.activation.**

# 忽略 javax.xml.bind 包中的类
-dontwarn javax.xml.bind.**

# 忽略 module-info 类
-dontwarn module-info

-ignorewarnings

-dontnote
# 配置保留注解
-keepattributes Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod

踩过的坑

需要完整配置需要保留的类

使用ProGuard最大的挑战应该是ProGuard默认会处理所有代码,因此需要精确配置哪些类需要保留,哪些需要混淆。特别是对于SpringBoot项目中存在大量注解、序列化、第三方框架、动态注入等场景。最简单的例子就是Spring Boot启动类的配置,不光要配置保留启动类同时还需要配置保留相关注解,否则混淆后的启动类class文件会没有注解。正常proguard.cfg配置片断如下:

# 保留Spring Boot启动类
-keep class com.hka.business.uaaserver.UaaCenterApplication { *;}
# 保留Spring Boot启动类注解
-keepclassmembers class com.hka.business.uaaserver.UaaCenterApplication {
    @* *;
}

对于其他普通的类也是一样,比如我们需要保留工程的bi模块不受代码混淆影响,也是需要同时配置相关类和注解保留配置,比如Lambda、Slf4j、mybatis以及spring注解等。proguard.cfg配置片断如下:

# 保留包及其类上的注解
-keep class com.hka.business.uaaserver.message.**,com.hka.business.uaaserver.bi.** { *; }
-keepclassmembers class com.hka.business.uaaserver.message.**,com.hka.business.uaaserver.bi.** {
    @* *;
}

Spring Bean 注入问题

在SpringBoot框架中,存在大量基于接口+依赖注入以及动态刷新机制来扩展第三方框架,例如集成SpringBoot Security Oauth2框架时,我们常会通过接口+依赖注入以及动态刷新机制来扩展ClientDetailsService,通过继承JdbcClientDetailsService ,扩展客户端加载机制,在使用数据库数据源基础增加redis缓存。但是我们在注入ClientDetailsService依赖时,无需显示指定注入RedisClientDetailsServiceImpl Bean。

@Slf4j
@Service
public class RedisClientDetailsServiceImpl extends JdbcClientDetailsService {
// 省略

public SecurityBrowserConfig(AuthenticationEntryPoint authenticationEntryPoint, CustomAccessDeniedHandler customAccessDeniedHandler, TokenStore tokenStore, UserDetailsService userDetailsService, RedisClientDetailsServiceImpl clientDetailsService) {
        this.authenticationEntryPoint = authenticationEntryPoint;
        this.customAccessDeniedHandler = customAccessDeniedHandler;
        this.tokenStore = tokenStore;
        this.userDetailsService = userDetailsService;
        // 这里仅需要通过接口方式动态注入Bean依赖
        this.clientDetailsService = clientDetailsService;
    }

但是通过代码混淆后,无法正常启动服务,出现异常提示如下:

2025-02-20 10:58:03.930 WARN [main]org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext.refresh:559 -Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userApiController' defined in URL [jar:file:/app.jar!/BOOT-INF/classes!/com/hka/business/uaaserver/interfaces/web/api/UserApiController.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userApplicationImpl' defined in URL [jar:file:/app.jar!/BOOT-INF/classes!/com/hka/business/uaaserver/application/impl/UserApplicationImpl.class]: Unsatisfied dependency expressed through constructor parameter 2; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'securityBrowserConfig' defined in URL [jar:file:/app.jar!/BOOT-INF/classes!/com/hka/business/uaaserver/config/SecurityBrowserConfig.class]: Unsatisfied dependency expressed through constructor parameter 4; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.security.oauth2.provider.ClientDetailsService' available: expected single matching bean but found 2: redisClientDetailsServiceImpl,clientDetailsService

这里问题暂时没通过直接修改项目的proguard.cfg配置解决(目前网上暂时没有相关直接的解决方案),而是通过曲线救国方式解决。主要是将项目的授权逻辑剥离封装成独立的纯Java依赖项目,在构建依赖项目时进行代码混淆,避免代码混淆影响Spring Bean依赖关系注入。实际的项目通过私有仓库引入混淆后的依赖包达到代码混淆的目的。

具体实施步骤:

  • 剥离授权逻辑,抽象成纯Java项目,没有任何Bean依赖和注解。
  • 依赖项目集成proguard-maven-plugin插件,支持在推送依赖到私有仓库时进行代码混淆,具体就是在执行mvn:deploy命令触发代码混淆。
  • 推送依赖到私有仓库
  • 应用项目引入授权依赖

下面是依赖项目的proguard-maven-plugin插件配置:
基本和上文的配置一致,只是多了deploy触发proguard的配置。

 <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-compiler-pluginartifactId>
                <version>3.3version>
                <configuration>
                    <source>1.8source>
                    <target>1.8target>
                configuration>
            plugin>
            
            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-deploy-pluginartifactId>
                <version>2.8.2version>
                <configuration>
                    <skip>falseskip>
                    <file>${project.build.directory}/${project.build.finalName}.jarfile> 
                configuration>
            plugin>
            
            <plugin>
                <groupId>com.github.wvengengroupId>
                <artifactId>proguard-maven-pluginartifactId>
                <version>2.6.0version>
                <executions>
                    
                    <execution>
                        <id>package-proguardid>
                        <phase>packagephase>
                        <goals>
                            <goal>proguardgoal>
                        goals>
                    execution>
                    
                    <execution>
                        <id>deploy-proguardid>
                        <phase>deployphase>
                        <goals>
                            <goal>proguardgoal>
                        goals>
                    execution>
                executions>
                <configuration>
                    <proguardVersion>6.2.2proguardVersion>
                    <injar>${project.build.finalName}.jarinjar>
                    <outjar>${project.build.finalName}.jaroutjar>
                    <obfuscate>trueobfuscate>
                    <proguardInclude>${project.basedir}/proguard.cfgproguardInclude>
                    <putLibraryJarsInTempDir>trueputLibraryJarsInTempDir>
                    <libs>
                        <lib>${java.home}/lib/rt.jarlib>
                        <lib>${java.home}/lib/jce.jarlib>
                        <lib>${java.home}/lib/jsse.jarlib>
                    libs>
                    <inLibsFilter>!META-INF/**,!META-INF/versions/9/**.classinLibsFilter>
                    <outputDirectory>${project.basedir}/targetoutputDirectory>
                    <options>
                    options>
                configuration>
                <dependencies>
                    <dependency>
                        <groupId>net.sf.proguardgroupId>
                        <artifactId>proguard-baseartifactId>
                        <version>6.2.2version>
                    dependency>
                dependencies>
            plugin>
        plugins>
    build>

proguard.cfg配置如下:
纯Java依赖项目的proguard.cfg配置就非常简单,仅需要保留保留所有公共类和方法配置,其他基本可以全部使用插件的默认配置。

# 指定Java的版本
-target 1.8

# 保留所有公共类和方法
-keep public class * {
    public *;
}
# 忽略 javax.activation 包中的类
-dontwarn javax.activation.**

# 忽略 javax.xml.bind 包中的类
-dontwarn javax.xml.bind.**

# 忽略 module-info 类
-dontwarn module-info

-ignorewarnings

-dontnote

以上就是我们使用proguard代码混淆的分享。也希望有大佬看到我的帖子可以帮忙分享Spring Bean 注入问题的解决方案。

参考

A慧眼如炬-ProGuard加密混淆Java代码
proguard

你可能感兴趣的:(spring,boot,后端,java)