前端不止:请告诉我,你要什么样的图标

有一个英语成语叫做一画胜千言(A picture is worth a thousand words),不知道大家有没有听过?它是指的是一张静态的图片就可表达一个复杂的概念或者与一个主题相关的图片有时比起详细的解释,能够更有效的描述有关主题。- “一画胜千言”维基百科

如果我们要用一句话来说明图标的作用,没有比这个成语更适合的词了。本篇文章,我们就来聊聊关于图标的一些事情。

一个图标的生命周期(工作流程)

关于图标的生命周期,在我个人所经历的开发项目中,有以下两种:

第一种方式:图标库(选择阶段) -> 图标使用(开发阶段)

第二种方式:图标设计(设计阶段) -> 图标导出(沟通阶段) -> 图标使用(开发阶段)

一般来说,小公司或者独立开发者会采用第一种工作流程。而大型组织或公司因为拥有更完善的团队和资源,一般会采取第二种方式,能够获得更多自主权和建立企业VI(Visual Identity,企业视觉识别)的能力。

但无论哪种方式,都包括两个角色:设计师和Web开发。只是在第一种工作方式中,设计师是不可见的。

图标的设计和使用

设计阶段通常是由不了解Web开发的设计师们来完成的,他们会根据产品的需要,绘出满足需求的图标,然后交给Web开发人员使用。

(ThoughtWorks官网“Contact with us”图标)

为什么要先介绍图标的使用,而一笔跳过导出过程呢?原因很简单,因为我们需要先知道服务的对象是谁,才知道如何正确的为它服务。

常见的三种图标的使用方式

1.使用图片

直接将设计师画好的图标,以PNG格式的图片一个个分离导出,这是最直观的图标打包方式。

(FlatIcon图标)

它的优点是:

  • 能够使用彩色的图标
  • 能够支持大部分浏览器

缺点是:

  • 图标大小是固定的(不能根据场景自由缩放)
  • Retina屏幕需要两倍图

开发人员拿到这样的图标,通常需要先将其合成为一张图片,以方便制作雪碧图,这个过程可以由开发人员自己完成,也可以由设计师来做(设计师可以根据源文件中心导出一张包含所有图标的PNG文件制作)。

制作雪碧图的工具有很多,我比较常用的在线雪碧图工具是:Sprite Cow,或者是NodeJS平台下的构建工具插件,如:webpack-spritesmith

2.直接使用svg

使用SVG(可缩放矢量图形),W3C标准是最被看好的Web端图形解决方案。它能提供如裁剪路径、Alpha通道、滤镜效果等复杂渲染能力,具备传统图片没有的矢量功能,还可以被记事本等阅读器、搜索引擎访问。

设计师可以轻松的在设计绘图软件(AI,PS)的帮助下导出SVG格式的图标/图片。

但目前,国内svg还没有被非常广泛的使用,原因在于兼容性不足,不能够很好的兼容旧的IE版本和一些Android原生浏览器。

(Can I use svg?)

上图为百度对2017年前三个月的浏览器使用进行的统计,目前国内还有超过20%的用户仍在使用IE8,9甚至是IE7。

3.IconFont

IconFont是目前最为流行的图标解决方案,顾名思义,它就是字体文件,你可以用任何一个字体编辑工具打开它,如果你打开某一个查看,就会发现它就是一些路径,这些路径可以用AI,PS,Sketch等软件来绘制。

IconFont的优点在于能够用CSS控制样式,无限缩放而不失真,支持IE7+,兼顾屏幕阅读器,不过缺点是不能支持彩色图标(拥有多种颜色的图标)。获得IconFont的方式也很简单,设计师将图标通过AI/PS转成SVG文件,然后由开发人员通过工具(在线或者本地)转换为IconFont,比如:国外的icomoon.io,国内的iconfont.cn,开源构建工具插件有gulp-iconfont等等。

产生适合Web开发的图标

“产生适合Web开发的图标”是我们本篇文章要关注的重点。

1.使用图片的方式

如果开发人员直接使用图片,则相对简单,设计师只需要针对普通屏幕和Retina屏幕准备两套图(单倍图和两倍图)。

以国内某著名的中文小说阅读网站为例,会针对不同的设备使用不同倍数的logo图片,以保证在如Retina屏幕下的清晰度。

.logo-wrap .logo a {
    display: block;
    width: 219px;
    height: 52px;
    background: url(/qd/images/logo.dbed5.png) no-repeat;
}

@media not all, not all, (-webkit-min-device-pixel-ratio: 1.3), not all, (min-resolution: 1.3dppx) {  
    .logo-wrap .logo a {
        background: url(/qd/images/logo3x.fd980.png) no-repeat;
        background-repeat: no-repeat;background-size: 217px;
    }
}
)

2.使用SVG

关于转换成SVG,这里就要引荐一下Sara Soueidan在Generate London 2015 Conference上的演讲《Sara Soueidan: SVG for Web Designers (and Developers)》(YouTube视频需要翻墙),如果不方便,Sara Soueidan有一篇博客《Tips for Creating and Exporting Better SVGs for the Web》更详细的讲解了关于SVG导出的内容,当然,还有一篇国内的翻译文章《创建和导出SVG的技巧》,最后再推荐一篇Adobe工程师michael chaize写的关于AI导出SVG的文章《Export SVG for the web with Illustrator CC》

