自研一套带双向认证的Android通用网络库

news2025/7/18 18:41:53

当前,许多网络库基于Retrofit或OkHttp开发,但实际项目中常需要定制化,并且需要添加类似双向认证等安全功能。这意味着每个项目都可能需要二次开发。那么,有没有一种通用的封装方式,可以满足大多数项目需求?本文将介绍一种通用化的封装方法,帮助你用最少的代码量开发出自己的网络库

框架简介

FlexNet 网络库是基于 Square 公司开源的 Retrofit 网络框架进行封装的。Retrofit 底层采用 OkHttp 实现,但相比于 OkHttp,Retrofit 更加便捷易用,尤其适合用于 RESTful API 格式的请求。

在网络库内部,我们实现了双向认证功能。在初始化时,您可以选择是否开启双向认证,框架会自动切换相应的 URL,而业务方无需关注与服务端认证的具体细节。

接入方式

1. 本地aar依赖

下载aar到本地(下载地址见文末),copy到app的libs目录下,如图:

image.png

implementation(files("libs/flex-net.aar"))

然后sync只会即可

2. 通过Maven远程依赖

FlexNet目前已上传Maven,可通过Maven的方式引入,在app的build.gradle中加入以下依赖:

implementation("com.max.android:flex-net:3.0.0")

sync之后即可拉到Flex-Net

快速上手

网络库中默认打开了双向认证,并根据双向认证开关配置了相应的 baseUrl,大多数场景下只需要控制双向认证开关,其余配置走默认即可。

  1. 初始化

在发起网络请求之前(建议在ApplicationonCreate()中),调用:

fun initialize(
    app: Application,
    logEnable: Boolean = BuildConfig.LOG_DEBUG,
    sslParams: SSLParams? = null,
)
  • application: Application类型,传入当前App的Application实例;
  • logEnable: Boolean类型,网络日志开关,会发打印Http的Request和Resonpse信息,可能涉及敏感数据,release包慎用;(仅限网络请求日志,和双向认证的日志不同)
  • sslParams: 双向认证相关参数,可选,为空则关闭双向认证。具体描述见下文。

当App需要双向认证功能时,需要在initialize()方法中传递sslParams参数,所有双向认证相关的参数都放在sslParams当中,传此参数默认打开双向认证。

SSLParams的定义如下:

data class SSLParams(
    /** App 是否在白名单之中。默认不在 */
    val inWhiteList: Boolean = false,
    /** 双向认证日志开关,可能涉及隐私,release版本慎开。默认关 */
    val logSwitch: Boolean = true,
    /** 是否开启双向认证。默认开 */
    val enable: Boolean = true,
    /** 双向认证回调。默认null */
    val callback: MutualAuthCallback = null,
)
  • inWhiteList: App是否在白名单中,默认不在
  • logSwitch: 双向认证日志开关,可能涉及隐私,release版本慎开。默认关,注意这里仅针对双向认证日志,与initialize()方法中的logEnable不同
  • callback 监听初始化结果回调,true表示成功,反之失败。可选参数,默认为null,仅enableMutualAuth为true时有效

在调用了initialize之后就完成了初始化工作,内部包含了双向认证、网络状态、本地网络缓存等等功能,所有的网络请求都需要在初始化之后发起。

初始化示例代码:

FlexNetManger.initialize(this,
    logEnable = true,
    SSLParams {
        Timber.i("Mutual auth result : $it")
    })

PS * *部分App在启动的时候获取不到证书,所以这里会失败。如果失败了后续可以在合适的时机通过MutualAuthenticate.isSSLReady()来检查是否认证成功,然后通过MutualAuthenticate.suspendBuildSSL()来主动触发双向认证,成功之后方可开始网络请求。具体可参见文档“配置项”的内容。

双向认证失败及其相关问题,可参考双向认证文档 [双向认证])

  1. 定义数据 Model

在请求之前需要根据接口协议的字段定义对应的数据Model,用来做Request或者Response的body。

比如我们需要通过UserId获取对应用户的UserName

  1. 定义 Request 数据 Model

