Skip to content

前端系统解决方案 ​

重要通知

一枚优秀的架构师,必须将所有工作都建立在高预期的团队效益输出与人效比基准上,然后再技术体系、开发团队、产品业务、公司战略的轻重利害维度,在不同的时间阶段作出相应的取舍,并根据阶段性目标整合人事与资源实现价值的最大化。

架构思维五大体系 ​

技术赋能业务,技术驱动业务 ​

作为技术团队的架构设计者与负责人,不仅需要能够洞察技术的广度与深度,更需要融合企业的战略方向、战术目标,同时将技术与业务充分协作起来,要能够区分业务、产品与技术的三者关系。在不同的阶段,业务、产品与技术的侧重是不同的。在企业的发展历程中,大概都会经历业务驱动阶段、技术驱动阶段、产品驱动阶段。在每一个阶段,企业的人事与资源倾向,都会有所不同。因此,无论是技术、产品或业务,必须清楚自己所负责的团队与事务所属于企业的哪个阶段,从而根据企业战略方向与战术目标来有效利用人事与资源实现团队价值的最大化。

团队高效协作,个体独立开发 ​

打造一个团队能够实现高效率与高质量协作的系统,同时保障每一个成员能够充分发挥个体的优势,这样才能促使团队打大仗、打胜仗、持续胜。而这样的一个系统架构,无疑需要系统架构设计在团队协作与个体开发之间做到各尽其美,却又能彼此兼容。

整体健壮稳定,局部模块容错 ​

一个满足24小时保持健壮与稳定的系统,不仅需要从整体结构去架构设计,更需要有效处理细节上的异常与错误,对于一个庞大的业务系统,每一条业务线都会随时发生线上奔溃的问题。

快速升级迭代,横纵便捷扩展 ​

在日益竞争激烈的今天,对于产品业务的快速迭代已经是常态,但是又不可能对一个系统反复去架构设计,因此如何去保障系统的扩展性与伸缩性就显得尤其重要。

增强用户体验,增强商业盈利 ​

一个系统、一个产品、一个业务,想要取得快速发展,其本质上决定于用户体验与商业价值。因此,作为一枚杰出的架构设计者,用户体验与商业价值是驱动系统不断发展的因素,离开了用户体验与商业价值,系统、产品与业务就会缺失底层的推进逻辑,夭折是迟早的事情。

设计模式与六大原则 ​

设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案

前端控制器模式/拦截过滤器模式 ​

前端控制器模式(Front Controller Pattern)是用来提供一个集中的请求处理机制,所有的请求都将由一个单一的处理程序处理。

  • 应用场景:认证/授权/记录日志、跟踪请求、请求拦截等。

观察者模式 ​

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。当一个对象被修改时,则会自动通知依赖它的对象。

代理模式 ​

为其他对象提供一种代理以控制对这个对象的访问。

装饰器模式 ​

允许向一个现有的对象添加新的功能,同时又不改变其结构。

设计模式之间的关系 ​

设计模式的六大原则 ​

  • 开闭原则(Open Close Principle):即对扩展开放,对修改关闭。
  • 里氏代换原则(Liskov Substitution Principle):任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。
  • 依赖倒转原则(Dependence Inversion Principle):针对接口编程,依赖于抽象而不依赖于具体。
  • 接口隔离原则(Interface Segregation Principle):降低类之间的耦合度。
  • 迪米特法则,又称最少知道原则(Demeter Principle):一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。
  • 合成复用原则(Composite Reuse Principle):尽量使用合成/聚合的方式,而不是使用继承。

前端微服务 ​

微前端是一种将复杂产品业务拆分成独立应用但统筹业务体系,以此实现多个团队协作而保持互不干扰的构建部署方法策略。

微前端基础功能 ​

  • JS沙箱
  • 样式隔离
  • 元素隔离
  • 数据通信
  • 插件系统
  • 预加载

micro-app案例 ​

micro-app是由京东零售iPaaS前端研发团队推出的一款微前端框架,它从组件化的思维,基于类WebComponent进行微前端的渲染,它旨在降低上手难度、提升工作效率,并且无关技术栈,也不和业务绑定,可以用于任何前端框架。同时提供了js沙箱、样式隔离、元素隔离、预加载、数据通信、静态资源补全等一系列完善的功能。

> npm i @micro-zoe/micro-app --save
  • React应用的代码示例

基座应用:index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import RouterConfig from './router/index.js';
import store from './store/index.js';
import microApp from '@micro-zoe/micro-app';

microApp.start();

const element = (
  <React.StrictMode>
    <Provider store={store}>
      <RouterConfig />
    </Provider>
  </React.StrictMode>
);

ReactDOM.createRoot(
  document.getElementById('root'),
).render(element);

基座应用:router.js

import { BrowserRouter, Switch, Route } from 'react-router-dom';
import PageHome from '../views/Home/index.js';

class RouterConfig extends React.Component {
  render() {
    return (
      <BrowserRouter>
        <Routes>
          <Route path='/' element={ <PageHome /> } />
        </Routes>
      </BrowserRouter>
    )
  }
}
export default RouterConfig;

基座应用:views/Home/index.js

import React from 'react';

export default function Home() {
  return (
    <micro-app name='app' url='http://localhost:3000/' baseroute='/'></micro-app>
  )
}

子应用:router.js

import { BrowserRouter, Switch, Route } from 'react-router-dom'

class RouterConfig extends React.Component {
  render() {
    return (
      // 设置基础路由,子应用可以通过window.__MICRO_APP_BASE_ROUTE__获取基座下发的baseroute,如果没有设置baseroute属性,则此值默认为空字符串
      <BrowserRouter basename={window.__MICRO_APP_BASE_ROUTE__ || '/'}></BrowserRouter>
    )
  }
}

single-spa ​

single-spa是通过监听 url change 事件,在路由变化时匹配到渲染的子应用并进行渲染。

qiankun ​

qiankun是一个快速、简单和完整的微前端解决方案,它基于single-spa进行封装。

> pnpm i qiankun -S

CDN部署方案 ​

内容分发网络(Content Delivery Network,CDN)通过将站点内容发布至遍布全球的海量加速节点,使其用户可就近获取所需内容,避免因网络拥堵、跨运营商、跨地域、跨境等因素带来的网络不稳定、访问延迟高等问题,有效提升下载速度、降低响应时间,提供流畅的用户体验。

实现原理 ​

前端业务异常错误监控系统 ​

一步一步搭建前端监控系统:如何监控资源加载错误?
https://blog.fundebug.com/2019/08/17/how-to-monitor-resource-error/

监控实现机制 ​

  • 不可以监听的静态资源类型
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">  
<iframe src="" frameborder="0">
<link rel="alternate" href="rss.xml" type="application/rss+xml" title="RSS">  
<link rel="dns-prefetch" href="/hostname_to_resolve.com">   
<link rel="subresource" href="/javascript/myapp.js">   
<link rel="apple-touch-icon" href="favicon.png">  
<link rel="prerender" href="/example.org/next_page.html">
  • 可以监听的静态资源类型
<link rel="stylesheet" href="">  
<link rel="import" href="component.html">  
<link rel="prefetch" href="name.js">  
<script src=""></>  
<img src="" alt="">
  • window.addEventListener('error', function(e) {});
window.addEventListener('error', function(e) {  
  if (e.target.localName == 'img') {  
    console.log(e.target.currentSrc);  
  }  
  if (e.target.localName == 'link') {  
    console.log(e.target.href);  
    console.log(e.target.rel);  
  }  
  if (e.target.localName == 'script') {  
    console.log(e.target.src);  
  }  
}, true);
  • window.onerror兼容性
window.onerror = function(message, source, lineno, colno, error) {}
  • 跨域脚本错误信息
    • 跨域脚本的服务器必须通过 Access-Control-Allow-Origin 头信息允许当前域名可以获取错误信息
    • 网页里的 script 标签也必须指明 src 属性指定的地址是支持跨域的地址,也就是 crossorigin 属性。
  • sourceMap映射源码

错误类型 ​

js编译时异常(开发阶段就能排)
js运行时异常(包括语法错误):JS错误类型、 JS错误信息、JS错误堆栈、JS错误发生的位置以及相关位置的代码;JS错误发生的几率、浏览器的类型,版本号,设备机型等等辅助信息
加载静态资源异常(路径写错、资源服务器异常、CDN异常、跨域)
接口请求异常等

统计日志 ​

错误率(单位时间错误率)(页面错误率)

throw ​

throw new Error("除数不能为0");

try {} catch(e) {} finally {} ​

功能:无论是否异常,finally中语句都执行 try { console.log(a); } catch(e) { console.log(e.message); } finally { console.log('ok'); }