在上述资料中,我觉得看视频更直观,顺便领略一下这位优秀的阿拉伯女性前端开发工程师(兼自由作家和演讲人)的风采。

博客和视频中谈到了多个点导出SVG需要注意的地方,由于篇幅限制,这里简单描述三个tips:

1. 选择适合绘画的画板

你有在网页上嵌入过SVG吗,给它指定一个高度和宽度,然后发现它其实比你指定的尺寸要小?开发人员常常会遇到这样的问题。

一般来说,这是因为SVG视窗中有一定大小的白色空白空间。视窗是按照样式表的指定尺寸显示的,但是它里面有额外的空白——在图形周围——使得你的图片看起来好像“缩水”了,因为这块空白在视窗里面是占空间的。为了避免这种情况,你需要确保你的画板是刚刚好能容纳里面的图像的,不要大太多。

画板的尺寸就是导出的SVG视窗的尺寸,所有画板上的空白最终都会变成视窗中的白色空白。

对于没有AI工具的开发,可以在下面的SVGO优化选项中选择“Prefer viewBox to width/height”。

2. 选择合适的导出选项

上图展示的选项是推荐的生成适合Web使用的SVG。如果你不想使用Web字体,可以选择把文本转换成轮廓。

如果SVG中包含大量的文字,这个选项output fewer tspan elements可以在很大程度上降低svg的大小。

3. 优化SVG

通常是建议在把SVG从图形编辑器中导出后,再用单独的优化工具来进行优化。比如:删除无用Comments和Metadata,简化代码,简化单个路径等。推荐的第三方工具:NodeJS工具svgomg,AI插件SVG-NOW,Sketch插件Svgo-compressor等,请参考Sara Soueidan的文章《Useful SVGO[ptimization] Tools》

3. IconFont

前面提到IconFont一般是由SVG通过工具转换而来,而如果开发最终需要使用IconFont来展示图标,那么对于导出的SVG有一些特殊要求。我在本文的前面一小节,已经介绍了几款IconFont的转换工具,每一款工具都有详细的文档来说明SVG绘制的规则,尽管不尽相同,但有一些基本原则是一致的:

  1. 将文字转换为路径
  2. 不可以使用图片(字体只是路径)
  3. 修剪画板(trimming to art boundaries)(前面已经介绍过)
  4. 将描边转化为闭合图形
  5. 简化无用的节点
  6. ……

更多关于IconFont的绘画规则,请参考:Iconfont.cn文档Icomoon文档gulp-iconfont文档fontello文档。

及时和频繁的沟通

Sara Soueidan说过一句话:“设计师和开发者应该成为好朋友”。

我们今天的话题正好涉及到这两个角色,也许你会觉得它们俩似乎有点“八竿子打不着”,但其实不是。请看下面这张图,敏捷的开发过程中不同角色共享职责,那么设计师和开发也不例外。

(敏捷开发中不同角色共享职责)

在ThoughtWorks工作,你会发现不少设计师懂HTML,CSS,甚至如何用Chrome查看元素,同时有不少开发对设计也颇有研究和兴趣。而我们的设计师和开发人员会坐在同一张桌子上一起完成工作,以保证及时和频繁的需求沟通和合作。

至于“设计师和开发者应该成为好朋友”,作为一名Dev,我就跟好多设计师都是朋友(至少我是这么认为的)。

而为了更好的做到沟通顺畅和职责共享,还出现了一种新(相对较新)的角色UI Dev,如下图。不过,关于这个角色的定义众说纷纭,我们就不在这里细聊了。

(UI Developer(参考自Stack Overflow答案))

结尾

在本篇文章中,我们谈了图标的三种使用方式:图片、SVG、IconFont,而它们也只是图标这个话题的冰山一角。虽然篇幅很短,但尤其重要的是,保证团队中设计师和开发人员便捷的协作工作,一起找到满足团队需求的解决方案,才是保证图标质量的关键。


更多精彩洞见,请关注微信公众号:思特沃克

Share

前端不止:Retina屏幕下两倍图

所见不一定即所得

眼睛是心灵的窗户,也是蒙蔽你的一种途径。

假设,我给你一张图片,你觉得肉眼可以观察到全部的细节吗?

屏幕上一张清晰的图片

肉眼在屏幕上看到图片的清晰度由三个因素决定,一是图片像素本身是否精细,二是屏幕分辨率,三是屏幕大小。

我们来逐步分析它们之间的关系:

屏幕分辨率

屏幕分辨率也就是设备分辨率,设备像素,它是物理的像素,比如,新的iPhone7,屏幕分辨率是1334 x 750像素分辨率,326 ppi。

图像大小

如果你学过《数字图像处理》这门课,那你对下面的解释就是非常熟悉了。

位图是由像素(Pixel)组成的,像素是位图最小的信息单元,存储在图像栅格中。每个像素都具有特定的位置和颜色值。按从左到右、从上到下的顺序来记录图像中每一个像素的信息,如:像素在屏幕上的位置、像素的颜色等。位图图像质量是由单位长度内像素的多少来决定的。单位长度内像素越多,分辨率越高,图像的效果越好。