后端请求接口参数如下:

{
    "userId" : "123456"
}

那么根据参数定义一个UserNameReq类:

data class UserNameReq(
    /** 用户id */
var userId: String
)
  1. 定义 Response 数据 Model

后端返回数据如下:

{
    "userName" : "MC"
}

对应定义一个UserNameRsp:

data class UserNameRsp(
    /** 用户id */
var userId: String
)
  1. 编写 Http 接口

接口类必须继承自IServerAPI:

interface UserApi: IServerApi

然后在IServerApi的实现类中,每个接口需要用注解的形式标注 Http 方法,通过参数传入 http 请求的 url:

interface UserApi: IServerApi {

    /** 获取用户ID */
    @POST("api/cloudxcar/atmos/v1/getName")
    suspend fun getUserName(@Body request: UserNameReq): ResponseEntity<UserNameRsp>
}

这里需要注意的是,我们的UserNameRsp需要用ResponseEntity封装一层,看一下ResponseEntity的内容:

sealed class ResponseEntity<T>(val body: T?, val code: Int, val msg: String)

有3个参数:

  • body: 消息体,即UserNameReq。仅成功时有效

  • code 返回码,这里要分多种情况描述。

    • Http错误:此时code为Http错误码
    • 其他异常:code对应错误原因,后面会附上映射表
    • 请求成功:区分网络数据和缓存数据
  • msg 错误信息

可调用ResponseEntity.isSuccessful()来判断是否请求成功,然后通过ResponseEntity.body获取数据,返回的是一个根据服务端返回的 Json 解析而来的UserNameRsp实体类。

如果请求失败,则从ResponseEntity.msgResponseEntity.code中获取失败ma失败码和失败提示

  1. 创建网络请求Repo

继承自BaseRepo,泛型参数为步骤3中创建的IserverApi实现类:

class VersionRepo : BaseRepo<VersionAPI>
  1. 其中需要有1个必覆写的变量:

    1. baseUrl: 网络接口的baseUrl
  2. 两个可选项:

    1. mutualAuthSwitch: 双向认证开关,此开关仅针对当前 baseUrl 生效。默认开
    2. interceptorList: 需要设置的拦截器列表
  3. 一个必覆写的方法:

    1. createRepository(): 创建当前网络仓库

完整的Repo类内容如下:

class UserRepo: BaseRepo<UserApi>() {
    // 必填
    override val baseUrl = "https://juejin.cn/editor/drafts/7379502040140218422"
    // 必填
    override fun createRepository(): VersionAPI =
        MutualAuthenticate.getServerApi(baseUrl, mutualAuthSwitch, interceptorList)
    // 可选:双向认证开关,仅针对当前repo生效
    override val mutualAuthSwitch = true
    // 可选:Http拦截器
    override val interceptorList: List<Interceptor>? = listOf(HeaderInterceptor())
        
    // 请求接口
    suspend fun getUserName(): ResponseEntity<UserNameRsp>{
        return mRepo.upgradeVersion(UserNameReq("123456"))
    }
}

注: 其中拦截器的设置interceptorList,如果声明的时候提示错误,可以尝试加上完整的类型声明:

interceptorList: List<Interceptor>?

5 发起网络请求

最后就可以在业务代码中通过Repo类完成网络请求的调用了:

lifecycleScope.launch {
    val entity= UserRepo().getUserName()
    Timber.i("Get responseEntity: $entity")
    
    if (entity.isSuccessful()) {
        val result = entity.body
        Timber.i("Get user name result: $result")
    } else {
        val code = entity.code
        val msg = entity.msg
        Timber.i("Get user name failed: code->$code; msg->$msg")
    }
}

到这里,就可以发起一次基础的网络请求接口了。

依赖项

  1. 双向认证


目前引入的双向认证版本为1.6.0,如果需要切换版本,或者编译出现依赖冲突,可以尝试使用exclude的方式自行依赖。当然也请自行确保功能正常。

  1. 日志库

implementation("com.jakewharton.timber:timber:4.7.0")

