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」:
| 字段 | 类型 | 默认值 | 含义 |
|---|---|---|---|
enabled | Boolean | false | 总开关,关闭时所有伪装信号不生效 |
deviceType | DeviceType | PHONE | 设备大类:PHONE / TABLET / DESKTOP / LAPTOP / WATCH / TV |
deviceOS | DeviceOS | ANDROID | 操作系统:Android、iOS、HarmonyOS、Windows、macOS、Linux、ChromeOS、watchOS、Wear OS、tvOS 共 10 种 |
deviceBrand | DeviceBrand | SAMSUNG | 品牌,共 30 种,每种通过supportedTypes/supportedOS声明适用的设备大类与系统 |
deviceModel | String | "" | 型号标识,写入 UA 的机型位(如SM-S938B),为空时按品牌回落到默认型号 |
deviceModelName | String | "" | 设备展示名(如Galaxy S26 Ultra) |
customUserAgent | String? | null | 完整自定义 UA,非空时覆盖一切自动生成 |
screenWidth/screenHeight | Int | 0 | 伪装分辨率,0表示未设置 |
pixelDensity | Float | 0f | 伪装像素密度(DPR) |
isDesktopViewport | Boolean | false | 强制桌面视口,即使设备类型不是桌面/笔记本 |
isCustomDevice | Boolean | false | 标记该配置来自「自定义设备」表单 |
同文件还定义了两个枚举(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 的多语言资源):
- 总开关:
WtaToggleRow切换enabled。 - 快速选择(Quick Select):FlowRow 排列 手机 / 平板 / 桌面 / 笔记本 / 手表 五个 Chip。点击某个类型时,会取该类型预置列表的第一项并整体应用(
DevicePresets.getPresetsForType(type),列表非空时presets.first().toConfig());无预置的类型(如 TV)则仅切换deviceType。 - 热门设备(Popular Devices):针对当前
deviceType列出预置设备 Chip,选中态以deviceModel + deviceBrand双字段比对;点击后应用预置配置但保留当前设备大类。 - 强制桌面视口(Force Desktop Viewport):仅在设备大类不是 DESKTOP/LAPTOP 时显示,提示语为「使用桌面宽度加载网页」。
- 自定义设备:开启后展开五个输入框——设备名称(如
Galaxy S26 Ultra)、型号标识(如SM-S938B)、屏幕宽/高(仅允许数字,占位符 1920/1080)、像素密度(占位符 2.0),点击「应用」按钮写入deviceModelName/deviceModel/screenWidth/screenHeight/pixelDensity并置isCustomDevice = true;名称留空时回落为Custom Device,型号留空时生成CUSTOM-<时间戳>。 - 自定义 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 × 956 | 3.0 |
| 手机 | Galaxy S26 Ultra(SM-S938B) | 412 × 915 | 3.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 × 1376 | 2.0 |
| 平板 | Galaxy Tab S11 Ultra(SM-X910N) | 1200 × 1848 | 2.0 |
| 桌面(5 台) | Windows PC 1080p / 1440p / 4K | 1920×1080 / 2560×1440 / 3840×2160 | 1.0 / 1.5 / 2.0 |
| 桌面 | iMac 24" M4(iMac21,1-M4) | 2560 × 1440 | 2.0 |
| 笔记本(8 台) | MacBook Pro 16" M5(MacBookPro19,1) | 1728 × 1117 | 2.0 |
| 笔记本 | Surface Pro 11、ThinkPad X1 Carbon G12、Chromebook Plus 等 | 1920×1080 起 | 1.0–2.0 |
| 手表(6 台) | Apple Watch Ultra 3(Watch8,1) | 205 × 251 | 2.0 |
| 手表 | Galaxy Watch Ultra(SM-L705)、HUAWEI WATCH GT 5 Pro | 187×187 / 194×194 | 2.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 |
| 设置了自定义 UA | CUSTOM |
| 手机/平板 + iOS | SAFARI_MOBILE |
| 手机/平板 + 其他 | CHROME_MOBILE |
| 桌面/笔记本 + macOS | SAFARI_DESKTOP |
| 桌面/笔记本 + Linux | FIREFOX_DESKTOP |
| 桌面/笔记本 + 其他 | CHROME_DESKTOP |
| 手表 | SAFARI_MOBILE |
| TV | CHROME_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()的优先级(高到低):
- 显式自定义 UA(
UserAgentMode.CUSTOM,仅当 CUSTOM 模式实际被选中); - 设备伪装生成的 UA(
deviceDisguiseUserAgent参数); - 选中的内核风味(KernelFlavor),旧版
UserAgentMode做迁移兼容; - 桌面模式(UA 与其视口策略一并改变);
- 持久化自 UA 模式之前的旧版纯
userAgent字符串; - 系统默认(
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),仅供参考