假设,以上这个logo的图像大小是1334 x 750像素和iPhone7屏幕分辨率一样,那么,一位图像素对应的就是一个设备像素,这就是会是一个完全保真的显示。因为一个位置像素不能进一步分裂,我想这一点应该大家非常容易理解,也就是一个萝卜一个坑。

屏幕分辨率和屏幕尺寸

相信大部分人对上面这个设置肯定特别熟悉,有些人可能对XP,甚至98系统的样式更熟悉(一不小心暴露了年龄),在Windows系统下,提高屏幕分辨率一般都需要提高屏幕尺寸。

因为在固定屏幕的情况下,提高屏幕分辨率(如上图),图像和文字显示目标会相应缩小,原因是系统并不会自动根据屏幕尺寸和分辨率关系相应的调整文字和图标的大小,这是Windows系统自身的行为。

我相信,如果家里有年长的人使用电脑,肯定屏幕分辨率调的很低,因为这样文字和图标才会比较大,我家06年买的台式机就是这样。

也因此,我们很容易有一个错觉,那就是屏幕越大,分辨率就能越大(在单位面积内像素数量固定的情况下,尺寸越大,单个屏幕拥有的像素就越多,分辨率自然就越大)。

直到,苹果Retina屏幕的出现,原来小屏幕也可以拥有大分辨率。

PPI的概念

PPI,像素密度,即每英寸所拥有的像素数目(比如:上面iPhone 7的PPI是326),PPI数值越高,代表显示屏能够以越高的密度显示图像,画面的细节就会越丰富。

以Retina屏幕为例,它并不是像普通显示器那样通过增大尺寸来增加分辨率,而是靠提升屏幕单位面积内的像素数量,即像素密度来提升分辨率,这样就有了高像素密度屏幕。

根据上面的分析,分辨率提升了,那么图标和文字尺寸就会变小,但是Mac的操作系统不同,它自动采取相应的模式(如Mac下的HiDPI)进行适配,将缩小后的字体(苹果一直采用矢量字体)和图标重新放大,这样苹果用了更多的像素数来显示同样的内容,所以显示尺寸仍然不变。

苹果将“高像素密度屏幕”的概念营销出一个专业的术语“Retina”,将其称为双密度显示,声称人类的肉眼将无法区分单个像素。

当一个显示屏像素密度超过300ppi时,人眼就无法区分出单独的像素。这也是讲:显示设备清晰度已达到人视网膜可分辨像素的极限。因此,行动电话显示器的像素密度达到或高于300ppi就不会再出现颗粒感,而手持平板类电器显示器的像素密度达到或高于260ppi就不会再出现颗粒感,苹果电脑Mac的Retina显示器像素密度只要超过200ppi就无法区分出单独的像素。

好,说了这么多,都是谈屏幕的问题,貌似和前端开发没有什么关系,我又不是要买新手机(呵呵),那么现在,我们现在来谈谈前端的问题。

Web中的像素(CSS像素)

CSS像素是一个抽象概念,设备无关像素,简称-“DIPS”,device-independent像素,主要使用在浏览器上,用来精确的度量(确定)Web页面上的内容。

在标准情况下一个CSS像素对应一个设备像素。

.box {
  width: 200px;
  height: 300px;
  font-size: 12px;
}

上面的代码,将会在显示屏设备上绘制一个200×300像素的盒子,在标准屏幕下,它占据的就是200×300设备像素。但是在Retina屏幕下,相同的div却使用了400×600设备像素,保持相同的物理尺寸显示,导致每个像素点实际上有4倍的普通像素点。

对于图片来说也是如此:

这个时候,屏幕会怎么处理呢?其实,有点类似图像软件的放大图片功能,采用自有的算法(图像处理算法)计算放大方式。只不过,这里是苹果Retina屏幕的计算方法,一个CSS像素点实际分成了四个,造成颜色肯定会存在偏差(非全保真的显示),于是,我们看上去就变得模糊了(特别是图片,非常的明显)。

开发当中遇到这样的事情,我们应该怎么处理呢?这时,我们需要引出devicePixelRatio的概念。

devicePixelRatio设备像素比

window.devicePixelRatio是设备上物理像素和设备独立像素(device-independent pixels (dips))的比例。

公式表示就是:window.devicePixelRatio = 物理像素 / dips

  • 普通密度桌面显示屏的devicePixelRatio=1
  • 高密度桌面显示屏(Mac Retina)的devicePixelRatio=2
  • 主流手机显示屏的devicePixelRatio=2或3

举例说明,一张100×100的图片,通过CSS设置它{ width:100px; height:100px }。在普通密度桌面显示屏的电脑上打开,没有什么问题,但假设在手机/或者Retina屏幕的Mac,按照逻辑分辨率来渲染,他们的devicePixelRatio=2,那么就相当于拿4个物理像素来描绘1个电子像素。这等于拿一个2倍的放大镜去看图片,图片可能因此变得模糊。

代码如何解决呢?

原理我们明白了,那么从代码层面,我们应该如何实现呢?

一个常见的做法是把图片换成200×200的,CSS宽高不变,仍然是{ width:100px; height:100px },这样,CSS宽高换算成物理像素是200×200,图片也是200×200,就不会变糊了。可以采用媒体查询和JS操作的方式

CSS Media Queries

#element { background-image: url('hires.png'); }

@media only screen and (min-device-pixel-ratio: 2) {
    #element { background-image: url('hires@2x.png'); }
}