组件库中的日志库。FlexNet推荐宿主使用Timber进行日志输出,但是需要宿主App在初始化FlexNet之前对Timber做plant操作。

  1. 网络请求内核

// Net
implementation ("com.squareup.retrofit2:retrofit:2.9.0")  
implementation ("com.squareup.retrofit2:converter-gson:2.9.0")

底层网络请求目前依赖OkHttp完成。

  1. 本地持久化

implementation("com.tencent:mmkv:1.2.14")

网络库中的本地存储,主要用于保存网络缓存,目前采用MMKV-1.2.14版本,同样如果有冲突,或者需要另换版本,可通过exclude实现。

  1. Gson

api(core.network.retrofit.gson) {
    exclude(module = "okio")
    exclude(module = "okhttp")
}

依赖Gson,用于做数据结构和Json的相互转化

错误码对照表

CODE_SUCCESS10000请求成功,数据来源网络
CODE_SUCCESS_CACHE10001返回成功,数据来源于本地缓存
CODE_SUCCESS_BODY_NULL10002请求成功,但消息体为空
CODE_ERROR_UNKNOWN-200未知错误
CODE_ERROR_UNKNOWN_HOST-201host解析失败,无网络也属于其中
CODE_ERROR_NO_NETWORK-202无网络

日志管理

从FlexNet 2.0.5开始,对接入方使用的日志库不再限制(2.0.5以下必须用Timber,否则无日志输出)。可以通过以下接口来设置日志监视器:

setLogMonitor(log: ILog)

设置之后所有的网络日志都会回调给ILog,即可由接入方自行决定如何处理日志数据。

如果没有设置LogMonitor,则会使用Timber或者Android原生Log来进行日志输出。当宿主App的Timber挂载优先于FlexNet的初始化,则会采用Timber做日志输出,反之使用Android Log。

文件下载

网络库内置了下载功能,可配置下载链接和下载目录。注意外部存储地址需要自行申请系统权限。

1 构建下载器

使用Downloader.builder()来构建你的下载器,Builder需要传入以下参数:

  • url:待下载文件的url
  • filePath:下载文件路径
  • listener:下载状态回调。可选参数,空则无回调

示例代码如下:

Downloader.Builder("https://juejin.cn/editor/drafts/7379502040140218422.zip",
    File(requireContext().filesDir, "MC").absolutePath)

2 回调监听

builder()最后一个参数,可传入下载监听器接口DownloadListener,内部有3个方法需要实现:

  • onFinish(file: File): 下载完成,返回下载完成的文件对象
  • onProgress( progress : Int, downloadedLengthKb: Long, totalLengthKb: Long): 下载进度回调,回传进度百分比、已下载的大小、总大小
  • onFailed(errMsg: String?): 下载失败,回调失败信息

示例代码如下:

val downloader = Downloader.Builder("https://juejin.cn/editor/drafts/7379502040140218422.zip",
    File(Environment.getExternalStorageDirectory(), "MC").absolutePath,
    object : DownloadListener {
        override fun onFinish(file: File) {
            Timber.e("下载的文件地址为:${file.absolutePath}".trimIndent())
        }

        override fun onProgress(
            progress: Int,
            downloadedLengthKb: Long,
            totalLengthKb: Long,
        ) {
            runOnUiThread {
                textView.text =
                    "文件文件下载进度:${progress}% \n\n已下载:%${downloadedLengthKb}KB | 总长:${totalLengthKb}KB"
            }
        }

        override fun onFailed(errMsg: String?) {
            Timber.e("Download Failed: $errMsg")
        }
    }).build()

PS 这里要注意,FlexNet会在业务方调用下载的线程返回下载回调,所以绝大部分时候回调是发生在子线程,此时如果有线程敏感的功能(比如刷新UI),需要自行处理线程切换。

3 触发下载

通过Builder.build()创建 Downloader 下载器,最后调用Downloader.download()方法即可开始下载。

和Http Request一样,download()是一个suspend方法,需要在协程中使用:

lifecycleScope.launch(Dispatchers.IO) {
    downloader.download()
}

整体架构

设置配置项

1. 设置双向认证开关

在初始化的时候控制双向认证开关:

fun init(context: Application, needMutualAuth: Boolean = true)

方法内部会根据开关值来切换不同的后端服务器,但是有些App不能过早的获取证书,这样会有双向认证失败的风险,FlexNet同时支持懒汉式的主动双向认证

2. 主动双向认证接口

在确定拿到证书,或者确定可以双向认证的时机,可随时发起双向认证请求:

MutualAuthenticate.suspendBuildSSL()

可通过

MutualAuthenticate.isSSLReady()

接口来检查当前双向认证是否成功。

主动触发示例代码如下:

MutualAuthenticate.suspendBuildSSL {
    if (it) {
        Toast.makeText(context, "双向认证成功,可以开始访问加密资源", Toast.LENGTH_SHORT).show()
    } else {
        Toast.makeText(context, "双向认证失败", Toast.LENGTH_SHORT).show()
    }
}

3. 数据缓存

在前面发起请求调用httpRequest顶层函数的时候,可以传入一个可选参数cacheKey,这个key不为空则网络库会在本地保存当前请求的返回数据。Key作为缓存的唯一标识,在无网络或请求失败的时候,会通知调用方错误,并返回缓存的数据。

缓存部分流程如下:

4. 错误及异常处理

在发起请求的顶层函数 httpRequest 中,有两个参数用来提供给调用方处理错误和异常。

首先区分一下错误和异常:

错误通常是发起了网络请求,且网络请求有响应,只是由于接口地址或者参数等等原因导致服务端解析失败,最终返回错误码及错误信息。

而异常是指在发起网络请求的过程中出现了 Exception,导致整个网络请求流程被中断,所以当异常发生的时候,网络库是不会返回错误码和错误信息的,只能返回异常信息供调用方定位问题。

回调的使用方式很简单,只需要在httpRequest中传入两个回调:failerror,下面分别看看二者的处理方式:

1. 错误处理

fai的定义如下:

fail: (response: ResponseEntity<T>) -> Unit = {
    onFail(it)
}

传入的回调有一个 ResponseEntity 参数,这是网络请求返回的响应实体,内部包含errorCodeerrorMessage,不传则默认打印这两个字段,可以在 Logcat 中通过Tag:Http Request **过滤出来。

2. 异常处理

error的定义如下:

error: (e: Exception) -> Unit = {
 onError(it)
} ,

回调函数只有一个 Exeption 对象,和前面的定义相符,在异常的时候将异常返回供调用方定位问题。不传网络库默认打印异常,可以在 Logcat 中通过Tag:Http Request **过滤出来。

扩展接口:发起请求并处理返回结果

网络库定义了一个顶层函数用来发起请求并接收返回结果或处理异常:

fun <reified T> httpRequest(block, fail, error, cacheKey): T?

  • block: 实际请求体,必填。可以传入步骤 4 中实现的接口
  • fail: 请求错误回调,非必填。用来处理服务端返回的请求错误,会携带错误码及错误信息
  • error: 请求异常回调,非必填。用来处理请求中发生的异常,此时没有response返回
  • cacheKey: 数据缓存唯一标识,非必填

httpRequest 中的泛型 T 就是接入步骤2定义的 Response 实体,正常返回会在方法内部自动解析出 UserNameRsp,到此就完成了一次网络请求。

以上是基本的使用方式,涵盖了安全、数据请求、缓存、异常处理等功能,可以适应于多种项目场景。应大家的建议,后续会完善几篇文章拆解具体的原理及开发思路,从源码的角度教你如何从0开发一套完善的网络库

需要体验的同学可以在评论区留下联系方式,我给你发送aar以及源码。有问题欢迎随时探讨

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1822239.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

什么是端口转发?路由器如何正确的设置端口转发和范围转发?(外网访问必备设置)

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 端口转发 📒🚀 端口转发的应用场景💡 路由器如何设置端口转发(示例)💡 端口范围转发(示例)🎯 范围转发的应用场景🛠️ 设置范围转发📝 范围转发实操示例🎈 注意事项 🎈⚓️ 相关链接 ⚓️📖 介绍 📖 …

