webview 和 原生通讯 安全性

nicepainkiller 2024-08-08 08:33:02 阅读 79

业务背景

App 内嵌 H5 应用的场景需求很常见,活动页面 或者 经常需要修改的页面也都可以做成H5内嵌的形式。那么 因为是 H5页面可以直接访问。安全性就大大降低。所以更多的时候 有些功能 接口调用还是放在 原生来完成,因为原生层面的 安全手段会更多一些。这样 H5层面仅作展示,对安全有要求的接口 可以都放在原生来完成。那么原生层和H5的通信 处理如何分装的 更高效 称为一个 有意思的点。

 调用H5页面的安全性

一般我们 H5 页面也都是有API 请求的,那么如何把 请求所需要的基础信息, 例如 请求地址请求接口的token, 传递给 H5页面, 最早参加工作的时候 是通过 url,传递参数的,全部通过明文的方式,最多加密处理下;但是 js 的 逆向更容易一些。后来又学会了另一种方式 JS注入,直接调用将基础数据 注入到 webview 的 window 上面 这样H5页面无法脱离App独立使用:

url 传参方式

<code>明文

http://www.shandw.com/auth/?channel=14073&openid=12345678&time=1597336113&nick=宝贝&avatar=http://363.com/images/logo.png&sex=1&phone=18368192452&sign=42dabe44a5343be9f336f4a30e7b0b86&sdw_simple=7

base64

http://www.shandw.com/auth/?data=Y2hhbm5lbD0xNDA3MyZvcGVuaWQ9MTIzNDU2NzgmdGltZT0xNTk3MzM2MTEzJm5pY2s95a6d6LSdJmF2YXRhcj1odHRwOi8vMzYzLmNvbS9pbWFnZXMvbG9nby5wbmcmc2V4PTEmcGhvbmU9MTgzNjgxOTI0NTImc2lnbj00MmRhYmU0NGE1MzQzYmU5ZjMzNmY0YTMwZTdiMGI4NiZzZHdfc2ltcGxlPTc= js  注入方式(代码调用 给 window 的对象赋值)

实际上就是通过 代码调用 给 window 的对象赋值

然后再 H5 端 就能直接 window.shellDatum 获取到数据

_webViewController.runJavaScript(

'window.shellDatum={authorization:"${AppCache().loginInfo?.accessToken}",phoneNumber:"${AppCache().userState!.phoneNumber}",gameID:"${AppCache().loginInfo!.gameId}",machineCode:"${await AppCache().machineCode}",hostApi:"https://${AppConfig.hostApi}","signKey":"${AppUtils.signKey()}"}');

在 H5 页面中可以直接拿到,取不到的时候 用本地的 方便开发阶段

get Authorization(): String {

if (window['shellDatum']) {

return window['shellDatum']['authorization']

} else {

return this.authorization;

}

}

get MachineCode(): String {

if (window['shellDatum']) {

return window['shellDatum']['machineCode']

} else {

return this.machineCode;

}

}

get GameID(): String {

if (window['shellDatum']) {

return window['shellDatum']['gameID']

} else {

return this.gameID;

}

}

get PhoneNumber() {

if (window['shellDatum']) {

return window['shellDatum']['phoneNumber']

} else {

return this.phoneNumber;

}

}

显然 js 注入的方式 显然安全性更高,和 App 应用绑定,即使H5页面地址被拿到了,也不能单独使用;

 H5页面 和 App通信 分装

最简单的集成,就是通过H5发过来的 字段,区别做相应操作,这种方式呢,拓展性不够好,共同性也不强,更不能实现异步转同步操作, 后来我这边分装了下,因为我这边主要是 API 的请求,(异步转同步,能在H5 中调用原生接口,并且转同步操作 在H5指继续执行后面逻辑)部分核心API防止脚本调用 使用了 阿里云的 bot.

/**

* 封装兼容 开发状态 H5 访问,生产 http Bot 访问

* @param url

* @param data

* @param showLoading

* @param showError

* @param sign

* @param auth

* @returns

*/