window.onerror = function(message, source, lineno, colno, error) {} ​

message:错误信息(字符串)。可用于HTML onerror=""处理程序中的event。 source:发生错误的脚本URL(字符串) lineno:发生错误的行号(数字) colno:发生错误的列号(数字) error:Error对象(对象)

[Object].onerror = function () {} ​

window.performance.getEntries() ​

var allImgs = document.getElementsByTagName('image') var loadedImgs = performance.getEntries().filter(i => i.initiatorType === 'img')

window.onunhandledrejection ​

window.addEventListener('error', function(event) { }) ​

ErrorEvent 类型的event包含有关事件和错误的所有信息

window.addEventListener ​

window.addEventListener = function() { //
}

################################################## ​

异常上报日志信息 ​

################################################## ​

<script>
  // 设置日志对象类的通用属性
  function setCommonProperty() {
    this.happenTime = new Date().getTime(); // 日志发生时间
    this.webMonitorId = WEB_MONITOR_ID;     // 用于区分应用的唯一标识(一个项目对应一个)
    this.simpleUrl =  window.location.href.split('?')[0].replace('#', ''); // 页面的url
    this.customerKey = utils.getCustomerKey(); // 用于区分用户,所对应唯一的标识,清理本地数据后失效
    this.pageKey = utils.getPageKey();  // 用于区分页面,所对应唯一的标识,每个新页面对应一个值
    this.deviceName = DEVICE_INFO.deviceName;
    this.os = DEVICE_INFO.os + (DEVICE_INFO.osVersion ? " " + DEVICE_INFO.osVersion : "");
    this.browserName = DEVICE_INFO.browserName;
    this.browserVersion = DEVICE_INFO.browserVersion;
    // TODO 位置信息, 待处理
    this.monitorIp = "";  // 用户的IP地址
    this.country = "china";  // 用户所在国家
    this.province = "";  // 用户所在省份
    this.city = "";  // 用户所在城市
    // 用户自定义信息, 由开发者主动传入, 便于对线上进行准确定位
    this.userId = USER_INFO.userId;
    this.firstUserParam = USER_INFO.firstUserParam;
    this.secondUserParam = USER_INFO.secondUserParam;
  }

  //设备信息
  function machin() {
    //
  }

  // JS错误日志,继承于日志基类MonitorBaseInfo
  function JavaScriptErrorInfo(uploadType, errorMsg, errorStack) {
    setCommonProperty.apply(this);
    this.uploadType = uploadType;
    this.errorMessage = encodeURIComponent(errorMsg);
    this.errorStack = errorStack;
    this.browserInfo = BROWSER_INFO;
  }
  JavaScriptErrorInfo.prototype = new MonitorBaseInfo();
</script>

JavaScript外链跨域与运行时错误 ​

核心问题 ​

跨域脚本 外链脚本

当引入跨域的脚本(比如用了 apis.google.com 上的库文件)时,如果这个脚本有错误,因为浏览器的限制(根本原因是协议的规定),是拿不到错误信息的。当本地尝试使用 window.onerror 去记录脚本的错误时,跨域脚本的错误只会返回 Script error。 而 HTML5 新的规定,是可以允许本地获取到跨域脚本的错误信息的,但有两个条件:一是,二是

<script src="url" integrity="sha384-" crossorigin="anonymous"></script>

try {} catch(e) {} ​

try { console.log(sex);
} catch(e) { console.log(e); console.log(e.message); console.log(e.stack); }

window.onerror = function(message, source, lineno, colno, error) {} ​

message:错误信息(字符串)。可用于HTML onerror=""处理程序中的event。 source:发生错误的脚本URL(字符串) lineno:发生错误的行号(数字) colno:发生错误的列号(数字) error:Error对象(对象)

window.onerror = function(message, source, lineno, colno, error) { console.log(arguments[0]); console.log(arguments[1]); console.log(arguments[2]); console.log(arguments[3]); console.log(arguments[4]); }

window.addEventListener('error', function(e) {}); ​

window.addEventListener('error', function(e) { console.log(e); console.log(e.target.localName); });

页面JS错误监控 ​

function recordJavaScriptError() { // 重写console.error, 可以捕获更全面的报错信息 var oldError = console.error; console.error = function () { // arguments的长度为2时,才是error上报的时机 // if (arguments.length < 2) return; var errorMsg = arguments[0] && arguments[0].message; var url = WEB_LOCATION; var lineNumber = 0; var columnNumber = 0; var errorObj = arguments[0] && arguments[0].stack; if (!errorObj) errorObj = arguments[0]; // 如果onerror重写成功,就无需在这里进行上报了 !jsMonitorStarted && siftAndMakeUpMessage(errorMsg, url, lineNumber, columnNumber, errorObj); return oldError.apply(console, arguments); }; // 重写 onerror 进行jsError的监听 window.onerror = function(errorMsg, url, lineNumber, columnNumber, errorObj) { jsMonitorStarted = true; var errorStack = errorObj ? errorObj.stack : null; siftAndMakeUpMessage(errorMsg, url, lineNumber, columnNumber, errorStack); };

function siftAndMakeUpMessage(origin_errorMsg, origin_url, origin_lineNumber, origin_columnNumber, origin_errorObj) {
  var errorMsg = origin_errorMsg ? origin_errorMsg : '';
  var errorObj = origin_errorObj ? origin_errorObj : '';
  var errorType = "";
  if (errorMsg) {
    var errorStackStr = JSON.stringify(errorObj)
    errorType = errorStackStr.split(": ")[0].replace('"', "");
  }
  var javaScriptErrorInfo = new JavaScriptErrorInfo(JS_ERROR, errorType + ": " + errorMsg, errorObj);
  javaScriptErrorInfo.handleLogInfo(JS_ERROR, javaScriptErrorInfo);
};

};

Sentry ​

Sentry 是一项跨平台的应用程序监控服务,专注于错误报告,可帮助您实时监控和修复崩溃。服务器使用 Python,但它包含一个完整的 API,用于在任何应用程序中从任何语言发送事件。

  • 官网:https://sentry.io/

  • GitHub:https://github.com/getsentry/sentry

  • 学 sentry 源码 可以学到如何监控错误(window.onerror、window.onunhandledrejection),劫持原生函数上报,如何上报,支持fetch则用fetch,不支持则用ajax(xhr)等。

pnpm install --save @sentry/browser

import { init, captureMessage } from '@sentry/browser';

init({
  dsn: '__DSN__',
  // ...
});
captureMessage('Hello, world!');

BadJS ​

定位:开源前端脚本错误监控及跟踪解决项目
作者:此项目为鹅厂 imweb(qq群:179045421) 团队的开源项目
支持单机,集群,docker。存储支持mongodb等
GitHub:https://github.com/BetterJS/doc

录屏技术 ​

RRWeb ​

是一种用于记录和重放用户在 Web 上的交互的工具。

  • 录制和重放 Web : https://www.rrweb.io/

  • https://github.com/rrweb-io

  • RRWeb 主要由 3 个部分组成 rrweb-snapshot 中,包括快照和重建功能。快照用于将 DOM 及其状态转换为具有唯一标识符的可序列化数据结构;重建功能是将快照重建为对应的 DOM。 rrweb,包括 record 和 replay 两个功能。record 函数用于记录 DOM 中的所有 mutation;重放是将记录的 Mutation 根据对应的 timestamp 逐一重放。 rrweb-player 是 rrweb 的播放器 UI,提供基于 GUI 的功能,如暂停、快进、拖拽等,随时播放。

Fundebug录制技术 ​

Fundebug ​

Fundebug是专业的应用 BUG 监控平台。当线上应用出现 BUG 时,Fundebug 会通过邮件或者第三方工具立即给开发者发送报警,这样能够帮助开发者及时发现并且修复应用 BUG,从而提升用户体验。

Fundebug项目团队负责人昝涛于2016年9月获得日本情报所颁发的博士学位,在厦门海外留学人员对接会上与芝麻开门创客汇工作人员相识。

一行代码搞定; 自动捕获未处理的错误; 能够捕获3种不同的前端错误:JavaScript执行错误,资源加载错误和HTTP请求错误。 出错场景完全可视化重现,相当于"录屏"; 支持通过Source Map还原出错源代码 记录出错前的鼠标点击、HTTP请求、页面跳转、console打印等用户行为,帮助您复现BUG 支持收集try/catch捕获的错误; 兼容所有浏览器包括IE 6到 IE 11; 兼容所有前端开发框架,例如Vue.js,React,AngularJS,Angular 2,Angular 4,Ionic 1,Ionic 2,Cordova,GitBook等;

pnpm install fundebug-javascript --save
pnpm install fundebug-revideo --save


# 测试插件
fundebug.test();
fundebug.notify("Test", "ERP系统异常错误监控!");