如何平衡安全访问和办公效率?零信任安全×统一身份才是解决之道

在远程办公、混合办公、跨团队协作日益频繁的今天&#xff0c;企业的业务开展需要支持多种访问接入的需求和场景。如何平衡企业数据的安全访问和办公效率将成为挑战。 在业务的多种接入场景上&#xff0c;企业引入零信任&#xff08;Zero Trust&#xff0c;ZT&#xff09;产品…

鸿蒙用 BuilderParam 实现同一个布局不同内容组件

面通过一个案例展示BuilderParam的具体用法&#xff0c;例如&#xff0c;现需要实现一个通用的卡片组件&#xff0c;如下图所示 卡片中显示的内容不固定&#xff0c;例如 具体实现代码如下&#xff1a; Entry Component struct BuildParamDemo {build() {Column(){Card() {imag…

aac如何转化mp3?超好用的四种音频转换方法!

aac如何转化mp3&#xff1f;AAC格式可能鲜为人知&#xff0c;但实际上它是一种音频文件格式&#xff0c;然而&#xff0c;AAC的应用却不太广泛&#xff0c;这并非偶然&#xff0c;首先&#xff0c;使用AAC需要支付专利费用&#xff0c;这对于个人和公司都可能是一笔不小的开支&…

【网络安全】【深度学习】【入侵检测】SDN模拟网络入侵攻击并检测,实时检测,深度学习【一】

文章目录 1. 前言2. Mininet 和 Ryu 的区别2.1 Mininet2.2 Ryu2.3 总结 3. 模拟攻击3.1 环境准备3.2 创建 Mininet 网络拓扑3.2 启动 Ryu 控制器3.3 模拟网络攻击3.4 捕获流量 4. 实时异常检测4.1 在 Ryu 控制器中4.2 在 h2 机器上的实验结果4.3 深度学习模型部署上h2机器 帮助…

Git保姆级教程

目录 Git是什么&#xff0c;为什么要学这个工具&#xff1f; 码云注册并创建仓库 Git安装 查看本地仓库状态 添加到暂存区 提交到本地库 修改文件 版本回退 创建、切换和删除分支 合并分支 克隆远端库到本地 将本地库推送到远端库 命令设置别名 Git是什么&#xf…

Qt 竖排文字研究(一)

在传统的编程环境中&#xff0c;代码的排列方式通常是水平的&#xff0c;这是基于我们日常的阅读习惯和编程规范。但是&#xff0c;当我们尝试打破这一常规&#xff0c;将代码字符以竖排的方式呈现时&#xff0c;发现没有什么现成有效的方式。所以本文基于Qt 的场景视图下&…

水表摄像直读抄表仪

1.技术性简述 水表摄像直读抄表仪&#xff0c;是一种前沿的智能计量机器设备&#xff0c;它利用超清摄像头部和图像识别算法&#xff0c;完成了远程控制、非接触的水表载入。这一技术的普及&#xff0c;颠覆了传统式人力抄表的形式&#xff0c;提高了效率&#xff0c;降低了不…

eclipse如何导入springboot项目

打开eclipse 找到你的springboot项目 点击finish即可 test02就已经导入进去了 配置一下maven 在将那个springboot项目刷新一下即可 运行成功

搭建 Redis 集群【Windows】

Redis 集群是一个分布式存储解决方案&#xff0c;它将数据分布在多个Redis节点上&#xff0c;以提高系统的可伸缩性、可靠性和性能。 1. 集群概念与特点 集群概念&#xff1a;Redis集群是由多个相互独立的 Redis 节点组成&#xff0c;这些节点通过高速网络互联&#xff0c;并作…

Java多线程-StampedLock(原子读写锁)

