☰
web-to-app 设备伪装实战:用 DeviceDisguiseConfig 让生成的应用报告另一台设备的完整档案
2026/9/29 6:08:15 网站建设 项目流程

web-to-app 设备伪装实战:用 DeviceDisguiseConfig 让生成的应用报告另一台设备的完整档案

本篇以 web-to-app 的「设备伪装」功能为核心,讲透这项能力如何支撑:在编辑通用配置中开启伪装后,应用会向网页报告另一套设备型号/厂商、User-Agent 与屏幕参数;读完后你能理解DeviceDisguiseConfig的完整字段与生成规则、桌面视口 JS 伪装脚本(patchnavigator.platform、Client Hints 等多信号)的注入链路,以及它与 UA/Client Hints 一致性解析器BrowserIdentityResolver的协作方式。

功能定位与入口

设备伪装的功能定义很直接:伪造向网站报告的设备级标识和属性。它的配置入口位于编辑通用配置编辑器中的设备伪装卡片,由 DeviceDisguiseConfig.kt 中的DeviceDisguiseConfig数据类支撑,UI 由 DeviceDisguiseCard.kt 实现。

配置的作用是让生成的应用向网页内容呈现一套自定义的设备属性(例如型号/厂商及相关标识),使应用报告一个不同的设备档案。典型用途是:把手机浏览器伪装成 iPhone、Windows 桌面 PC 或 iPad,让网页端按对应设备档案返回内容(桌面版布局、桌面版功能等)。

设备伪装与浏览器级指纹伪装(50+ 维,在高级设置中配置)是互补关系:前者聚焦「报告哪台设备」,后者聚焦更广义的指纹面。

DeviceDisguiseConfig:字段一览

DeviceDisguiseConfig是纯 Kotlin 数据类(DeviceDisguiseConfig.kt),默认值即「关闭、手机、Android、Samsung」:

字段类型默认值含义
enabledBooleanfalse总开关,关闭时所有伪装信号不生效
deviceTypeDeviceTypePHONE设备大类:PHONE / TABLET / DESKTOP / LAPTOP / WATCH / TV
deviceOSDeviceOSANDROID操作系统:Android、iOS、HarmonyOS、Windows、macOS、Linux、ChromeOS、watchOS、Wear OS、tvOS 共 10 种
deviceBrandDeviceBrandSAMSUNG品牌,共 30 种,每种通过supportedTypes/supportedOS声明适用的设备大类与系统
deviceModelString""型号标识,写入 UA 的机型位(如SM-S938B),为空时按品牌回落到默认型号
deviceModelNameString""设备展示名(如Galaxy S26 Ultra)
customUserAgentString?null完整自定义 UA,非空时覆盖一切自动生成
screenWidth/screenHeightInt0伪装分辨率,0表示未设置
pixelDensityFloat0f伪装像素密度(DPR)
isDesktopViewportBooleanfalse强制桌面视口,即使设备类型不是桌面/笔记本
isCustomDeviceBooleanfalse标记该配置来自「自定义设备」表单

同文件还定义了两个枚举(L226-L246):DeviceType带 emoji 与显示顺序,DeviceOS带展示名。DeviceBrand则以「适用设备类型集合 + 适用系统集合」约束了品牌与设备大类的合法组合,例如SAMSUNG只支持 PHONE + Android,HUAWEI同时支持 Android 与 HarmonyOS,GENERIC_MAC支持 DESKTOP/LAPTOP + macOS。DeviceBrand.getBrandsForType(type)按设备大类过滤品牌并排序。

UI 操作流:卡片里的五个控件