API接口异常监控上报 ​

AJAX请求

function recordHttpLog() {
  // 监听ajax的状态
  function ajaxEventTrigger(event) {
    var ajaxEvent = new CustomEvent(event, {
        detail: this
    });
    window.dispatchEvent(ajaxEvent);
  }
  var oldXHR = window.XMLHttpRequest;
  function newXHR() {
    var realXHR = new oldXHR();
    realXHR.addEventListener(
        "loadstart",
        function() {
            ajaxEventTrigger.call(this, "ajaxLoadStart");
        },
        false
    );
    realXHR.addEventListener(
        "loadend",
        function() {
            ajaxEventTrigger.call(this, "ajaxLoadEnd");
        },
        false
    );
    // 此处的捕获的异常会连日志接口也一起捕获,如果日志上报接口异常了,就会导致死循环了。
    // realXHR.onerror = function () {
    //   siftAndMakeUpMessage("Uncaught FetchError: Failed to ajax", WEB_LOCATION, 0, 0, {});
    // }
    return realXHR;
  }
  var timeRecordArray = [];
  window.XMLHttpRequest = newXHR;
  window.addEventListener("ajaxLoadStart", function(e) {
      var tempObj = {
          timeStamp: new Date().getTime(),
          event: e
      };
      timeRecordArray.push(tempObj);
  });
  window.addEventListener("ajaxLoadEnd", function() {
    for (var i = 0; i < timeRecordArray.length; i++) {
      if (timeRecordArray[i].event.detail.status > 0) {
        var currentTime = new Date().getTime();
        var url = timeRecordArray[i].event.detail.responseURL;
        var status = timeRecordArray[i].event.detail.status;
        var statusText = timeRecordArray[i].event.detail.statusText;
        var loadTime = currentTime - timeRecordArray[i].timeStamp;
        if (!url || url.indexOf(HTTP_UPLOAD_LOG_API) != -1) return;
        var httpLogInfoStart = new HttpLogInfo(
            HTTP_LOG,
            url,
            status,
            statusText,
            "发起请求",
            timeRecordArray[i].timeStamp,
            0
        );
        httpLogInfoStart.handleLogInfo(HTTP_LOG, httpLogInfoStart);
        var httpLogInfoEnd = new HttpLogInfo(
            HTTP_LOG,
            url,
            status,
            statusText,
            "请求返回",
            currentTime,
            loadTime
        );
        httpLogInfoEnd.handleLogInfo(HTTP_LOG, httpLogInfoEnd);
        // 当前请求成功后就在数组中移除掉
        timeRecordArray.splice(i, 1);
      }
    }
  });
}

fetch请求

return new Promise(function(resolve, reject) {
    var request = new Request(input, init);
    var xhr = new XMLHttpRequest();

    xhr.onload = function() {
        var options = {
            status: xhr.status,
            statusText: xhr.statusText,
            headers: parseHeaders(xhr.getAllResponseHeaders() || "")
        };
        options.url =
            "responseURL" in xhr
                ? xhr.responseURL
                : options.headers.get("X-Request-URL");
        var body = "response" in xhr ? xhr.response : xhr.responseText;
        resolve(new Response(body, options));
    };
    // .......
    xhr.send(
        typeof request._bodyInit === "undefined" ? null : request._bodyInit
    );
});

设置日志对象类的通用属性

function setCommonProperty() {
  this.happenTime = new Date().getTime(); // 日志发生时间
  this.webMonitorId = WEB_MONITOR_ID; // 用于区分应用的唯一标识(一个项目对应一个)
  this.simpleUrl = window.location.href.split("?")[0].replace("#", ""); // 页面的url
  this.completeUrl = utils.b64EncodeUnicode(
      encodeURIComponent(window.location.href)
  ); // 页面的完整url
  this.customerKey = utils.getCustomerKey(); // 用于区分用户,所对应唯一的标识,清理本地数据后失效,
  // 用户自定义信息, 由开发者主动传入, 便于对线上问题进行准确定位
  var wmUserInfo = localStorage.wmUserInfo
      ? JSON.parse(localStorage.wmUserInfo)
      : "";
  this.userId = utils.b64EncodeUnicode(wmUserInfo.userId || "");
  this.firstUserParam = utils.b64EncodeUnicode(
      wmUserInfo.firstUserParam || ""
  );
  this.secondUserParam = utils.b64EncodeUnicode(
      wmUserInfo.secondUserParam || ""
  );
}

接口请求日志,继承于日志基类MonitorBaseInfo

function HttpLogInfo(uploadType, url, status, statusText, statusResult, currentTime, loadTime) {
  setCommonProperty.apply(this);
  this.uploadType = uploadType; // 上传类型
  this.httpUrl = utils.b64EncodeUnicode(encodeURIComponent(url)); // 请求地址
  this.status = status; // 接口状态
  this.statusText = statusText; // 状态描述
  this.statusResult = statusResult; // 区分发起和返回状态
  this.happenTime = currentTime; // 客户端发送时间
  this.loadTime = loadTime; // 接口请求耗时
}

错误上报实现机制 ​

上报错误的基本原理 ​

1.采用Ajax通信的方式上报

2.利用Image对象上报 (主流方式)

Image上报错误方式: (new Image()).src = 'https://lxchuan12.cn/error?name=若川'

html
<script>
	function uploadData(data, api = '') {
		const image  = new Image();
		let argspath = '';
		if (typeof data !== 'string') {
			for (let [key, value] of Object.entries(data = {})) {
				argspath += `${key}=${value}`;
			}
		} else {
			argspath = data;
		}
		image.src = api + '?' + argspath;
	}
</script>

录屏上报 ​

截屏上报 ​

JSCapture

截图上报 ​

// js处理截图 this.screenShot = function(cntElem, callback) { var shareContent = cntElem; //需要截图的包裹的(原生的)DOM 对象 var width = shareContent.offsetWidth; //获取dom 宽度 var height = shareContent.offsetHeight; //获取dom 高度 var canvas = document.createElement("canvas"); //创建一个canvas节点 var scale = 0.6; //定义任意放大倍数 支持小数 canvas.style.display = "none"; canvas.width = width * scale; //定义canvas 宽度 * 缩放 canvas.height = height * scale; //定义canvas高度 *缩放 canvas.getContext("2d").scale(scale, scale); //获取context,设置scale var opts = { scale: scale, // 添加的scale 参数 canvas: canvas, //自定义 canvas logging: false, //日志开关,便于查看html2canvas的内部执行流程 width: width, //dom 原始宽度 height: height, useCORS: true // 【重要】开启跨域配置 }; html2canvas(cntElem, opts).then(function(canvas) { var dataURL = canvas.toDataURL(); var tempCompress = dataURL.replace("data:image/png;base64,", ""); var compressedDataURL = Base64String.compress(tempCompress); callback(compressedDataURL); }); };

// 加载js文件的小工具 this.loadJs = function(url, callback) { var script = document.createElement("script"); script.async = 1; script.src = url; script.onload = callback; var dom = document.getElementsByTagName("script")[0]; dom.parentNode.insertBefore(script, dom); return dom; }; // html2Canvas 库文件加载完成后,通知全局变量,lz-string 同理 utils.loadJs("//html2canvas.hertzen.com/dist/html2canvas.min.js", function() { html2CanvasLoaded = true; });

前端安全防御措施 ​

内容安全策略 (CSP) ​

内容安全策略 (CSP) 是一个额外的安全层,用于检测并削弱某些特定类型的攻击,包括跨站脚本 (XSS (en-US)) 和数据注入攻击等。

CSP, Content-Security-Policy内容安全策略,白名单制度 ​

(Content Security Policy)内容安全策略:用于指定哪些内容可执行 res.setHeader("Content-Security-Policy", "frame-ancestors'self'"); 方法一:一种是通过 HTTP 头信息的Content-Security-Policy的字段: 方法二:通过网页的<meta>标签

<meta http-equiv="Content-Security-Policy" content="script-src 'self'; object-src 'none'; style-src cdn.ysun.com cdn.wang.com">

以下选项限制各类资源的加载。

script-src:外部脚本 style-src:样式表 img-src:图像 media-src:媒体文件(音频和视频) font-src:字体文件 object-src:插件(比如 Flash) child-src:框架 frame-ancestors:嵌入的外部资源(比如<frame>、<iframe>、<embed>和<applet>) connect-src:HTTP 连接(通过 XHR、WebSockets、EventSource等) worker-src:worker脚本 manifest-src:manifest 文件

Nginx配置 ​

add_header Content-Security-Policy "upgrade-insecure-requests;connect-src *"; Content-Security-Policy: default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com

同时meta中也支持设置Content-Security-Policy ​

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';">

