复习一下 Go 和 Web 的相关知识。
更新历史
- 2017.01.23: 完成初稿
Go 的类型系统没有层级。用户不需要在定义类型之间花费时间。Go 是垃圾回收型预研,为并发执行与通信提供了基本的支持。
Go 使用 package
来组织代码。main.main()
函数是每一个独立的可运行程序的入口点。Go 使用 UTF-8 字符串和标志符,所以天生支持多语言。
Go 的 if
允许声明一个变量,这个变量的作用域只能在该条件逻辑块内,如
1 | if x := dosomething(); x > 10 { |
Go 里面有两个保留的函数:init
函数(能够应用于所有的 package)和 main
函数(只能应用于 package main)。这两个函数在定义时不能有任何的参数和返回值。虽然一个 package
里面可以写任意多个 init
函数,但这无论是对于可读性还是以后的可维护性来说,都强烈建议用户在一个 package
中每个文件只写一个 init
函数。
引入包的操作有两个需要注意
1 | import ( |
第一种引入方式是把包命名成一个比较好记忆的名字。第二个使用 _
则是引入该包而不直接使用包里面的函数,主要是为了调用该包里的 init
函数。
如果匿名字段实现了一个方法,那么包含这个匿名字段的 struct
也能调用该方法。
interface
就是一组抽象方法的集合,它必须由其他非 interface
类型实现,而不能自我实现,Go 通过 interface 实现了鸭子类型:当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来像鸭子,那么这只鸟就可以被称为鸭子。
空 interface 有点类似于 C 中的 void*
类型。
fmt.Println
可以接受任意类型的数据,其源代码中可以看到:
1 | type Stringer interface { |
也就是说,任何实现了 String
方法的类型都能作为参数被 fmt.Println
调用。
如何知道 interface
变量里面保存了什么类型的数值?可以使用 comma-ok 语法,比如 value, ok = element.(T)
如果 element 确实是 T 类型,那么 ok 为 true,反之为 false。
另外一种方式是使用 switch
,比如:
1 | for index, element := range list { |
在 Go 中进行并行程序开发时要注意:不要通过共享来通信,而要通过通信来共享。
对于普通的上网过程,浏览器本身是一个客户端,输入 URL 时首先会去请求 DNS 服务器,通过 DNS 获取相应的域名对应的 IP,然后通过 IP 地址找到 IP 对应的服务器后,要求建立 TCP 连接,等待浏览器发送完 HTTP Request 包之后,服务器收到请求包才开始处理请求包,调用自身服务,返回 HTTP Response 包,客户端收到来自服务器的响应后开始渲染这个 Response 包里的 body,收到全部内容后,断开与服务器之间的 TCP 连接。
以下均是服务器端的几个概念:
- Request: 用户请求的信息,用来解析用户的请求信息,包括 post, get, cookie, url 等信息
- Response: 服务器需要反馈给客户端的信息
- Conn: 用户的每次请求链接
- Handler: 处理请求和生成返回信息的处理逻辑
http 包执行流程
- 创建 Listen Socket,监听指定的端口,等待客户端请求到来
- Listen Socket 接受客户端的请求,得到 Client Socket,接下来通过 Client Socket 与客户端通信
- 处理客户端的请求,首先从 Client Socket 读取 HTTP 请求的协议头,如果是 post 方法,还可能要读取客户端提交的数据,然后交给相应的 handler 处理请求,handler 处理完毕准备好客户端需要的数据,通过 Client Socket 写给客户端
开发 Web 的一个原则就是,不能信任用户输入的任何信息,所以验证和过滤用户的输入信息就变得非常重要。一般有两方面的数据验证,一个是在页面端的 js 验证,一个是在服务端验证。
要使表单能够上传文件,第一步是添加 form 的 enctype
属性,有如下三种情况:
application/x-www-form-urlencoded
发送前编码所有字符(默认)multipart/form-data
不对字符编码。在使用包含文件上传控件的表单时,必须使用该值text/plain
空格转换为+
,但不对特殊字符编码
Web 开发中一个很重要的议题就是如何做好用户整个浏览过程的控制,经典的解决方案是 cookie 和 session,cookie 是一种客户端机制,把用户数据保存在客户端,而 session 机制是一种服务端的机制,服务器使用一种类似于散列表的结构来保存信息,每一个网站访客都会被分配给一个唯一的标志符,即 sessionID,它的存放形式无非两种,要么经过 url 传递,要么保存在客户端的 cookies 里(当然也可以保存到数据库里,更安全,但是效率会下降)
cookie 是有时间限制的,根据生命期不同分成两种:会话 cookie 和持久 cookie。如果不设置过期时间,则表示这个 cookie 生命周期为凑够创建到浏览器关闭为止,只要关闭浏览器窗口,cookie 就消失了。这种生命期为浏览会话期的 cookie 被称为会话 cookie。会话 cookie 一般不保存在硬盘上而是保存在内存里。
如果设置了过期时间 setMaxAge(606024)
,浏览器就会把 cookie 保存到硬盘上,关闭后再次打开浏览器,这些 cookie 依然有效直到超过设定的过期时间。存储在硬盘上的 cookie 可以在不同的浏览器进程间共享,比如两个 IE 窗口。而对于保存在内存的 cookie,不同的浏览器有不同的处理方式。
session 机制本身并不复杂,然而实现和配置上的灵活性却使得具体情况复杂多变。这也要求我们不能把仅仅某一次的经验或者某一个浏览器,服务器的经验当做普适的。
session 的基本原理是由服务器为每个会话维护一份信息数据,客户端和服务端依靠一个全局唯一的标识来访问这份数据,以达到交互的目的。当用户访问 Web 应用时,服务端程序会随需要创建 session,这个过程可以概括为三个步骤:
- 生成全局唯一标志符 sessionid
- 开辟数据存储空间。一般会在内存中创建相应的数据结构,但这种情况下,系统一旦掉电,所有的会话数据就回丢失,如果是电子商务类网站,这将造成严重的后果。所以为了解决这类问题,你可以将会话数据写到文件里或存储在数据库中,这样虽然会增加 I/O 开销,但是可以实现某种程度的 session 持久化,也更有利于 session 的共享
- 将 session 的全局唯一标志符发送给客户端
这里最关键的是如何发送 sessionid,一般有两种方式:cookie 和 URL 重写。
- Cookie 方式中服务端通过设置 Set-cookie 头就可以将 session 的标志符传送到客户端,而客户端此后的每一次请求都会带上这个标志符,另外包含 session 信息的 cookie 会将失效时间设置为 0(会话 cookie),即浏览器进程有效时间。至于浏览器怎么处理这个 0,不同浏览器有不同方案,但差别都不会太大。
- URL 重写方式,就是在返回给用户的页面里的所有 URL 后面追加 sessionid,这样用户收到响应后会自动带上 sessionid,这种做法比较麻烦,但是如果客户端禁用了 cookie,这样方案是首选。
session 劫持是一种广泛存在的比较严重的安全威胁,在 session 技术中,客户端和服务端通过 session 的标志符来维护会话,但这个标志符很容易就能被嗅探到,从而被其他人利用,是中间人攻击的一种类型。
如何有效防止 session 劫持呢?其中一个解决方案就是 sessionID 的值只允许 cookie 设置,而不是通过 URL 重置方式设置,同时设置 cookie 的 httponly 为 true,这个属性是设置是否可通过客户端脚本访问这个设置的 cookie,可以防止这个 cookie 被 XSS 读取从而引起 session 劫持,也更难获取 sessionID。然后我们需要在每个请求里面加上隐藏 token,每次提交都需要认证,这样来进行防范。
还有一个解决方案是给 session 额外设置一个创建时间的值,一旦超过,则销毁并重新生成,在一定程度上可以防止 session 劫持的问题。
Unmarshal
解析的时候 XML 元素和字段怎么对应起来呢?首先会读取 struct tag,如果没有,那么就寻找对应字段名。必须注意的是解析的时候 tag、字段名、XML 元素都是大小写敏感的,所以必须一一对应字段。
现在的网络编程几乎都是用 Socket 来编程。Socket 起源于 Unix,而 Unix 基本哲学之一就是『一切皆文件』,都可以用『打开 open - 读写 write/read - 关闭 close』模式来操作。Socket 就是该模式的一个实现,网络的 Socket 数据传输是一种特殊的 I/O,Socket 也是一种文件描述符。Socket 具有一个类似于打开文件的函数调用 Socket()
,该函数返回一个整型的 Socket 描述符,随后的连接建立、数据传输等操作都是通过该 Socket 实现的。
常用的 Socket 类型有两种:流式 Socket(SOCK_STREAM
)和数据报式 Socket(SOCK_DGRAM
)。流式是一种面向连接的 Socket,针对于面向连接的 TCP 服务应用;数据报式 Socket 是一种无连接的 Socket,对应于无连接的 UDP 服务应用。
Socket 有两种:TCP Socket 和 UDP Socket,TCP 和 UDP 是协议,而要确定一个进程需要三元组,还要 IP 地址和端口。
WebSocket 是 HTML5 的重要特性,它实现了基于浏览器的远程 Socket,它使浏览器和服务器可以进行全双工通信。在 WebSocket 出现之前,为了实现即时通信,采用的技术都是『轮询』,这样会占用大量带宽。WebSocket 采用了一些特殊的报头,使得浏览器和服务器只需要做一个握手的动作,就可以在浏览器和服务器之间建立一条连接通道。且此连接会保持在活动状态,你可以使用 JavaScript 来向连接写入或从中接收数据,就像在使用一个常规的 TCP Socket 一样。
WebSocket 的协议颇为简单,在第一次握手通过以后,连接便建立成功,其后的通讯数据都是以 \x00
开头,以 \xFF
结尾。
REST 是一种架构风格,汲取了 WWW 的成功经验:无状态,以资源为中心,充分利用 HTTP 协议和 URI 协议,提供统一的接口定义,使得它作为一种设计 Web 服务的方法而变得流行。在某种意义上,通过强调 URI 和 HTTP 等早期 Internet 标准,REST 是对大型应用程序服务器时代之前的 Web 方式的回归。
RPC 就是想实现函数调用模式的网络化。客户端就像调用本地函数一样,然后客户端把这些参数打包之后通过网络传递到服务端,服务端解包到处理过程中执行,然后执行的结果反馈给客户端。
RPC(Remote Procedure Call Protocol) 远程过程调用协议,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。它假定某些传输协议的存在,如 TCP 或 UDP,以便为通信程序之间携带信息数据。通过它可以使函数调用模式网络化。在 OSI 网络通信模型中,RPC 跨越了传输层和应用层。RPC 使得开发包括网络分布式多程序在内的应用程序更加容易。
运行时,一次客户机对服务器的 RPC 调用,其内部操作大致有如下十步:
- 调用客户端句柄;执行传送参数
- 调用本地系统内核发送网络消息
- 消息传送到远程主机
- 服务器句柄得到消息并取得参数
- 执行远程过程
- 执行的过程将结果返回服务器句柄
- 服务器句柄返回结果,调用远程系统内核
- 消息传回本地主机
- 客户句柄由内核接收消息
- 客户接受句柄返回的数据
很多 Web 应用程序中的安全问题都是由于轻信了第三方提供的数据造成的。在使用第三方提供的数据,包括用户提供的数据时,首先检验这些数据的合法性非常重要,这个过程叫做过滤。
加密的本质就是扰乱数据,某些不可恢复的数据扰乱我们称为单向加密算法或者散列算法。另外还有一种双向加密方式,也就是可以对加密后的数据进行解密。
XSS 攻击:跨站脚本攻击(Cross-Site Scripting)是一种常见的 web 安全漏洞,它允许攻击者将恶意代码植入到提供给其他用户使用的页面中。不同于大说书攻击(一般只涉及攻击者和受害者),XSS 涉及到三方,即攻击者、客户端与 Web 应用。XSS 的攻击目标是为了盗取存储在客户端的 cookie 或者其他网站用于识别客户端身份的敏感信息。一旦获取到合法用户的信息后,攻击者甚至可以假冒合法用户与网站进行交互。
XSS 通常可以分为两大类:一类是存储型 XSS,主要出现在让用户输入数据,供其他浏览此页的用户进行查看的地方,包括留言、评论、博客日志和各类表单等。应用程序中查询数据,在页面中显示出来,攻击者在相关页面输入恶意的脚本数据后,用户浏览此类页面就可能受到攻击。这个流程简单可以描述为:恶意用户的 HTML 输入 Web 程序 - 进入数据库 - Web 程序 - 用户浏览器。另一类是反射型 XSS,主要做法是将脚本代码加入 URL 地址的请求参数里,请求参数进入程序后在页面直接输出,用户点击类似的恶意链接就可能受到攻击。
XSS 目前主要的手段和目的如下:
- 盗用 cookie,获取敏感信息
- 利用植入 Flash,通过 crossdomain 权限设置进一步获取更高权限;或者利用 Java 等得到类似的曹禺
- 利用 iframe, frame, XMLHttpRequest 或上述 Flash 等方式,以(被攻击者)用户的身份执行一些管理动作
- 利用可被攻击的域收到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动
- 在访问量极大的一些页面上的 XSS 可以攻击一些小型网站,实现 DDoS 攻击的效果
防治 SQL 注入的方法:
- 严格限制 Web 应用的数据库操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害
- 检查输入的数据是否具有所期望的数据格式,严格限制变量的类型
- 对进入数据库的特殊字符进行转义处理
- 所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中,即不要直接拼接 SQL 语句
- 在应用发布之前建议使用专业的 SQL 注入检测工具进行检测,及时修补发现的 SQL 注入漏洞
- 避免网站打印出 SQL 错误信息,比如类型错误、字段不匹配等,把代码里的 SQL 语句暴露出来,以防止攻击者利用这些错误信息进行 SQL 注入
目前用的最多的密码存储方案是将明文密码做单向哈希后存储,常用算法包括 SHA-256, SHA-1, MD5 等,可用 rainbow table 破解。
安全性比较好的网站,都会用一种『加盐』的方式来存储密码,就是常说的 salt,先将用户输入的密码进行一次 MD5(或其他哈希算法)加密,将得到的 MD5 值前后加上一些只有管理员自己知道的随机串,再进行一次 MD5 加密。
专家级方案是 scrypt
,由著名的 FreeBSD 黑客 Colin Percival 为其备份服务 Tarsnap 开发的。