从 DeviceDisguiseCard.kt 的实现看,卡片开启后依次展开以下控件(文案来自 Strings.kt 的多语言资源):

  1. 总开关:WtaToggleRow切换enabled。
  2. 快速选择(Quick Select):FlowRow 排列 手机 / 平板 / 桌面 / 笔记本 / 手表 五个 Chip。点击某个类型时,会取该类型预置列表的第一项并整体应用(DevicePresets.getPresetsForType(type),列表非空时presets.first().toConfig());无预置的类型(如 TV)则仅切换deviceType。
  3. 热门设备(Popular Devices):针对当前deviceType列出预置设备 Chip,选中态以deviceModel + deviceBrand双字段比对;点击后应用预置配置但保留当前设备大类。
  4. 强制桌面视口(Force Desktop Viewport):仅在设备大类不是 DESKTOP/LAPTOP 时显示,提示语为「使用桌面宽度加载网页」。
  5. 自定义设备:开启后展开五个输入框——设备名称(如Galaxy S26 Ultra)、型号标识(如SM-S938B)、屏幕宽/高(仅允许数字,占位符 1920/1080)、像素密度(占位符 2.0),点击「应用」按钮写入deviceModelName/deviceModel/screenWidth/screenHeight/pixelDensity并置isCustomDevice = true;名称留空时回落为Custom Device,型号留空时生成CUSTOM-<时间戳>。
  6. 自定义 User-Agent:多行输入框(2–4 行),提示「输入完整的 User-Agent 字符串(覆盖自动生成)」;关闭开关会清空customUserAgent。

设备预置库:55 台覆盖五大类