Content-Security-Policy内容安全策略 ​

内容安全策略(CSP)需要仔细调整和精确定义策略。如果启用,CSP会对浏览器呈现页面的方式产生重大影响(例如,默认情况下禁用内联JavaScript,并且必须在策略中明确允许)。CSP可防止各种攻击,包括跨站点脚本和其他跨站点注入。

----------------------------------------------------------------------------------
  Values Directive	                   Description
----------------------------------------------------------------------------------
	base-uri	                   Define the base uri for relative uri.
	default-src	                 Define loading policy for all resources type in case of a resource type dedicated directive is not defined (fallback).
	script-src	                 Define which scripts the protected resource can execute.
	object-src	                 Define from where the protected resource can load plugins.
	style-src	                   Define which styles (CSS) the user applies to the protected resource.
	img-src	                     Define from where the protected resource can load images.
	media-src	                   Define from where the protected resource can load video and audio.
	frame-src	                   Deprecated and replaced by child-src. Define from where the protected resource can embed frames.
	child-src	                   Define from where the protected resource can embed frames.
	frame-ancestors	             Define from where the protected resource can be embedded in frames.
	font-src	                   Define from where the protected resource can load fonts.
	connect-src	                 Define which URIs the protected resource can load using script interfaces.
	manifest-src	               Define from where the protected resource can load manifest.
	form-action	                 Define which URIs can be used as the action of HTML form elements.
	sandbox	                     Specifies an HTML sandbox policy that the user agent applies to the protected resource.
	script-nonce	               Define script execution by requiring the presence of the specified nonce on script elements.
	plugin-types	               Define the set of plugins that can be invoked by the protected resource by limiting the types of resources that can be embedded.
	reflected-xss	               Instructs a user agent to activate or deactivate any heuristics used to filter or block reflected cross-site scripting attacks, equivalent to the effects of the non-standard X-XSS-Protection header.
	block-all-mixed-content	     Prevent user agent from loading mixed content.
	upgrade-insecure-requests	   Instructs user agent to download insecure resources using HTTPS.
	referrer	                   Define information user agent must send in Referer header.
	report-uri	                 Specifies a URI to which the user agent sends reports about policy violation.
	report-to	                   Specifies a group (defined in Report-To header) to which the user agent sends reports about policy violation.
----------------------------------------------------------------------------------

CSRF跨站请求伪造 ​

CSRF(Cross Site Request Forgy),即跨站请求伪造,通过不同渠道路径来获取有效的身份权限信息,在不同的站点实现伪装真实身份的相关权限操作行为。

  • 解决方案
    • 禁止网页被iframe嵌套,使用top.location.hostname !== window.location.hostname,或者后端在.htaccess文件中设置X-Frame-Options的值为SAMEORIGIN。同时配置referer校验请求来源。
    • 强化身份权限校验信息的时空人机唯一性与时效性,即基于用户IP、top.location.hostname、域名、账户与密码、随机性唯一长字符串组合的Token校验机制。
    • 手机号与验证码的校验机制。

核心:利用用户身份伪造请求 描述:利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,冒充用户对被攻击的网站发送执行某项操作的请求

身份信息被盗的渠道与途径? ​

如何保持身份信息来源的唯一性? ​

实现原理 ​

用户在a站前端页面发起登录(身份认证)请求 a站后端确认身份,登录成功,cookie中存在用户的身份认证信息 b站前端页面向a站后端发起请求,带着a站的cookie信息(身份认证信息),请求成功

攻击案例 ​

爬虫,就是典型的CSRF攻击

点击劫持 ​

原理:第三方网站通过iframe内嵌某一个网站,并且将iframe设置为透明不可见,将其覆盖在其他经过伪装的DOM上,伪装的可点击DOM(按钮等)与实际内嵌网站的可点击DOM位置相同,当用户点击伪装的DOM时,实际上点击的是iframe中内嵌的网页的DOM从而触发请求操作 特点:用户自己做了点击操作;用户毫不知情;

最权威的防御措施 ​

基于IP + 用户账户生成的token编码

CSRF攻击防御 ​

设置Request Headers中的Referer字段与Origin字段,爬虫可以伪造 cookies基于IP生成信息,实现较为复杂 配置Same-Site Cookies Token检验机制:Token根据IP、设备号生成唯一性

方案一 ​

前端检测 top 窗口是否就是 self

try {
  // 非Chrome浏览器回报错,存在跨域,所以在catch语句块内处理
  top.location.hostname;

  // 兼容Chrome不会报错
 if (top.location.hostname != window.location.hostname) {
  top.location.href =window.location.href;
 }
} catch(e) {
 top.location.href = window.location.href;
}

方案二 在后端项目的 .htaccess 文件中使用 X-Frame-Options 的 SAMEORIGIN 参数

<ifModule mod_headers.c>
  Header always append X-Frame-Options SAMEORIGIN
</ifModule>

备注 X-Frame-Options 有三个选项: DENY:表示该页面不允许在 frame 中展示,即便是在相同域名的页面中嵌套也不允许。 SAMEORIGIN:表示该页面可以在相同域名页面的 frame 中展示。 ALLOW-FROM uri:表示该页面可以在指定来源的 frame 中展示。

Cookies ​

Set-Cookie: =[; =]
[; expires=][; domain=]
[; path=][; secure][; HttpOnly]

为 Set-Cookie 响应头新增 SameSite 属性,它用来标明这个 cookie 是个“同站 cookie”,同站 cookie 只能作为第一方 cookie,不能作为第三方 cookie。SameSite 有两个属性值,分别是 Strict 和 Lax, Set-Cookie: foo=1; SameSite=Strict Set-Cookie: bar=2

方法一:禁止第三方网站携带本网站的cookie信息 ​

设置same-site属性,same-site属性有两个值,Strict(所有的第三方请求都不能携带本网站的cookie)和Lax(链接可以,但是form表单提交和ajax请求不行)

方法二 ​

本网站前端页面添加验证信息:使用验证码或者添加token验证

  验证码:当发起请求时,前端需要输入本网站页面的验证码信息,后端对验证码进行验证,验证码正确才会进行相关操作(存取数据等)

  token验证:a站前端将token存在当前页面中(比如表单中的input隐藏域,meta标签或者任何一个dom的属性)和cookie中,当请求a站后端的时候,参数中带上这个token字段,a站后端将参数中的token和cookie中的token做对比, 相同则验证通过,不同则请求不合法

不管是验证码还是token验证,原理都是一样的,在a站前端页面加入验证,当第三方网站请求a站后端时,即使能携带a站cookie,但是因为没有经过a站的前端页面从而拿不到验证信息,也会导致请求失败。

两种防御的方法也有区别,验证码需要用户去填写,从而增加了用户使用网站的复杂度,而token验证在用户无感知的情况下就可以实现,不影响用户体验。我个人理解,验证码验证一般使用在需要提高用户认知的场景,比如,登录多次失败,修改个人信息(用户名,密码,绑定手机号等等),而一些获取商品列表信息,搜索等接口,使用token比较合理。可以看看我们平时使用的这些网站,作参考~

(3)referer验证:禁止来自第三方的请求

(4)使用post请求:有一个说法是“post请求比get请求更安全”,那这种说法对不对呢?实际上这种说法并不准确,对于CSRF攻击来讲,不管是post还是get都能实现攻击,区别只是post请求攻击方需要构造一个form表单才可以发起请求,比get请求(img的src, a标签的href等等)的攻击方式复杂了一些,但是并不能有效的阻止攻击。

XSS跨站脚本攻击 ​

XSS(Cross Site Script),即跨站脚本攻击,通过网页交互的漏洞输入可执行破坏脚本提交后,在服务器或客户端产生恶意可执行程序的攻击,利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。

  • 反射型(url参数直接注入)
  • 存储型(存储到DB后读取时注入)
  • 解决方案
    • 防范文件上传漏洞
    • 对用户输入内容进行编码
    • 对特定字符做转义

核心:恶意脚本注入 描述:攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。

XSS(Cross Site Script)跨站脚本攻击 ​

原理:通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。 危害:劫持用户会话,插入恶意内容、重定向用户、使用恶意软件劫持用户浏览器、繁殖XSS蠕虫,甚至破坏网站、修改路由器配置信息。 攻击的基本类型:反射型(url参数直接注入)和存储型(存储到DB后读取时注入) 注入点:HTML节点内的内容(text);HTML中DOM元素的属性;Javascript代码;富文本编辑器提交内容

攻击形式 ​

<!--HTML节点内容注入-->
<div><script>alert(1);</script></div>  


<!--DOM属性注入-->
<img src='/images/1.png' onerror='alert(1);'>  


<!--javascript代码-->
<script>
  var a = '1';alert(1);''