@media only screen and (min-device-pixel-ratio: 3) {
    #element { background-image: url('hires@3x.png'); }
}

JS查询

retinajs库

是不是适配Retina屏幕所有的图片都需要切换呢?

不是,一般情况下,不需要针对网站上的所有图片都提供两个版本(非Retina屏幕和Retina屏幕),大部分图片缩放并不会太多的影响用户的体验。

常常需要被处理的图片有:网站的logo、彩色图片图标,因为他们的图像大小都偏小,在Retina上物理像素放两倍显示就会出现模糊情况,这个时候,你就需要通过媒体查询或者JS操作来替换图片。

最后

眼睛是心灵的窗户,也是蒙蔽你的一种途径,带上知识的眼镜,将世界看个清楚。


参考资料:

  1. http://www.w3cplus.com/css/towards-retina-web.html
  2. http://www.jianshu.com/p/bb76c606f0b4
  3. https://developer.mozilla.org/zh-CN/docs/Mobile/Viewport_meta_tag
  4. http://caniuse.com/#search=devicePixelRatio
  5. https://www.web-tinker.com/article/20590.html

更多精彩洞见,请关注微信公众号:思特沃克

Share

8大前端安全问题(下)

《8大前端安全问题(上)》这篇文章里我们谈到了什么是前端安全问题,并且介绍了其中的4大典型安全问题,本篇文章将介绍剩下的4大前端安全问题,它们分别是:

  • 防火防盗防猪队友:不安全的第三方依赖包
  • 用了HTTPS也可能掉坑里
  • 本地存储数据泄露
  • 缺乏静态资源完整性校验

防火防盗防猪队友:不安全的第三方依赖包

现如今进行应用开发,就好比站在巨人的肩膀上写代码。据统计,一个应用有将近80%的代码其实是来自于第三方组件、依赖的类库等,而应用自身的代码其实只占了20%左右。无论是后端服务器应用还是前端应用开发,绝大多数时候我们都是在借助开发框架和各种类库进行快速开发。

这样做的好处显而易见,但是与此同时安全风险也在不断累积——应用使用了如此多的第三方代码,不论应用自己的代码的安全性有多高,一旦这些来自第三方的代码有安全漏洞,那么对应用整体的安全性依然会造成严峻的挑战。