public requestPostAdapter(url: string, data = {}, showLoading = true, showError = true, sign = false, auth = true) {

if (DEV) {

//开发时候的逻辑 直接走 H5 的 http 请求

return new Promise<HttpResponse>((resolve) => {

this.onRequest('post', url, data, showLoading, showError, "", sign).then(<HttpResponse>(res) => {

resolve(res);

}).catch((error) => {

resolve(error);

})

});

} else {

//测试 生产的逻辑 走 App 的 http

return new Promise<HttpResponse>((resolve) => {

let entity: UniversalEntity = new UniversalEntity();

entity.auth = auth;

entity.channelId = url;

entity.path = url;

entity.data = data;

entity.function = new FunctionApply((res: HttpResponse) => {

resolve(res);

}, this);

BridgeManager.getInstance<BridgeManager>().toNativeUniversal(entity);

});

}

}

  这里是 分装的辅助类

/**

* 通用接口调用

*/

export class UniversalEntity {

//唯一通道ID 用于区别与其他

public channelId: string;

//请求的地址

public path: string;

//请求的数据

public data: any;

//是否 启用 bot

public auth: boolean;

//回调

public function: FunctionApply;

}

/**

* App 和 h5 通信桥接

*/

export class BridgeManager extends BaseSingleton {

//通过 APP 请求的实体

private universalArray: Array<UniversalEntity> = [];

constructor() {

super();

///通用接口返回

window['universal'] = (res) => {

// Log.trace('universal', res);

this.onUniversal(res);

};

}

/**

* 通用接口调用

* @param res

*/

public onUniversal(res) {

const index = this.universalArray.findIndex(e => e.channelId = res['channelId']);

if (index > -1) {

const entity: UniversalEntity = this.universalArray.splice(index, 1)[0];

entity.function.apply(res['data']);

}

}

/**

* 通用接口调用

*/

public toNativeUniversal(entity: UniversalEntity): void {

this.universalArray.push(entity);

if (window['X5Web']) {

window['X5Web'].postMessage(JSON.stringify({

action: 'universal',

path: entity.path,

data: entity.data,

auth: entity.auth,

channelId: entity.channelId

}));

}

}

}

/**

* 函数回调

*/

export class FunctionApply {

public callback: Function = null;

public target: any = null;

constructor(callback: Function,target: any){

this.callback = callback;

this.target = target;

}

public apply(...data: any){

this.callback.apply(this.target,data);

}

}

 H5中的API的调用,这样H5在开发模式下 也能正常访问接口,发布之后就自动走APP的 http,比对之前的写法 基本没有区别

async onConformBuy(data: any) {

//旧的的写法 只能走H5 的 http

const res = await HttpHelper.getInstance<HttpHelper>().requestPost('FilmTv/BuyingEquipmentList', {

EquipsOrderID: data['orderID'],

})

if (res.success) {

this.onRequest();

HttpHelper.getInstance<HttpHelper>().refreshUserInfo();

UIOverlay.getInstance<UIOverlay>().showToast(res.content);

}

//新的写法 兼容 开发模式 H5 的 http 生成测试走 APP 的 http

const res = await HttpHelper.getInstance<HttpHelper>().requestPostAdapter('FilmTv/BuyingEquipmentList', {

EquipsOrderID: data['orderID'],

})

if (res.success) {

this.onRequest();

HttpHelper.getInstance<HttpHelper>().refreshUserInfo();

UIOverlay.getInstance<UIOverlay>().showToast(res.content);

}

}

    

 APP 端的代码

void _onMessage(String json) {

Map<String, dynamic> map = jsonDecode(json);

String action = map['action'];

switch (action) {

//统一处理

case 'universal':

_universal(map);

break;

}

}

//App 端 http 请求

Future<void> _universal(Map<String, dynamic> map) async {

// debugPrint('_universal:$map');

// 防止脚本调用 https 请求

ResponseBase res = await AppHttp().postWithAuthSign(

RequestBase(

path: map['path'],

data: map['data'],

),

showLoad: true,

auth: map['auth'] ?? false,

);

_webViewController!.runJavaScript('window.universal(${jsonEncode({

'channelId': map['channelId'],

'data': res,

})})');

}

这样以来 H5 调用原生的方式就很便捷, 如同访问 h5 自己的方法一样。同时还能兼容 开发阶段和 打包发布阶段


声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。