</script>

对用户输入内容进行编码 ​

let name = `<html><body data-url="http://test.ysunlight.com"><script></script></body></html>`;
name.replace(/</g, "&lt;")
    .replace(/>/g, "&lt;")
    .replace(/&/g, "&amp;")

XSS攻击防御 ​

对特定字符做转义:内容注入替换尖括号( < => < > => > ) 属性注入替换单引号或双引号( " => " ' => ' )

浏览器自带防御机制,主要应对反射型攻击(HTML内容或属性):http响应头中自动添加x-xss-protection,值为0(关闭),1(打开),默认打开

SQL注入 ​

SQL注入,即通过向数据库注入SQL语句,实现程序读取SQL语句执行恶意破坏的目的。 设定$name 中插入了我们不需要的SQL语句 以上的注入语句中,我们没有对 $name 的变量进行过滤,$name 中插入了我们不需要的SQL语句,将删除 users 表中的所有数据。

$name = "Qadir'; DELETE FROM users;";
mysqli_query($conn, "SELECT * FROM users WHERE name='{$name}'");

劫持攻击 ​

URL劫持 ​

  • HTTP劫持:当我们访问页面的时候,运营商在页面的HTML代码中,插入弹窗、广告等HTML代码,来获取相应的利益。

点击劫持 ​

第三方网站通过iframe内嵌某一个网站,并且将iframe设置为透明不可见,将其覆盖在其他经过伪装的DOM上,伪装的可点击DOM(按钮等)与实际内嵌网站的可点击DOM位置相同,当用户点击伪装的DOM时,实际上点击的是iframe中内嵌的网页的DOM从而触发请求操作。

  • 当前窗口与window窗口是否为同一个
  • 服务器响应http文本设置X-Frame-Options: DENY | SAMEORIGIN | ALLOW-FROM
    • DENY:禁止内嵌
    • SAMEORIGIN:只允许同域名页面内嵌
    • ALLOW-FROM:指定可以内嵌的地址
  • Nginx配置:add_header X-Frame-Options DENY;
<script>
  //方法一
  if (top.location !== window.location) {
    //被嵌套了
  }
  //方法二
  if (window.top !== window.self) {
    //被嵌套了
  }
  if(self.frameElement && self.frameElement.tagName == "IFRAME"){
    //相关处理
  }
</script>

// 缺陷:iframe标签中的属性sandbox属性是可以禁用内嵌网页的脚本
<iframe sandbox='allow-forms' src='...'></iframe>

DDoS攻击 ​

DDoS 攻击全称拒绝服务(Denial of Service),简单的说就是让一个公开网站无法访问,而 DDoS 攻击(分布式拒绝服务 Distributed Denial of Service)是 DoS 的升级版。

致因 ​

攻击者不断地提出服务请求,让合法用户的请求无法及时处理,这就是 DoS 攻击。

代码示例 ​

攻击者使用多台计算机或者计算机集群进行 DoS 攻击,就是 DDoS 攻击。

解决方案 ​

防止 DDoS 攻击的基本思路是限流,限制单个用户的流量(包括 IP 等)。

SYN攻击 ​

同步序列编号(Synchronize Sequence Numbers), 是TCP/IP建立连接时使用的握手信号,服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到SYN洪泛攻击。SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server则回复确认包,并等待Client确认,由于源地址不存在,因此Server需要不断重发直至超时,这些伪造的SYN包将长时间占用半连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。SYN 攻击是一种典型的 DoS/DDoS 攻击。

检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击。在 Linux/Unix 上可以使用系统自带的 netstats 命令来检测 SYN 攻击。 常见的防御 SYN 攻击的方法有如下几种:

  • 防御措施
    • 缩短超时(SYN Timeout)时间
    • 增加最大半连接数
    • 过滤网关防护
    • SYN cookies技术

HTTP劫持 ​

核心:广告、弹框html注入 描述:当我们访问页面的时候,运营商在页面的HTML代码中,插入弹窗、广告等HTML代码,来获取相应的利益

界面操作劫持 ​

核心:视觉欺骗 描述:界面操作劫持是一种基于视觉欺骗的劫持攻击。通过在页面上覆盖一个iframe + opacity:0的页面,让用户误点击

错误的内容推断 ​

核心:js伪装成图片文件 描述:攻击者将含有JavaScript的脚本文件伪装成图片文件(修改后缀等)。该文件逃过了文件类型校验,在服务器里存储了下来。接下来,受害者在访问这段评论的时候,浏览器请求这个伪装成图片的JavaScript脚本并执行

不安全的第三方依赖包 ​

核心:第三方漏洞 描述:框架及第三方依赖的安全漏洞

HTTPS降级HTTP ​

核心:拦截首次http通信 描述:问题的本质在于浏览器发出去第一次请求就被攻击者拦截了下来并做了修改,根本不给浏览器和服务器进行HTTPS通信的机会。大致过程如下,用户在浏览器里输入URL的时候往往不是从https://开始的,而是直接从域名开始输入,随后浏览器向服务器发起HTTP通信,然而由于攻击者的存在,它把服务器端返回的跳转到HTTPS页面的响应拦截了,并且代替客户端和服务器端进行后续的通信

本地存储数据泄露 ​

核心:敏感、机密数据 描述:前端存储敏感、机密信息易被泄露

缺失静态资源完整性校验 ​

核心:CDN资源劫持 描述:存储在CDN中的静态资源,攻击者劫持了CDN,或者对CDN中的资源进行了污染

文件上传漏洞 ​

核心:文件类型限制 描述:文件后缀及文件内容没有严格限制

文件下载漏洞 ​

核心:文件类型、目录限制 描述:下载敏感文件、下载目录

前端跨域解决措施 ​

域与同源策略 ​

  • 域的格式:protocol 😕/ hostname[:port]
  • 同源:如果两个 URL 的 protocol、port 和 host 都相同。
  • 同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

嵌入跨源资源示例 ​

  • <script src="..."></script> 标签嵌入跨域脚本。语法错误信息只能被同源脚本中捕捉到。
  • <link rel="stylesheet" href="..."> 标签嵌入 CSS。由于 CSS 的松散的语法规则,CSS 的跨域需要一个设置正确的 HTTP 头部 Content-Type 。不同浏览器有不同的限制: IE, Firefox, Chrome, Safari (跳至 CVE-2010-0051) 部分 和 Opera。
  • 通过 <img> 展示的图片。支持的图片格式包括 PNG、JPEG、GIF、BMP、SVG等。
  • 通过 <video> 和 <audio> 播放的多媒体资源。
  • 通过 <object>、 <embed> 和 <applet> 嵌入的插件。
  • 通过 @font-face 引入的字体。一些浏览器允许跨域字体(cross-origin fonts),一些需要同源字体(same-origin fonts)。
  • 通过 <iframe> 载入的任何资源。站点可以使用 X-Frame-Options 消息头来阻止这种形式的跨域交互。

跨源资源共享 (CORS) ​