(图片来自:http://t.cn/RlAQsZ0

举个例子,jQuery就存在多个已知安全漏洞,例如jQuery issue 2432,使得应用存在被XSS攻击的可能。而Node.js也有一些已知的安全漏洞,比如CVE-2017-11499,可能导致前端应用受到DoS攻击。另外,对于前端应用而言,除使用到的前端开发框架之外,通常还会依赖不少Node组件包,它们可能也有安全漏洞。

手动检查这些第三方代码有没有安全问题是个苦差事,主要是因为应用依赖的这些组件数量众多,手工检查太耗时,好在有自动化的工具可以使用,比如NSP(Node Security Platform),Snyk等等。

用了HTTPS也可能掉坑里

为了保护信息在传输过程中不被泄露,保证传输安全,使用TLS或者通俗的讲,使用HTTPS已经是当今的标准配置了。然而事情并没有这么简单,即使是服务器端开启了HTTPS,也还是存在安全隐患,黑客可以利用SSL Stripping这种攻击手段,强制让HTTPS降级回HTTP,从而继续进行中间人攻击。

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

解决这个安全问题的办法是使用HSTS(HTTP Strict Transport Security),它通过下面这个HTTP Header以及一个预加载的清单,来告知浏览器在和网站进行通信的时候强制性的使用HTTPS,而不是通过明文的HTTP进行通信:

Strict-Transport-Security: max-age=<seconds>; includeSubDomains; preload

这里的“强制性”表现为浏览器无论在何种情况下都直接向服务器端发起HTTPS请求,而不再像以往那样从HTTP跳转到HTTPS。另外,当遇到证书或者链接不安全的时候,则首先警告用户,并且不再让用户选择是否继续进行不安全的通信。

(图片来自:http://t.cn/Rfj3Tku

本地存储数据泄露

以前,对于一个Web应用而言,在前端通过Cookie存储少量用户信息就足够支撑应用的正常运行了。然而随着前后端分离,尤其是后端服务无状态化架构风格的兴起,伴随着SPA应用的大量出现,存储在前端也就是用户浏览器中的数据量也在逐渐增多。

前端应用是完全暴露在用户以及攻击者面前的,在前端存储任何敏感、机密的数据,都会面临泄露的风险,就算是在前端通过JS脚本对数据进行加密基本也无济于事。

举个例子来说明,假设你的前端应用想要支持离线模式,使得用户在离线情况下依然可以使用你的应用,这就意味着你需要在本地存储用户相关的一些数据,比如说电子邮箱地址、手机号、家庭住址等PII(Personal Identifiable Information)信息,或许还有历史账单、消费记录等数据。

尽管有浏览器的同源策略限制,但是如果前端应用有XSS漏洞,那么本地存储的所有数据就都可能被攻击者的JS脚本读取到。如果用户在公用电脑上使用了这个前端应用,那么当用户离开后,这些数据是否也被彻底清除了呢?前端对数据加密后再存储看上去是个防御办法,但其实仅仅提高了一点攻击门槛而已,因为加密所用到的密钥同样存储在前端,有耐心的攻击者依然可以攻破加密这道关卡。

所以,在前端存储敏感、机密信息始终都是一件危险的事情,推荐的做法是尽可能不在前端存这些数据。

缺乏静态资源完整性校验

出于性能考虑,前端应用通常会把一些静态资源存放到CDN(Content Delivery Networks)上面,例如Javascript脚本和Stylesheet文件。这么做可以显著提高前端应用的访问速度,但与此同时却也隐含了一个新的安全风险。

如果攻击者劫持了CDN,或者对CDN中的资源进行了污染,那么我们的前端应用拿到的就是有问题的JS脚本或者Stylesheet文件,使得攻击者可以肆意篡改我们的前端页面,对用户实施攻击。这种攻击方式造成的效果和XSS跨站脚本攻击有些相似,不过不同点在于攻击者是从CDN开始实施的攻击,而传统的XSS攻击则是从有用户输入的地方开始下手的。

防御这种攻击的办法是使用浏览器提供的SRI(Subresource Integrity)功能。顾名思义,这里的Subresource指的就是HTML页面中通过<script><link>元素所指定的资源文件。

每个资源文件都可以有一个SRI值,就像下面这样。它由两部分组成,减号(-)左侧是生成SRI值用到的哈希算法名,右侧是经过Base64编码后的该资源文件的Hash值。

<script src=“https://example.js” integrity=“sha384-eivAQsRgJIi2KsTdSnfoEGIRTo25NCAqjNJNZalV63WKX3Y51adIzLT4So1pk5tX”></script>

浏览器在处理这个script元素的时候,就会检查对应的JS脚本文件的完整性,看其是否和script元素中integrity属性指定的SRI值一致,如果不匹配,浏览器则会中止对这个JS脚本的处理。

小结

在上一篇和本篇文章中,我们为大家介绍了在开发前端应用的时候容易遇到的8大安全问题,它们是:

  • 老生常谈的XSS
  • 警惕iframe带来的风险
  • 别被点击劫持了
  • 错误的内容推断
  • 防火防盗防猪队友:不安全的第三方依赖包
  • 用了HTTPS也可能掉坑里
  • 本地存储数据泄露
  • 缺乏静态资源完整性校验

我们希望能通过对这些问题的介绍,引起前端开发小伙伴的注意,尽可能提前绕过这些安全问题的坑。


更多精彩洞见,请关注微信公众号:思特沃克

Share

8大前端安全问题(上)

当我们说“前端安全问题”的时候,我们在说什么

“安全”是个很大的话题,各种安全问题的类型也是种类繁多。如果我们把安全问题按照所发生的区域来进行分类的话,那么所有发生在后端服务器、应用、服务当中的安全问题就是“后端安全问题”,所有发生在浏览器、单页面应用、Web页面当中的安全问题则算是“前端安全问题”。比如说,SQL注入漏洞发生在后端应用中,是后端安全问题,跨站脚本攻击(XSS)则是前端安全问题,因为它发生在用户的浏览器里。

除了从安全问题发生的区域来分类之外,也可以从另一个维度来判断:针对某个安全问题,团队中的哪个角色最适合来修复它?是后端开发还是前端开发?

总的来说,当我们下面在谈论“前端安全问题”的时候,我们说的是发生在浏览器、前端应用当中,或者通常由前端开发工程师来对其进行修复的安全问题。

8大前端安全问题

按照上面的分类办法,我们总结出了8大典型的前端安全问题,它们分别是:

  • 老生常谈的XSS
  • 警惕iframe带来的风险
  • 别被点击劫持了
  • 错误的内容推断
  • 防火防盗防猪队友:不安全的第三方依赖包
  • 用了HTTPS也可能掉坑里
  • 本地存储数据泄露
  • 缺失静态资源完整性校验

由于篇幅所限,本篇文章先给各位介绍前4个前端安全问题。

老生常谈的XSS

XSS是跨站脚本攻击(Cross-Site Scripting)的简称,它是个老油条了,在OWASP Web Application Top 10排行榜中长期霸榜,从未掉出过前三名。XSS这类安全问题发生的本质原因在于,浏览器错误的将攻击者提供的用户输入数据当做JavaScript脚本给执行了。

XSS有几种不同的分类办法,例如按照恶意输入的脚本是否在应用中存储,XSS被划分为“存储型XSS”和“反射型XSS”,如果按照是否和服务器有交互,又可以划分为“Server Side XSS”和“DOM based XSS”。

无论怎么分类,XSS漏洞始终是威胁用户的一个安全隐患。攻击者可以利用XSS漏洞来窃取包括用户身份信息在内的各种敏感信息、修改Web页面以欺骗用户,甚至控制受害者浏览器,或者和其他漏洞结合起来形成蠕虫攻击,等等。总之,关于XSS漏洞的利用,只有想不到没有做不到。

如何防御

防御XSS最佳的做法就是对数据进行严格的输出编码,使得攻击者提供的数据不再被浏览器认为是脚本而被误执行。例如<script>在进行HTML编码后变成了&lt;script&gt;,而这段数据就会被浏览器认为只是一段普通的字符串,而不会被当做脚本执行了。

编码也不是件容易的事情,需要根据输出数据所在的上下文来进行相应的编码。例如刚才的例子,由于数据将被放置于HTML元素中,因此进行的是HTML编码,而如果数据将被放置于URL中,则需要进行URL编码,将其变为%3Cscript%3E。此外,还有JavaScript编码、CSS编码、HTML属性编码、JSON编码等等。好在现如今的前端开发框架基本上都默认提供了前端输出编码,这大大减轻了前端开发小伙伴们的工作负担。

其他的防御措施,例如设置CSP HTTP Header、输入验证、开启浏览器XSS防御等等都是可选项,原因在于这些措施都存在被绕过的可能,并不能完全保证能防御XSS攻击。不过它们和输出编码却可以共同协作实施纵深防御策略。

你可以查阅OWASP XSS Prevention Cheat Sheet_Prevention_Cheat_Sheet),里面有关于XSS及其防御措施的详细说明。

警惕iframe带来的风险

有些时候我们的前端页面需要用到第三方提供的页面组件,通常会以iframe的方式引入。典型的例子是使用iframe在页面上添加第三方提供的广告、天气预报、社交分享插件等等。

iframe在给我们的页面带来更多丰富的内容和能力的同时,也带来了不少的安全隐患。因为iframe中的内容是由第三方来提供的,默认情况下他们不受我们的控制,他们可以在iframe中运行JavaScirpt脚本、Flash插件、弹出对话框等等,这可能会破坏前端用户体验。

如果说iframe只是有可能会给用户体验带来影响,看似风险不大,那么如果iframe中的域名因为过期而被恶意攻击者抢注,或者第三方被黑客攻破,iframe中的内容被替换掉了,从而利用用户浏览器中的安全漏洞下载安装木马、恶意勒索软件等等,这问题可就大了。

如何防御

还好在HTML5中,iframe有了一个叫做sandbox的安全属性,通过它可以对iframe的行为进行各种限制,充分实现“最小权限“原则。使用sandbox的最简单的方式就是只在iframe元素中添加上这个关键词就好,就像下面这样:

<iframe sandbox src="..."> ... </iframe>

sandbox还忠实的实现了“Secure By Default”原则,也就是说,如果你只是添加上这个属性而保持属性值为空,那么浏览器将会对iframe实施史上最严厉的调控限制,基本上来讲就是除了允许显示静态资源以外,其他什么都做不了。比如不准提交表单、不准弹窗、不准执行脚本等等,连Origin都会被强制重新分配一个唯一的值,换句话讲就是iframe中的页面访问它自己的服务器都会被算作跨域请求。

另外,sandbox也提供了丰富的配置参数,我们可以进行较为细粒度的控制。一些典型的参数如下:

  • allow-forms:允许iframe中提交form表单
  • allow-popups:允许iframe中弹出新的窗口或者标签页(例如,window.open(),showModalDialog(),target=”_blank”等等)
  • allow-scripts:允许iframe中执行JavaScript
  • allow-same-origin:允许iframe中的网页开启同源策略

更多详细的资料,可以参考iframe中关于sandbox的介绍

别被点击劫持了

有个词叫做防不胜防,我们在通过iframe使用别人提供的内容时,我们自己的页面也可能正在被不法分子放到他们精心构造的iframe或者frame当中,进行点击劫持攻击。

这是一种欺骗性比较强,同时也需要用户高度参与才能完成的一种攻击。通常的攻击步骤是这样的:

  1. 攻击者精心构造一个诱导用户点击的内容,比如Web页面小游戏
  2. 将我们的页面放入到iframe当中
  3. 利用z-index等CSS样式将这个iframe叠加到小游戏的垂直方向的正上方
  4. 把iframe设置为100%透明度
  5. 受害者访问到这个页面后,肉眼看到的是一个小游戏,如果受到诱导进行了点击的话,实际上点击到的却是iframe中的我们的页面

点击劫持的危害在于,攻击利用了受害者的用户身份,在其不知情的情况下进行一些操作。如果只是迫使用户关注某个微博账号的话,看上去仿佛还可以承受,但是如果是删除某个重要文件记录,或者窃取敏感信息,那么造成的危害可就难以承受了。

如何防御

有多种防御措施都可以防止页面遭到点击劫持攻击,例如Frame Breaking方案。一个推荐的防御方案是,使用X-Frame-Options:DENY这个HTTP Header来明确的告知浏览器,不要把当前HTTP响应中的内容在HTML Frame中显示出来。

关于点击劫持更多的细节,可以查阅OWASP Clickjacking Defense Cheat Sheet

错误的内容推断

想象这样一个攻击场景:某网站允许用户在评论里上传图片,攻击者在上传图片的时候,看似提交的是个图片文件,实则是个含有JavaScript的脚本文件。该文件逃过了文件类型校验(这涉及到了恶意文件上传这个常见安全问题,但是由于和前端相关度不高因此暂不详细介绍),在服务器里存储了下来。接下来,受害者在访问这段评论的时候,浏览器会去请求这个伪装成图片的JavaScript脚本,而此时如果浏览器错误的推断了这个响应的内容类型(MIME types),那么就会把这个图片文件当做JavaScript脚本执行,于是攻击也就成功了。

问题的关键就在于,后端服务器在返回的响应中设置的Content-Type Header仅仅只是给浏览器提供当前响应内容类型的建议,而浏览器有可能会自作主张的根据响应中的实际内容去推断内容的类型。

在上面的例子中,后端通过Content-Type Header建议浏览器按照图片来渲染这次的HTTP响应,但是浏览器发现响应中其实是JavaScript,于是就擅自做主把这段响应当做JS脚本来解释执行,安全问题也就产生了。

如何防御

浏览器根据响应内容来推断其类型,本来这是个很“智能”的功能,是浏览器强大的容错能力的体现,但是却会带来安全风险。要避免出现这样的安全问题,办法就是通过设置X-Content-Type-Options这个HTTP Header明确禁止浏览器去推断响应类型。

同样是上面的攻击场景,后端服务器返回的Content-Type建议浏览器按照图片进行内容渲染,浏览器发现有X-Content-Type-OptionsHTTP Header的存在,并且其参数值是nosniff,因此不会再去推断内容类型,而是强制按照图片进行渲染,那么因为实际上这是一段JS脚本而非真实的图片,因此这段脚本就会被浏览器当作是一个已经损坏或者格式不正确的图片来处理,而不是当作JS脚本来处理,从而最终防止了安全问题的发生。

更多关于X-Content-Type-Options的细节请参考这里

小结

本文对前端安全问题进行了一次梳理,介绍了其中4个典型的前端安全问题,包括它们发生的原因以及防御办法。在下篇文章中,我们将介绍其他的几个前端安全问题,敬请期待。


更多精彩洞见,请关注微信公众号:ThoughtWorks商业洞见

Share

在前端性能优化中应用HTTP缓存的三部曲

Spike先生是Best Experience公司的IT运营部门主管,他的团队成功地利用Http Cache优化了前端工程。

Spike将通过三个Scenario来展示他的团队是如何做到这一点的:

  • 通过配置Http Cache Expire来消减访问压力,提高用户体验
  • 通过版本化来强制失效本地的过期缓存
  • 通过内容摘要命名文件来更精确的控制缓存以及实现非覆盖式的发布

第一个故事:我不想要那么多服务器和带宽

Best Experience面临的资源访问压力和用户体验方面的问题

随着Best Experience提供的前端应用越来越强大,Spike的压力也越来越大:

  • IT部门为了应对来自静态资源的访问压力,不断购置服务器和带宽。
  • 糟糕的用户体验使得用户转投到竞争对手的网站。

工程师们刚刚通过应用Minify、AMD、打包、Gzip等手段优化了前端页面的体验, 最终得到如下图所示的一个资源引用关系:

“还是很多东西要下载啊,该拿什么来拯救该死的延迟呢?”——Spike看着图想到。

他突然想起来:在早年间,Yahoo曾发布了关于优化前端体验的35条建议和指导,其中第三条是:“Add an Expires or a Cache-Control Header”。

Yahoo是这样描述这条建议的:

Web page designs are getting richer and richer, which means more scripts, stylesheets, images, and Flash in the page. A first-time visitor to your page may have to make several HTTP requests, but by using the Expires header you make those components cacheable. This avoids unnecessary HTTP requests on subsequent page views. Expires headers are most often used with images, but they should be used on all components including scripts, stylesheets, and Flash components.

Browsers (and proxies) use a cache to reduce the number and size of HTTP requests, making web pages load faster.

“这个正是我寻找的银弹”——Spike得意的笑了。

于是,Spike写下了第一个Technology Story。

作为IT 部门的老大:

我希望通过应用HTTP缓存技术,重用已经下载过的资源,

用于消减用户在浏览页面时产生的不必要的Http Request。

以此,来提升用户在浏览页面时候的体验,

以及降低对于公司服务器资源的访问压力。

并找来了工程师Tom。

Expire带来的美好生活

Tom刚刚参与了前一轮的优化工作,虽然成果显著,但是他并不满足。

当Tom看到Jim写下的Story时眼前一亮:“这个方法太赞了!我甚至可以在登录页面底部放置对其他页面资源的引用。提升用户在整个网站的浏览体验。”——Tom的小宇宙瞬间爆发,很快就完成了新的优化方案。

Best-Experience的用户在接下来的时间里浏览页面,会这样下载资源,以图片bgimage.png为例:

  • 用户第一次获取图片的时候,Http Request 如图:

  • 之后用户再次获取图片的时候,则完全可以从浏览器的缓存中读取数据了。

因为采用了Http缓存方案,

  • 用户的feedback越来越好,访问量提高了;
  • IT部门也不用那么多服务器和带宽了。

财务总监邀请Spike共进晚餐,并谈起了自己在希腊的度假。

“我想我也应该去圣托里尼度个假,犒劳下自己”——Spike美滋滋的想到。

第二个故事:失效缓存是个技术活

这个BUG我们明明修了啊!

一天,QA Tyke发现最近一轮发布的前端应用中没有包含很多新的feature。Jerry承诺说已经跟着这个月的release上线了,还测试过了。经过一番折腾,Jerry发现浏览器一直在使用旧的缓存,而不是最新的版本。Spike找来了Jerry 和Tom,三个人一起手动对引用的资源做了重命名、做了紧急修复。

“真是没有银弹啊,我的圣托里尼啊!”——Spike头疼的想到。

Spike、Jerry、Tom和Tyke坐在了一起,得出了新的结论:

  • 缓存前端工程中的资源时,需要考虑缓存有效期的问题
  • 虽然35条建议和指导中建议“Configure ETags”,但是很难确定静态资源缓存的有效期
  • 虽然Http缓存可以支持No-Cache或者max-age =0的方式,保证浏览器每次都向服务器验证缓存有效性,但是这样会大大增加服务器的压力
  • 可以通过在资源引用上增加形如:<…. src=”###.js?v=$version$”>的版本化方式,来强制浏览器更新缓存。

Spike写下了新的Technology Story

作为IT部门的老大:

我希望在前端系统中,对引用的静态资源进行版本化管理。

使之既可以通过Http缓存来提升用户体验,降低服务器压力;

也可以方便用户即时获得更新后的资源。

“这都10月了,看来是去不成圣托里尼了,总觉得这个方案哪里有问题”——Spike忐忑不安。

用版本机制来保证浏览器更新资源

Jerry和Tom(很难想象他们两怎么配合的)终于在前端工程中实现了自动化的资源版本化管理:用户在最初访问页面的时候,会得到这样一个资源引用:

而当新的版本上线后,用户会得到这样一个资源引用:

第三个故事:更精确的缓存管理和平滑升级

(这个案例来自于知乎的大公司里怎样开发和部署前端代码? 张云龙的回答,前一个 story的内容有涉及)

每次更新后的尖峰时刻

11月的Release后,运维人员Nibbles找到Spike,“这次上线以后,服务器压力突然剧增,从GA上看到用户花了很多时间在资源下载上”,Spike找来了Tom、Jerry、Tyke和Nibbles,几个人坐在一起分析原因:

“这是因为11月的部署完成后,前端应用引用的资源版本升级,所有缓存失效导致的”——Tom 想了想说

“所有的资源引用?我还以为我们能精确到每一个文件的更新呢”——Nibbles惊讶道

“如果单独标明每一个资源的版本,那么按照我们的实际情况来看,每次上线后访问压力就没那么大了”——Tyke

“我之前看WebPack做到了”——Jerry兴致勃勃的谈了起来。

“他们采用的是文件摘要的方式,就是用MD5对文件求值,如果两个文件是相同的,那么就求得同一个hash值;如果文件是不同的,就求得不同的hash值”——Jerry

“我们可以用这些文件的hash值作为版本号,就像这样”——Jerry

“能不能通过文件名做版本管理,我希望知道哪些文件是这次部署要移除的,哪些是新增的”——Nibbles

“这有什么问题么?”——Spike很疑惑

“明年不是要做CDN么?静态资源和页面文件会放置到不同的服务器上,很难做到页面文件和静态资源同批次更新,而且CDN的资源生效是有延迟的”——Nibbles

(关于 CDN 和非覆盖部署式部署,请参考张云龙的大公司里怎样开发和部署前端代码?前端工程之CDN部署

“恩,那么就这样吧,我回去写Story。”——Spike 一锤定音。

“还好,我们之前用了WebPack,这就简单了”——Jerry

Spike写下了第三个story

作为IT 部门的老大:

我希望能用文件hash来命名静态资源文件,

使之可以按照文件来控制缓存和部署

“我觉得这回是最后一个Story了”——Spike越来越乐观。

过渡到非覆盖式部署——大圆满?

如何应用WebPack的具体过程不再概述。

图片来源大公司里怎样开发和部署前端代码?

这样,Nibbles就可以很愉快的通过文件名比对,来分析每次部署变更的内容;而Best Experience未来上线的流程也会变为:

  • 先将新增的静态资源文件发布到静态资源服务器上
  • 验证新的静态资源是否正确发布
  • 服务器暂时离线,替换 html 文件等
  • 删除无用的静态资源文件

“终于可以踏踏实实过圣诞节了”——Spike看着日历。

总结

Spike的总结

年底了,Spike在年终总结中写到:

以后在实施前端工程中,我们可以通过:

  • 配置永不过期的本地缓存——节约带宽,提升用户体验
  • 采用文件摘要作为缓存依据——更精确的缓存控制
  • 采用CDN——降低用户请求资源时解析DNS的延迟
  • 利用文件摘要作为文件名——实现非覆盖式的部署,降低down time

我的总结

我引用前端工程之CDN部署一文中对非覆盖式、缓存设计、CDN这些解决方案间的前因后果做的总结:

如果考虑到项目开发阶段,那么这将是更为复杂的软件工程问题。在这个问题域中,还需要囊括文件压缩、合并、打包、重命名、目录设置等问题。还好Gulp、Webpack、FIS、AMD、RequireJS这些工具及对应的插件能帮助到我们。WebPack提供了Hash、ChunkHash、ContentHash,与此同时,社区提供了MD5-Hash。

当然这些都是关于工具的话题了,这次我们主要谈的是工程。浅谈前端集成解决方案里提到了前端领域的8个技术元素与分类,挺有意思的。

再终——没有银弹

我们的Spike先生来到了2月的北京旅游,放个带色的图:

我们自强不吸

在机场,Spike还是接到了Tyke的电话,“老爹啊,WebPack那个文件摘要不准啊……..”

“您好,因为天气原因,去往####的飞机延误,请您耐心等候……..”

“…….”


更多精彩洞见,请关注微信公众号:思特沃克

Share