StampedLock 是读写锁的实现&#xff0c;对比 ReentrantReadWriteLock 主要不同是该锁不允许重入&#xff0c;多了乐观读的功能&#xff0c;使用上会更加复杂一些&#xff0c;但是具有更好的性能表现。StampedLock 的状态由版本和读写锁持有计数组成。 获取锁方法返回一个邮戳&…

源代码防泄密经验分享之安全上网篇

场景描述&#xff1a; 随着信息技术的发展&#xff0c;越来越多的新技术产品进入到政府、军事、科研等涉密单位。这些新技术产品在给工作人员带来便利的同时&#xff0c;也给信息安全保密工作带来了许多新的不容忽视的安全隐患&#xff0c;应引起高度重视。常规的内外网隔离手…

VSCode插件开发之初始化项目

VS code常见组件 在VS Code插件开发中&#xff0c;常用的组件有很多&#xff0c;这些组件可以帮助你实现各种功能和交互。以下是一些常见的组件&#xff1a; Extension API模块: 提供了许多类和方法&#xff0c;用于与VS Code编辑器进行交互&#xff0c;例如vscode.workspace用…

抽象语法树AST(Abstract Syntax Tree)

抽象语法树(Abstract Syntax Tree) 抽象语法树&#xff08;Abstract Syntax Tree&#xff0c;AST&#xff09;是源代码语法结构的一种抽象表示它以树状的形式表现编程语言的语法结构&#xff0c;树上的每个节点都表示源代码中的一种结构 抽象语法树用途 代码语法的检查、代码…

辣椒属2个T2T基因组-文献精读23

Two telomere-to-telomere gapless genomes reveal insights into Capsicum evolution and capsaicinoid biosynthesis 两个端粒到端粒无缝基因组揭示了辣椒进化和辣椒素生物合成的相关见解 摘要 辣椒&#xff08;Capsicum&#xff09;因其果实中含有辣椒素而闻名&#xff0c…

【SQL每日一练】获取北纬度(LAT_N)的中位数

文章目录 前言一、题析二、题解1.mysql2.sqlserver 前言 从 STATION 查询北纬度 &#xff08;LAT_N&#xff09; 的中位数&#xff0c;并将您的答案四舍五入到小数点后4位. 中位数的定义是&#xff1a;如果数据量是奇数&#xff0c;则中位数是排序后位于中间的数&#xff1b;如…

拥抱数字世界|AI在娱乐行业的应用,娱乐新纪元已到来

在蓬勃发展的全球化趋势下&#xff0c;越来越多的厂商正在批量涌入娱乐赛道&#xff0c;期待能创造新的增长奇迹。随着科技的不断发展&#xff0c;人工智能技术正日益深入各行各业&#xff0c;其中媒体和娱乐行业更是迎来了一场革命性的变革。在媒体和娱乐领域展现出了巨大的潜…

海康威视-NVR使用及ISAPI协议透传接入

目录 1、初始化配置 1.1、设置通道默认密码 1.2、添加摄像头 1.3、设置不采集时间段 1.4、抓拍延迟设置 1.5、录像保存时长设置 1.6、人脸库维护 1.7、导入照片 1.8、设置事件 1.8.1、引擎配置 1.8.2、事件设置 1.8.2.1、目标比对 1.8.2.2、设置屏蔽区 1.8.2.3、…

每日一练:攻防世界:北京地铁

首先是找图片隐写 在这里可以看到一串类似base64格式的字符串 再结合题目&#xff0c;这应该就是明文了&#xff0c;要AES解密&#xff0c;还需要密钥&#xff0c;提示要看图片本身&#xff0c;那密钥可能藏在里面&#xff0c;找了半天没找到&#xff0c;参考师傅的wp&#x…

Docker:利用Docker搭建一个nginx服务

文章目录 搭建一个nginx服务认识nginx服务Web服务器反向代理服务器高性能特点 安装nginx启动nginx停止nginx查找nginx镜像拉取nginx镜像&#xff0c;启动nginx站点其他方式拉取nginx镜像信息通过 DIGEST 拉取镜像 搭建一个nginx服务 首先先认识一下nginx服务&#xff1a; NGI…