跨源资源共享 (CORS)(或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其它 origin(域,协议和端口),使得浏览器允许这些 origin 访问加载自己的资源。

注意事项

出于安全性,浏览器限制脚本内发起的跨源 HTTP 请求。例如,XMLHttpRequest 和 Fetch API 遵循同源策略。

CORS应用场景 ​

  • 由 XMLHttpRequest 或 Fetch APIs 发起的跨源 HTTP 请求。
  • Web 字体 (CSS 中通过 @font-face 使用跨源字体资源),因此,网站就可以发布 TrueType 字体资源,并只允许已授权网站进行跨站调用。
  • WebGL 贴图
  • 使用 drawImage 将 Images/video 画面绘制到 canvas。
  • 来自图像的 CSS 图形

跨源资源共享机制工作原理 ​

  • 简单请求:某些请求不会触发 CORS 预检请求。
    • 使用下列方法之一
      • GET:请求指定的资源。
      • HEAD:请求资源的头部信息,并且这些头部与 HTTP GET 方法请求时返回的一致。
      • POST:发送数据给服务器。
    • 除了被用户代理自动设置的首部字段(例如 Connection,User-Agent)和在 Fetch 规范中定义为 禁用首部名称 的其他首部,允许人为设置的字段为 Fetch 规范定义的 对 CORS 安全的首部字段集合。
      • Accept
      • Accept-Language
      • Content-Language
      • Content-Type(需要注意额外的限制)
    • Content-Type 的值仅限于下列三者之一
      • text/plain
      • multipart/form-data
      • application/x-www-form-urlencoded
    • 请求中的任意 XMLHttpRequest 对象均没有注册任何事件监听器;XMLHttpRequest 对象可以使用 XMLHttpRequest.upload 属性访问。
    • 请求中没有使用 ReadableStream 对象。
  • 预检请求:要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。
    • 最大有效时间:The Access-Control-Max-Age 这个响应头表示 preflight request (预检请求)的返回结果(即 Access-Control-Allow-Methods 和Access-Control-Allow-Headers 提供的信息) 可以被缓存多久。在有效时间内,浏览器无须为同一请求再次发起预检请求。
      • 在 Firefox 中,上限是 24 小时 (即 86400 秒)。
      • 在 Chromium v76 之前, 上限是 10 分钟(即 600 秒)。
      • 从 Chromium v76 开始,上限是 2 小时(即 7200 秒)。
      • Chromium 同时规定了一个默认值 5 秒。
      • 如果值为 -1,表示禁用缓存,则每次请求前都需要使用 OPTIONS 预检请求。
    • 预检请求与重定向:并不是所有浏览器都支持预检请求的重定向。如果一个预检请求发生了重定向,一部分浏览器将报告错误
    • 附带身份凭证请求
      • 当发出跨源请求时,第三方 cookie 策略仍将适用。无论如何改变本章节中描述的服务器和客户端的设置,该策略都会强制执行。
      • XMLHttpRequest 或 Fetch 与 CORS可以基于 HTTP cookies 和 HTTP 认证信息发送身份凭证,withCredentials = true。
    • 预检请求和凭据:CORS 预检请求不能包含凭据。预检请求的 响应 必须指定 Access-Control-Allow-Credentials: true 来表明可以携带凭据进行实际的请求。
    • 附带身份凭证的请求与通配符
      • 服务器不能将 Access-Control-Allow-Origin 的值设为通配符“*”,而应将其设置为特定的域,如:Access-Control-Allow-Origin: https://example.com。
      • 服务器不能将 Access-Control-Allow-Headers 的值设为通配符“*”,而应将其设置为首部名称的列表,如:Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
      • 服务器不能将 Access-Control-Allow-Methods 的值设为通配符“*”,而应将其设置为特定请求方法名称的列表,如:Access-Control-Allow-Methods: POST, GET
    • 第三方 cookies:Cookie 策略受 SameSite 属性控制。
  • HTTP 响应首部字段
    • Access-Control-Allow-Origin: <origin> | *
    • Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
    • Access-Control-Max-Age: <delta-seconds>
    • Access-Control-Allow-Credentials: true
    • Access-Control-Allow-Methods: <method>[, <method>]*
    • Access-Control-Allow-Headers: <field-name>[, <field-name>]*
  • HTTP 请求首部字段
    • Origin: <origin>
    • Access-Control-Request-Method: <method>
    • Access-Control-Request-Headers: <field-name>[, <field-name>]*

重要通知

"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

通过jsonp跨域 ​

限于get请求

  • 浏览器配置
<script>
  function jsonpCallback(res) {
    //获取回调参数, 执行业务模型
    console.log(res);
  }
</script>
<script src="http://127.0.0.1:7901/jsonp?callback=jsonpCallback"></script>
  • 服务器配置
async function jsonp() {
  router.get("/jsonp", async (ctx, next) => {
    let resdt = {status: 200, msg: "success", data: {}};
    let fn = ctx.request.query.callback;
    console.log(fn);
    ctx.body = fn + "(" + JSON.stringify(resdt) + ")";
    process.stdout.write(`${fn}\n\n`);
  });
}
  • 返回的函数存在XSS风险

nginx代理跨域 ​

upstream pxyStrm {
  server www.fujinhuo.com;
}
server {
  listen 80;
  server_name www.ysun.com;

  location / {
    proxy_pass http://pxyStrm;
  }
}

跨域资源共享(CORS) ​

  • 客户端
    • Access-Control-Allow-Credentials: true | false; #表示是否允许发送Cookie
    • Access-Control-Expose-Headers,CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定
  • 服务器
// 跨域后台设置
res.writeHead(200, {
  'Access-Control-Allow-Credentials': 'true',     // 后端允许发送Cookie
  'Access-Control-Allow-Origin': 'http://www.domain1.com',    // 允许访问的域(协议+域名+端口)
  /* 
    * 此处设置的cookie还是domain2的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
    * 但只要domain2中写入一次cookie认证,后面的跨域接口都能从domain2中获取cookie,从而实现所有的接口都能跨域访问
    */
  'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'  // HttpOnly的作用是让js无法读取cookie
});
  • IE8/9需要使用XDomainRequest对象来支持CORS

postMessage跨域 ​

配置一

var iframe = document.getElementById('iframe');
iframe.onload = function() {
  var data = {
    name: 'aym'
  };
  // 向domain2传送跨域数据
  iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
};

// 接受domain2返回数据
window.addEventListener('message', function(e) {
  console.log('data from domain2 ---> ' + e.data);
}, false);

配置二

// 接收domain1的数据
window.addEventListener('message', function(e) {
  alert('data from domain1 ---> ' + e.data);

  var data = JSON.parse(e.data);
  if (data) {
    data.number = 16;

    // 处理后再发回domain1
    window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
  }
}, false);

nodejs中间件代理跨域 ​

var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();

app.use('/', proxy({
  // 代理跨域目标接口
  target: 'http://www.domain2.com:8080',
  changeOrigin: true,

  // 修改响应头信息,实现跨域并允许带cookie
  onProxyRes: function(proxyRes, req, res) {
      res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
      res.header('Access-Control-Allow-Credentials', 'true');
  },

  // 修改响应信息中的cookie域名
  cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
}));

document.domain + iframe跨域 ​

location.hash + iframe ​

window.name + iframe跨域 ​

WebSocket协议跨域 ​

前端缓存与通信方案 ​

PageTransitionEvent ​

作用:当网页在加载完成或卸载后会触发页面传输事件。 为了查看页面是直接从服务器上载入还是从缓存中读取,你可以使用 PageTransitionEvent 对象的 persisted 属性来判断。 如果页面从浏览器的缓存中读取该属性返回 ture,否则返回 false 。

document.body.onpageshow = function(e) {
  if (e.persisted) {
    // 标记页面是从缓存(Backforward Cache)中加载
  } else {
    // 从服务器加载
  }
}

postMessage ​

APP调用H5方法 ​

H5中声明函数,APP直接调用即可。

语法结构 ​

// otherWindow:其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。
otherWindow.postMessage(
  message,       // 将要发送到其他 window的数据。它将会被结构化克隆算法序列化。这意味着你可以不受什么限制的将数据对象安全的传送给目标窗口而无需自己序列化。     
  targetOrigin,  // 通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。
  [transfer]     // 是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。 
);

传输File数据、Blob数据 ​

const aLink = document.getElementsByTagName('a')[0];
console.info(aLink.href);

document.getElementById('uploadFile').onclick = async function() {
  const file = document.createElement('input');
  file.setAttribute('type', 'file');
  // file.setAttribute('accept', 'video/mp4,video/webm,video/ogg');
  file.click();

  const getFileData = (file) => {
    return new Promise((resolve) => {
      file.onchange = async function(e) {
        resolve(e.target.files[0]);
      }
    });
  }

  const getReaderData = (fileData) => {
    return new Promise((resolve) => {
      const reader = new FileReader();

      // reader.readAsText(file);  
      // reader.readAsText(file, 'gb2312');
      reader.readAsDataURL(fileData);
      // reader.readAsArrayBuffer(file);
      // reader.readAsText(fileData, 'utf-8');

      reader.onload = function() {
        resolve(reader.result);
      };
    });
  };

  const fileData = await getFileData(file);
  console.info('File对象', fileData);

  const readerData = await getReaderData(fileData);
  console.info(readerData);

  window.postMessage({
    type: 'upload-file',
    data: {
      file: {name: fileData.name, type: fileData.type, size: fileData.size},
      content: readerData
    }
  });

}



window.addEventListener("message", (e) => {
  const { type, data } = e.data;
  if (type !== 'upload-file') return;

  console.info(data);
  const fileData = data.file;
  const readerData = data.content;

  const dataURLtoBlob = (dataURL) => {
    var arr = dataURL.split(','),
        mime = arr[0].match(/:(.*?);/)[1],
        bstr = atob(arr[1]),
        n = bstr.length,
        u8arr = new Uint8Array(n);
    while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
    }
    return new Blob([u8arr], { type: mime });
  }

  const dataURLBlob = dataURLtoBlob(readerData);
  console.info('dataURLBlob:', dataURLBlob);

  const newFileData = new File([dataURLBlob], fileData.name, {type: fileData.type});
  console.info('newFileData:', newFileData); 

  const formData = new FormData();
  formData.append('file', newFileData);
  uploadFileByFetch(formData);
});

代码示例 ​