DevicePresets(DeviceDisguiseConfig.kt#L292-L422)内置了 55 台设备的完整档案,每个预置包含型号、展示名、分辨率与 DPR,toConfig()会一并设置enabled = true,并对 DESKTOP/LAPTOP 自动置isDesktopViewport = true。代表性条目:

大类预置示例分辨率DPR
手机(23 台)iPhone 17 Pro Max(iPhone18,2)440 × 9563.0
手机Galaxy S26 Ultra(SM-S938B)412 × 9153.5
手机Pixel 10 Pro XL、Xiaomi 17 Ultra、Pura 90 Pro(HarmonyOS)等412 × 915 居多2.6–3.5
平板(7 台)iPad Pro 13" M5(iPad17,1)1032 × 13762.0
平板Galaxy Tab S11 Ultra(SM-X910N)1200 × 18482.0
桌面(5 台)Windows PC 1080p / 1440p / 4K1920×1080 / 2560×1440 / 3840×21601.0 / 1.5 / 2.0
桌面iMac 24" M4(iMac21,1-M4)2560 × 14402.0
笔记本(8 台)MacBook Pro 16" M5(MacBookPro19,1)1728 × 11172.0
笔记本Surface Pro 11、ThinkPad X1 Carbon G12、Chromebook Plus 等1920×1080 起1.0–2.0
手表(6 台)Apple Watch Ultra 3(Watch8,1)205 × 2512.0
手表Galaxy Watch Ultra(SM-L705)、HUAWEI WATCH GT 5 Pro187×187 / 194×1942.0

TV 大类目前无预置(getPresetsForType(DeviceType.TV)返回空列表)。

User-Agent 生成规则

generateUserAgent()(L43-L85)是 UA 生成的单一来源,规则为:

  • enabled == false→ 返回空串,一切伪装不生效;
  • customUserAgent非空 → 直接原样返回;
  • 否则按「设备大类 × 操作系统」选择 UA 模板,机型位取deviceModel,为空时按品牌回落getDefaultModel()(如 SAMSUNG →SM-S938B、GOOGLE_PIXEL →Pixel 10 Pro、HUAWEI →ALT-AL10、APPLE →iPhone/iPad等 28 条品牌默认值)。

部分模板实际生成结果:

# Android 手机(含平板非 iOS 分支) Mozilla/5.0 (Linux; Android 15; SM-S938B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/<cv>.0.0.0 Mobile Safari/537.36 # iOS 手机(iOS 版本固定为 19.0,点号换下划线) Mozilla/5.0 (iPhone; CPU iPhone OS 19_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/<sv>.0 Mobile/15E148 Safari/604.1 # HarmonyOS 手机(带 HMSCore 标记) Mozilla/5.0 (Linux; Android 14; ALT-AL10; HMSCore 6.14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/<cv>.0.0.0 Mobile Safari/537.36 # Windows 桌面 / 笔记本 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/<cv>.0.0.0 Safari/537.36 # macOS 桌面 / 笔记本 Mozilla/5.0 (Macintosh; Intel Mac OS X 15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/<sv>.0 Safari/605.1.15 # Linux 桌面(Firefox 模板) Mozilla/5.0 (X11; Linux x86_64; rv/<fv>.0) Gecko/20100101 Firefox/<fv>.0 # TV(固定 Tizen 模板) Mozilla/5.0 (SMART-TV; Linux; Tizen 9.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/<cv>.0.0.0 Safari/537.36

其中<cv>/<sv>/<fv>来自UserAgentVersions.CHROME/SAFARI/FIREFOX(定义在 data/model 层),Chrome 与 Firefox 的 UA 使用真实浏览器版本号,Version/与Mobile/15E148等 Safari 位保持固定值。

与 UA 平行的还有toUserAgentMode()(L21-L41),把伪装配置折叠成内核可识别的UserAgentMode:

条件映射结果
未启用DEFAULT
设置了自定义 UACUSTOM
手机/平板 + iOSSAFARI_MOBILE
手机/平板 + 其他CHROME_MOBILE
桌面/笔记本 + macOSSAFARI_DESKTOP
桌面/笔记本 + LinuxFIREFOX_DESKTOP
桌面/笔记本 + 其他CHROME_DESKTOP
手表SAFARI_MOBILE
TVCHROME_MOBILE

这些映射分支与边界行为都有单元测试覆盖,见 DeviceDisguiseConfigTest.kt(含「禁用时返回空 UA」「自定义 UA 优先」「Android 手机 UA 含 Chrome/Android 关键词」「iOS 手机 UA 含 Safari/iPhone」等 20+ 用例)。

桌面伪装不只有 UA:document-start 伪装脚本

requiresDesktopViewport()(L120-L124)回答「该配置是否要求桌面视口」:enabled && (isDesktopViewport || 设备大类为 DESKTOP/LAPTOP)。需要桌面视口时,generateSpoofScript()(L135-L223)会生成一段文档起始 JS polyfill——源码注释明确点出动机:现代 PWA(如 Strava)使用多信号联合检测(platform、touchPoints、screen、matchMedia、Client Hints),只改 UA 是不够的。该脚本通过window.__wta_disguise__哨兵防止重复执行,逐项 patch:

(function(){ 'use strict'; if(window.__wta_disguise__)return; window.__wta_disguise__=true; // navigator.platform → "Win32" / "MacIntel" / "Linux x86_64"(按 deviceOS) try{Object.defineProperty(navigator,'platform',{get:function(){return '$platform';},configurable:true});}catch(e){} // navigator.maxTouchPoints → 0(桌面没有触控) try{Object.defineProperty(navigator,'maxTouchPoints',{get:function(){return 0;},configurable:true});}catch(e){} // screen 维度 → 伪装分辨率(未设置时默认 1920×1080,availHeight = 高度-40) var sw=$screenWidth,sh=$screenHeight,ah=$availHeight; var sProps={width:sw,height:sh,availWidth:sw,availHeight:ah,colorDepth:24,pixelDepth:24}; // ...逐项 defineProperty // window.devicePixelRatio / outerWidth / outerHeight // matchMedia 包装:(pointer: coarse) / (any-pointer: coarse) 的 matches 强制为 false // navigator.userAgentData:platform 改为 "Windows"/"macOS"/"Linux"/"Chrome OS", // mobile 改 false,getHighEntropyValues 的结果再改一次 mobile/platform // navigator.connection.effectiveType → "4g" })();

注意几个默认回落:screenWidth/screencHeight未填时脚本按 1920×1080 生成,pixelDensity未填时按 1.0。这段脚本的注入点在 WebViewManager.kt:WebView 初始化时判断ddConfig.enabled && ddConfig.requiresDesktopViewport(),成立则通过installDeviceDisguiseDocumentStart(this, disguiseScript)以文档起始脚本注入,保证早于页面 JS 执行。

同时,L1470 处桌面模式判定也接入了伪装配置:config.userAgentMode in DESKTOP_UA_MODES || config.desktopMode || currentDeviceDisguiseConfig?.requiresDesktopViewport() == true,也就是说设备伪装开启桌面档案后,视口策略与 UA 策略会被统一按桌面处理。

身份解析:UA 与 Client Hints 必须成对出现

UA 字符串最终如何生效,由 BrowserIdentity.kt 中的BrowserIdentityResolver统一裁决。该文件的注释解释了这个设计的根本原因:页面靠User-Agent请求头和引擎生成的Sec-CH-UA*Client Hints 两个信号判断「这是什么浏览器」,两者必须成对出现——UA 声称一个浏览器而 Client Hints 声称另一个,正是反爬系统判定伪造的典型特征。

resolve()的优先级(高到低):

  1. 显式自定义 UA(UserAgentMode.CUSTOM,仅当 CUSTOM 模式实际被选中);
  2. 设备伪装生成的 UA(deviceDisguiseUserAgent参数);
  3. 选中的内核风味(KernelFlavor),旧版UserAgentMode做迁移兼容;
  4. 桌面模式(UA 与其视口策略一并改变);
  5. 持久化自 UA 模式之前的旧版纯userAgent字符串;
  6. 系统默认(BrowserIdentity.SYSTEM_DEFAULT)。

设备伪装 UA 从哪来?WebViewManager.kt#L5448-L5459 的resolveBrowserIdentityFor()读取当前缓存的伪装配置,enabled时调用generateUserAgent()注入第 2 优先级。

另一个关键设计在UserAgentProfileDeriver(BrowserIdentity.kt#L128-L199):对非内核风味来源的 UA(用户手写的自定义 UA、设备伪装生成的 UA),用正则从 UA 字符串中反推平台(Android/iOS/Windows/macOS/Linux)与品牌列表(Chromium/Chrome、Edge,Firefox/Safari 则按真实行为留空 brands),从而让Sec-CH-UA*与 UA 自洽。若平台无法识别,解析器会整体放弃该来源而非发出一个与 hints 矛盾的 UA——describe()返回 null 并记录警告日志「Unrecognised platform in a custom User-Agent; keeping the engine default」。

配置如何被持久化与传递

从源码结构看,这份配置随应用全生命周期流转:

  • 数据模型:WebApp.kt#L123 持有deviceDisguiseConfig字段,数据库以 TEXT 列存储,AppDatabase.kt#L417 记录了 32→33 版本迁移专门新增deviceDisguiseConfig列(迁移脚本见 app/schemas 下的 Room 版本快照);
  • 导出 APK:ApkConfig.kt#L342 暴露deviceDisguiseConfig,ApkConfigJsonFactory.kt#L127 将其序列化进伪装配置块,ApkBuilder在 L4634 写入构建配置;
  • 运行态多入口:ShellModeManager.kt#L406-L407(shell 模式 JSON 字段)、FloatingWindowService.kt#L438(悬浮窗)、EngineViewFactory.kt#L62(引擎视图工厂)都会把该配置传入 WebView 管理层;
  • 发布前检查:PlayPolicyChecker.kt#L79-L86 在应用启用设备伪装时会标记一条策略项并给出修复提示,说明该能力在应用商店合规检查中是被识别为特殊行为的。

与浏览器级指纹伪装的关系

按设备伪装文档的说明:设备伪装补充了浏览器级指纹伪装(50+ 维),后者在高级设置中配置。两者的分工可以概括为:设备伪装负责「设备档案」这一层——设备大类、操作系统、品牌型号、UA、屏幕与桌面端多信号 JS 补丁;而高级设置中的浏览器指纹伪装(由 BrowserDisguiseJsGenerator.kt 生成)覆盖 canvas、WebGL、字体等更宽的指纹面。若目标是「让网站认为我是一台 Windows PC 上的 Chrome」,设备伪装是主力;若还需要同步压制其他指纹维度,再叠加高级设置中的指纹伪装。

小结

  • 入口:编辑通用配置 → 设备伪装卡片,核心数据类为DeviceDisguiseConfig;
  • 三种用法:快速选设备大类、点热门设备预置(55 台内置档案)、自定义设备/自定义 UA;
  • UA 生成按「大类 × 系统」选模板,型号位支持品牌默认回落,自定义 UA 优先级最高;
  • 桌面档案会自动触发requiresDesktopViewport(),进而统一切换视口策略,并注入一段 document-start 脚本 patchnavigator.platform、maxTouchPoints、screen/window 维度、matchMedia、Client Hints 与effectiveType;
  • 最终 UA 由BrowserIdentityResolver按六级优先级裁决,且强制 UA 与Sec-CH-UA*成对自洽,避免「自相矛盾的伪装」被识别为伪造。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询