// 接收消息
class PostMessage {
  constructor() {
    window.addEventListener("message", (e) => {
      this.get();
    });
  }

  // 消息源origin白名单
  WHITE_LIST = [];

  // 发送消息
  send(key, type = 'postMessage', data = null) {
    if (!key || key === '') return;
    const content = { type, data };
    window.postMessage(key, content);
  }

  // 接收消息
  get(typeName) {
    window.addEventListener("message", (e) => {
      if (WHITE_LIST.includes(e.origin)) return;

      const { type, data } = e.data;

      if (typeName && typeName.trim() !== '') {
        return data;
      }
      this[type] && this[type](data);
    });
  }

}

export default new PostMessage();

传输二进制数据 ​

传输文件对象 ​

MessageChannel ​

Channel Messaging API的MessageChannel 接口允许我们创建一个新的消息通道,并通过它的两个MessagePort 属性发送数据。

const channel = new MessageChannel();

channel.port1.onmessage = function(e) {
  console.log('port1:', e.data);
}
channel.port2.onmessage = function(e) {
  console.log('port2:', e.data);
}
// onmessageerror

channel.port1.postMessage({type: '', data: { text: '我是port1' }});
channel.port2.postMessage({type: '', data: { text: '我是port2' }});

window.postMessage('我是port2--第二次', '*', [channel.port2]);

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Cookies

Nginx配置cookie ​

设置 ​

document.cookie = "account=ysun"; document.cookie="username=John Doe; expires=Thu, 18 Dec 2043 12:00:00 GMT";

获取 ​

document.cookie

删除 ​

设置 expires 参数为以前的时间即可:document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 GMT";

Cookie属性 ​

Name:为一个cookie的名称。 Value:为一个cookie的值。 Domain:为可以访问此cookie的域名。 Path:为可以访问此cookie的页面路径。 比如domain是abc.com,path是/test,那么只有/test路径下的页面可以读取此cookie。 expires/Max-Age:为此cookie超时时间。若设置其值为一个时间,那么当到达此时间后,此cookie失效。不设置的话默认值是Session,意思是cookie会和session一起失效。当浏览器关闭(不是浏览器标签页,而是整个浏览器) 后,此cookie失效。 Size:此cookie大小 http:cookie的httponly属性。若此属性为true,则只有在http请求头中会带有此cookie的信息,而不能通过document.cookie来访问此cookie。 secure:设置是否只能通过https来传递此条cookie

{
	name: '',
	value: '',
	domain: '.baidu.com',  //m.baidu.com | app.baidu.com | www.baidu.com都可以访问,即所有以.baidu.com结尾的域名都可以访问
	path: '',
	expires: 7 | new Date((new Date().getTime()) + 60 * 1000 * 15) | 
}

限制cookie大小 ​

每个cookie限制4k

	// 设置有效日期为1天
  let date = new Date();
  date.setTime(date.getTime() + 24 * 3600 * 1000);
	console.log(date.getTime());
	let expires_times = date.toGMTString();

浏览器限制cookie数量 ​

Microsoft指出InternetExplorer8增加cookie限制为每个域名50个,但IE7似乎也允许每个域名50个cookie。 Firefox每个域名cookie限制为50个。 Opera每个域名cookie限制为30个。 Safari/WebKit貌似没有cookie限制。但是如果cookie很多,则会使header大小超过服务器的处理的限制,会导致错误发生。

超出限制后删除规则 ​

除Safari(可以设置全部cookie,不管数量多少),有两个方法:   最少最近使用(leastrecentlyused(LRU))的方法:当Cookie已达到限额,自动踢除最老的Cookie,以使给最新的Cookie一些空间。Internet Explorer和Opera使用此方法。   Firefox很独特:虽然最后的设置的Cookie始终保留,但似乎随机决定哪些cookie被保留。似乎没有任何计划(建议:在Firefox中不要超过Cookie限制)。

Firefox和Safari允许cookie多达4097个字节,包括名(name)、值(value)和等号。   Opera允许cookie多达4096个字节,包括:名(name)、值(value)和等号。   Internet Explorer允许cookie多达4095个字节,包括:名(name)、值(value)和等号。 注:多字节字符计算为两个字节。在所有浏览器中,任何cookie大小超过限制都被忽略,且永远不会被设置。

无服务器云函数架构设计 ​

无服务器云函数,即无需搭建服务器(serverless)执⾏环境而获取云函数业务实现的一种架构设计理念。开发者只需要使⽤云平台⽀持的语⾔编写核⼼代码及设置代码运⾏的条件,代码即可伸缩式、健壮性地安全运行业务逻辑,而无需考虑复杂的各种服务器运维与数据库配置等,仅仅关注实现业务模块的逻辑函数。

多语言系统 ​

export const LANGUAGE_FLAG_MAP: Record<string, LANGUAGE_FLAG_ITEM> = {
  // 现有:App 当前支持的语言
  en: { flag: "🇬🇧", name: "英语", required: true },
  ja: { flag: "🇯🇵", name: "日语", required: false },
  es: { flag: "🇪🇸", name: "西班牙语", required: false },
  de: { flag: "🇩🇪", name: "德语", required: false },
  fr: { flag: "🇫🇷", name: "法语", required: false },
  it: { flag: "🇮🇹", name: "意大利语", required: false },
  pt: { flag: "🇵🇹", name: "葡萄牙语", required: false },
  ko: { flag: "🇰🇷", name: "韩语", required: false },
  ru: { flag: "🇷🇺", name: "俄语", required: false },
  zh: { flag: "🇨🇳", name: "中文", required: false },
  // 亚洲
  zh_tw: { flag: "🇨🇳", name: "繁体中文", required: false },
  zh_hk: { flag: "🇭🇰", name: "粤语(香港)", required: false },
  hi: { flag: "🇮🇳", name: "印地语", required: false },
  bn: { flag: "🇧🇩", name: "孟加拉语", required: false },
  ur: { flag: "🇵🇰", name: "乌尔都语", required: false },
  ta: { flag: "🇮🇳", name: "泰米尔语", required: false },
  te: { flag: "🇮🇳", name: "泰卢固语", required: false },
  mr: { flag: "🇮🇳", name: "马拉地语", required: false },
  gu: { flag: "🇮🇳", name: "古吉拉特语", required: false },
  kn: { flag: "🇮🇳", name: "卡纳达语", required: false },
  ml: { flag: "🇮🇳", name: "马拉雅拉姆语", required: false },
  pa: { flag: "🇮🇳", name: "旁遮普语", required: false },
  th: { flag: "🇹🇭", name: "泰语", required: false },
  vi: { flag: "🇻🇳", name: "越南语", required: false },
  id: { flag: "🇮🇩", name: "印尼语", required: false },
  ms: { flag: "🇲🇾", name: "马来语", required: false },
  tl: { flag: "🇵🇭", name: "菲律宾语", required: false },
  km: { flag: "🇰🇭", name: "高棉语", required: false },
  my: { flag: "🇲🇲", name: "缅甸语", required: false },
  lo: { flag: "🇱🇦", name: "老挝语", required: false },
  si: { flag: "🇱🇰", name: "僧伽罗语", required: false },
  ne: { flag: "🇳🇵", name: "尼泊尔语", required: false },
  mn: { flag: "🇲🇳", name: "蒙古语", required: false },
  kk: { flag: "🇰🇿", name: "哈萨克语", required: false },
  uz: { flag: "🇺🇿", name: "乌兹别克语", required: false },
  az: { flag: "🇦🇿", name: "阿塞拜疆语", required: false },
  kmr: { flag: "🇮🇶", name: "库尔德语", required: false },
  // 中东
  ar: { flag: "🇸🇦", name: "阿拉伯语", required: false },
  fa: { flag: "🇮🇷", name: "波斯语", required: false },
  he: { flag: "🇮🇱", name: "希伯来语", required: false },
  tr: { flag: "🇹🇷", name: "土耳其语", required: false },
  // 欧洲
  nl: { flag: "🇳🇱", name: "荷兰语", required: false },
  pl: { flag: "🇵🇱", name: "波兰语", required: false },
  uk: { flag: "🇺🇦", name: "乌克兰语", required: false },
  sv: { flag: "🇸🇪", name: "瑞典语", required: false },
  no: { flag: "🇳🇴", name: "挪威语", required: false },
  nb: { flag: "🇳🇴", name: "挪威语(书面)", required: false },
  fi: { flag: "🇫🇮", name: "芬兰语", required: false },
  da: { flag: "🇩🇰", name: "丹麦语", required: false },
  is: { flag: "🇮🇸", name: "冰岛语", required: false },
  cs: { flag: "🇨🇿", name: "捷克语", required: false },
  sk: { flag: "🇸🇰", name: "斯洛伐克语", required: false },
  hu: { flag: "🇭🇺", name: "匈牙利语", required: false },
  ro: { flag: "🇷🇴", name: "罗马尼亚语", required: false },
  bg: { flag: "🇧🇬", name: "保加利亚语", required: false },
  el: { flag: "🇬🇷", name: "希腊语", required: false },
  sr: { flag: "🇷🇸", name: "塞尔维亚语", required: false },
  hr: { flag: "🇭🇷", name: "克罗地亚语", required: false },
  sl: { flag: "🇸🇮", name: "斯洛文尼亚语", required: false },
  bs: { flag: "🇧🇦", name: "波斯尼亚语", required: false },
  mk: { flag: "🇲🇰", name: "马其顿语", required: false },
  sq: { flag: "🇦🇱", name: "阿尔巴尼亚语", required: false },
  lt: { flag: "🇱🇹", name: "立陶宛语", required: false },
  lv: { flag: "🇱🇻", name: "拉脱维亚语", required: false },
  et: { flag: "🇪🇪", name: "爱沙尼亚语", required: false },
  ca: { flag: "🇪🇸", name: "加泰罗尼亚语", required: false },
  eu: { flag: "🇪🇸", name: "巴斯克语", required: false },
  gl: { flag: "🇪🇸", name: "加利西亚语", required: false },
  ga: { flag: "🇮🇪", name: "爱尔兰语", required: false },
  cy: { flag: "🏴󠁧󠁢󠁷󠁬󠁳󠁿", name: "威尔士语", required: false },
  gd: { flag: "🏴󠁧󠁢󠁳󠁣󠁴󠁿", name: "苏格兰盖尔语", required: false },
  mt: { flag: "🇲🇹", name: "马耳他语", required: false },
  be: { flag: "🇧🇾", name: "白俄罗斯语", required: false },
  // 美洲
  pt_br: { flag: "🇧🇷", name: "巴西葡萄牙语", required: false },
  es_mx: { flag: "🇲🇽", name: "墨西哥西班牙语", required: false },
  es_ar: { flag: "🇦🇷", name: "阿根廷西班牙语", required: false },
  qu: { flag: "🇵🇪", name: "克丘亚语", required: false },
  ht: { flag: "🇭🇹", name: "海地克里奥尔语", required: false },
  // 非洲
  sw: { flag: "🇰🇪", name: "斯瓦希里语", required: false },
  am: { flag: "🇪🇹", name: "阿姆哈拉语", required: false },
  ha: { flag: "🇳🇬", name: "豪萨语", required: false },
  yo: { flag: "🇳🇬", name: "约鲁巴语", required: false },
  ig: { flag: "🇳🇬", name: "伊博语", required: false },
  zu: { flag: "🇿🇦", name: "祖鲁语", required: false },
  af: { flag: "🇿🇦", name: "南非荷兰语", required: false },
  so: { flag: "🇸🇴", name: "索马里语", required: false },
  mg: { flag: "🇲🇬", name: "马尔加什语", required: false },
  // 其他
  eo: { flag: "🌍", name: "世界语", required: false },
  la: { flag: "🏛️", name: "拉丁语", required: false }
};

语言简称 ​

{
  cn: "中文",

  ca: "加拿大",
  us: "美国",
  mx: "墨西哥",
  br: "巴西",

  es: "西班牙",
  uk: "英国",
  fr: "法国",
  be: "比利时",
  nl: "荷兰",
  de: "德国",
  it: "意大利",
  se: "瑞典",
  pl: "波兰",

  za: "南非",
  eg: "埃及",
  tr: "土耳其",
  sa: "沙特阿拉伯",
  ae: "阿拉伯联合酋长国",

  in: "印度",
  sg: "新加坡",
  au: "澳大利亚",
  jp: "日本"
}


代码	语言名称
af	南非荷兰语
ar	阿拉伯语
cs	捷克语
da	丹麦语
de	德语
en	英语
en-AU	英语(澳大利亚)
en-GB	英语(英国)
en-US	英语(美国)
es	西班牙语
es-419	西班牙语(拉丁美洲)
fil	菲律宾语
fr	法语
ga	爱尔兰语
id	印度尼西亚语
it	意大利语
ja	日语
ms	马来语
ml	荷兰语
no	挪威语
pl	波兰语
pt-BR	葡萄牙语(巴西)
pt-PT	葡萄牙语
ru	俄语
sv	瑞典语
tr	土耳其语
zh-CN	中文(简体)
zh-TW	中文(繁体)

自定义语言方案 ​

每一种语种,仅需在URL后缀参数lang赋值即可

  • utils.ts
/**
 * 原始语言数据结构转换机制
 * 仅支持Object结构
 */
export function flatLanguageModel(language = {} as Record<string, any>, lang = 'cn') {
  const currentLang = lang;

  // 转换模型
  const doFlatModel = function (
    config = {} as Record<string, any>,
    instance = {} as Record<string, any>
  ) {
    if (!config || config?.constructor !== Object) return {};

    for (const key in config) {
      // 是否为有效对象
      const value = config[key];
      if (!value || value?.constructor !== Object) {
        instance[key] = value;
        continue;
      }

      // 是否为多语种配置
      else if (
        value.hasOwnProperty('cn') ||
        value.hasOwnProperty('en') ||
        value.hasOwnProperty('jp')
      ) {
        instance[key] = value[currentLang];
      }

      // 子级嵌套
      else {
        doFlatModel(value, (instance[key] = {}));
      }
    }
  };

  const flatLanguage: Record<string, any> = Object.create(language);
  doFlatModel(language, flatLanguage);

  // 返回转换后的语言结构
  return flatLanguage;
}
  • exmaple.ts
/**
 * 示例
 * import { initLanguage } from '@/languages/exmaple';
 * const Lang = initLanguage();
 */
import { flatLanguageModel } from '../utils';

// 自动加载不同语言环境内容
export function initLanguage() {
  const flatLanguage = flatLanguageModel(configLanguage);
  return flatLanguage;
}

export const configLanguage: Record<string, any> = {
  // 示例结构
  structure: {
    cn: '',
    en: '',
    jp: ''
  },
  moreDeep: {
    other: {
      cn: '',
      en: '',
      jp: ''
    }
  }
};

代码安全解决方案 ​

代码混淆 ​

JSFuck、AAEncode、JJEncode、代码压缩、变量名混淆、字符串混淆、自我保护,比如卡死浏览器、控制流平坦化、僵尸代码注入、对象键名替换、禁用控制台输出、域名锁定。

代码混淆原理 ​

  • 通过正则替换实现的混淆器
  • 通过语法树替换实现的混淆器

混淆规则 ​

  • 拆分字符串、拆分数组、增加废代码

JShaman ​

SecurityWorker ​

SecurityWorker:不同于普通的Javascript代码混淆,我们使用独立Javascript VM+二进制混淆opcode核心执行的方式防止您的代码被开发者工具调试、代码反向。

混淆 ​

市面上JavaScript词法和文法分析器有很多,比如其实v8就是一个,还有mozilla的SpiderMonkey, 知名的esprima等等,我这里要推荐的是uglify,一个基于nodejs的解析器。

  • https://jscrambler.com/

  • parser,把 JavaScript 代码解析成抽象语法树

  • code generator,通过抽象语法树生成代码

  • scope analyzer,分析变量定义的工具

  • tree walker,遍历树节点

  • tree transformer,改变树节点

混淆对性能的影响 ​

  • 减少循环混淆,循环太多会直接影响代码执行效率
  • 避免过多的字符串拼接,因为字符串拼接在低版本IE下面会有性能问题
  • 控制代码体积,在插入废代码时应该控制插入比例,文件过大会给网络请求和代码执行都带来压力

美化 ​

净化 ​

压缩 ​

解压 ​

/*   美化:格式化代码,使之容易阅读			*/
/*   净化:去掉代码中多余的注释、换行、空格等	*/
/*   压缩:将代码压缩为更小体积,便于传输		*/
/*   解压:将压缩后的代码转换为人可以阅读的格式	*/
/*   混淆:将代码的中变量名简短化以减小体积,但可读性差,经混淆后的代码无法还原	*/  

// JS压缩、解压、格式化、混淆加密、解密  | https://www.css-js.com/

jsCodeConfusion ​

<script src="/source/jsCodeConfusion/jsCodeConfusion.js"></script>
<script type="text/javascript">
  CodeConfusion(code);
</script>

webpack配置 ​

<script type="text/javascript">
var WebpackObfuscator = require('webpack-obfuscator');

plugins: [
  new WebpackObfuscator (
    { 
      rotateStringArray: true 
    }, 
    ['excluded_bundle_name.js']  // 过滤不需要混淆的文件
  )
]
</script>

Released under the MIT License.