序
关于HTTP与HTML的发明有个很有趣的插曲, 那就是首个万维网服务器与浏览器是在一台NeXTStep计算机上编写的, 在1997年, Apple收购了NeXTStep Computer并将NeXTStep作为mac OS的基础后来成为了iOS的基础.
URL
每个Web资源被称为统一资源标识符(Uniform Resource Identifier, URI) 其中包括 URL 和 URN, 现在几乎所有的URI都是URL.
URL 通用格式
://
:
@
:
/
;
?
#
几乎没有哪个URL中包含了所有这些组件, URL最重要的三个部分是 (scheme 方案), (host 主机), (path 路径)
比如说, 你想要获取URL
https://www.apple.com/index.html
那么URL包含以下三个部分:
-
https 是URL方案(scheme). 方案告诉客户端如何访问资源.
-
www.apple.com 是服务器的位置, 告知客户端资源位于何处.
-
/index.html 是资源路径, 说明了请求的是服务器上哪个指定文件资源.
构建网络架构URL时遵循服务版本化和服务定位器原则.
报文
所有的HTTP报文都可以分为两类, 请求报文(request message) 和 响应报文 (response message) 我们可以通过Chrome模拟请求得到请求报文和响应报文, 我们来简单的看一下首部中的一些简单的概念.
响应首部
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET,HEAD,PUT,POST,DELETE
Content-Type: application/json; charset=utf-8
Content-Length: 2180
Date: Mon, 31 Jul 2017 02:56:17 GMT
Connection: keep-alive
由上述响应首部, 我们可得知以下信息:
-
应用程序支持最高的HTTP版本号为1.1.
-
状态码200表示请求成功. 如为3XX表示重定向, 4XX表示客户端错误, 5XX表示服务器错误.
-
原因短语OK仅为显示, 并无实际含义.
-
Content-Type就是MIME Type, 用以区分传输资源, 例子中主体部分是字符集为utf-8的json数据.
-
Content-Length表示主体部分包含了2180字节的数据.
-
Date表示了服务器产生响应的日期.
-
Connection 连接类型为keep-alive.
-
Access-Control-Allow-Origin 服务器域名为http://localhost:3000.
-
Access-Control-Allow-Methods服务器实现的方法为: GET,HEAD,PUT,POST,DELETE.
请求首部
GET /api/J1/getJ1List HTTP/1.1
Host: localhost:3001
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Accept: application/json, text/plain, */*
Origin: http://localhost:3000
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1
Referer: http://localhost:3000/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.8
由上述请求首部, 我们可以得知以下信息:
-
使用了GET方法进行请求, 请求的路由为/api/J1/getJ1List, HTTP版本号为1.1
-
Host 提供了接受请求的服务器的主机名和端口号localhost:3001.
-
Connection 和响应首部信息对照.
-
Pragma 随报文传送指示的方式, 并不专用于缓存.
-
Cache-Control 用于随报文传送缓存指示.
-
Accept 接受的任意媒体类型, 和响应首部的Content-Type信息对照
-
Origin 当前访问域名, 与Access-Control-Allow-Origin信息对照
-
User-Agent 将发起请求的应用程序名称告知服务器.
-
Referer 提供了包含当前请求URI的文档的URL.
-
Accept-Encoding 告诉服务器能够发送哪些编码方式.
-
Accept-Language 告诉服务器能够发送哪些语言.
实体
具体来说, HTTP承载的实体需要满足以下条件.
-
可以被正确识别(通过Content-Type首部说明媒体格式, Content-Language首部说明语言), 以便浏览器和其他客户端能正确处理内容.
-
可以被正确地解包(通过Content-Length首部和Content-Encoding首部).
-
是最新的(通过实体验证码和缓存过期控制).
-
符合用户的需要(基于Accept系列的内容协商首部).
-
在网络上可以快速有效地传输(通过范围请求, 差异编码以及其他数据压缩方法).
-
完整到达, 未被篡改(通过传输编码首部和Content-MD5校验和首部).
HTTP / 1.1版定义了一下10个基本字体首部字段.
-
Content-Type 实体中所承载对象的类型.
-
Content-Length 所传送实体的长度或大小.
-
Content-Language 与所传送实体主体的长度或大小.
-
Content-Encoding 对象数据所做的任意变换 (比如, 压缩).
-
Content-Location 一个备用位置, 请求时可通过它获得对象.
-
Content-MD5 实体主体内容的校验和.
-
Last-Modified 所传输内容在服务器上创建或最后修改的日期时间.
-
Expires 实体数据将要失效的日期时间.
-
Allow 该资源所允许的各种请求方法, 例如, GET和HEAD.
-
ETag 这份文档特定实例的唯一验证码. ETag首部没有正式定义为实体首部, 但它对许多涉及实体的操作来说, 都是一个重要的首部.
-
Chahe-Control 指出应该如何缓存该文档, 和ETag首部类似, Chche-Control首部也没有正式定义为实体首部.
连接
世界上几乎所有的HTTP通信都是由TCP/IP承载的, HTTP要传送一条报文时, 会以流的形式将报文数据的内容通过一条打开的TCP链接按序传输, TCP收到数据流之后, 会将数据流砍成被称作段的小数据块, 并将段封装在IP分组中.
iOS URLSession
我们先来简单的看下iOS中如何使用HTTP网络, 使用系统的URLSession进行网络请求, 将请求方法设置为GET, 当然默认就是GET, 使用单例创建URLSession进行任务回调, URLSession是异步请求, dataTask默认是关闭状态, 需要手动开启dataTask.resume().
var request = URLRequest(url: URL(string: "http://localhost:3001/api/J1/getJ1List")!)
request.httpMethod = "GET"
let session = URLSession.shared
let dataTask = session.dataTask(with: request) { (data, response, err) in
if err != nil {
print(err.debugDescription)
} else {
let responseStr = String(data: data!, encoding: String.Encoding.utf8)
print(responseStr!)
print("mimeType: \(String(describing: response?.mimeType))")
}
if let response = response as? HTTPURLResponse {
print("statusCode: \(response.statusCode)")
for (tab, result) in response.allHeaderFields {
print("\(tab.description) - \(result)")
}
if response.statusCode == 200 {
print(response)
}
}
}
dataTask.resume()
结合上面的内容, 我们发送了一个GET请求到
http://localhost:3001/api/J1/getJ1List, 现在我们就会分析URL了, 方案是http, 主机为localhost, 端口号为3001, 路径为/api/J1/getJ1List
{ URL: http://localhost:3001/api/J1/getJ1List } { status code: 200, headers {
"Access-Control-Allow-Methods" = "GET,HEAD,PUT,POST,DELETE";
"Access-Control-Allow-Origin" = "*";
Connection = "keep-alive";
"Content-Length" = 2180;
"Content-Type" = "application/json; charset=utf-8";
Date = "Mon, 31 Jul 2017 07:29:52 GMT";
} }
返回的响应报文与Chrome中显示相同, 在iOS9之后系统推荐使用URLSeesion, 使用起来非常的方便快捷. 当然URLSession的功能不止于此, 若想深究请看官方文档, 在网络可达性方面使用系统Reachability框架.
缓存
HTTP为我们提供了几个用来对已缓存对象进行再验证的工具吗但最常用的是If-Modified-Since和If-None-Match首部. 将这个首部添加到GET请求中去, 就可以告诉服务器, 只有在缓存了对象的副本之后, 又对其进行了修改的情况下, 才发送此对象.
iOS NSURLRequestCachePolicy
typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy)
{
NSURLRequestUseProtocolCachePolicy = 0,
NSURLRequestReloadIgnoringLocalCacheData = 1,
NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, // Unimplemented
NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,
NSURLRequestReturnCacheDataElseLoad = 2,
NSURLRequestReturnCacheDataDontLoad = 3,
NSURLRequestReloadRevalidatingCacheData = 5, // Unimplemented
};
设置缓存策略会对应的添加请求首部Cache-Control等到URLRequest中.
缓存的处理步骤
-
接收: 缓存从网络中读取抵达的请求报文.
-
解析: 缓存对报文进行解析, 提取出URL和各种首部.
-
查询: 缓存查看是否有本地副本可用, 如果没有, 就获取一份副本 (并将其保存在本地).
-
新鲜度检测: 缓存查看已缓存副本是否足够新鲜, 如果不是, 就询问服务器是否有任何更新.
-
创建响应: 缓存会用新的首部和一缓存的主体来构建一条响应报文.
-
发送: 缓存通过网络将响应发回客户端.
-
日志: 缓存可选地创建一个日志文件条目来描述这个事务.
Cookie
可以笼统的将cookie分为两类: 会话cookie和持久cookie. 会话cookie是一种临时cookie, 它记录了用户访问站点时的设置和偏好. 用户退出浏览器时, 会话cookie就被删除了. 持久cookie的生存时间更长一些, 他们存储在硬盘上, 浏览器退出, 计算机重启时他们仍然存在, 通常会用持久cookie维护某个用户会周期性访问的站点的配置文件或登录名.
会话cookie和持久cookie之间唯一的区别就是它们的过期时间, 如果设置了Discard参数, 或者没有设置Expires或Max-Age参数来说明扩展的过期时间, 这个cookie就是一个会话cookie.
iOS HTTPCookie / HTTPCookieStorage
// 阻止应用保存`cookie`.
HTTPCookieStorage.shared.cookieAcceptPolicy = .never
// 从响应中获取`cookie`.
guard let url = URL(string: "http://localhost:3001/api/J1/getJ1List") else {
return
}
let request = URLRequest(url: url)
let session = URLSession.shared
let dataTask = session.dataTask(with: request) { (data, response, err) in
if err != nil {
print(err.debugDescription)
} else {
let responseStr = String(data: data!, encoding: String.Encoding.utf8)
print(responseStr!)
print("mimeType: \(String(describing: response?.mimeType))")
}
if let response = response as? HTTPURLResponse {
print("statusCode: \(response.statusCode)")
// get cookie from response
let cookies = HTTPCookie.cookies(withResponseHeaderFields: response.allHeaderFields as! [String : String], for: url)
for cookie in cookies {
print("Cookie: \(cookie)")
}
for (tab, result) in response.allHeaderFields {
print("\(tab.description) - \(result)")
}
if response.statusCode == 200 {
print(response)
}
}
}
dataTask.resume()
// 删除cookie.
func deleteCookie(cookieName:String, url:URL) {
let jar = HTTPCookieStorage.shared
guard let storedcookies = jar.cookies(for: url) else {
return
}
for cookie in storedcookies {
jar.deleteCookie(cookie)
}
}
// 创建cookie.
guard let url = URL(string: "http://localhost:3001/api/J1/getJ1List") else {
return
}
let properties = [HTTPCookiePropertyKey.name : "FOO",
HTTPCookiePropertyKey.value : "This is foo",
HTTPCookiePropertyKey.path : "/",
HTTPCookiePropertyKey.originURL : "url"]
guard let cookie = HTTPCookie.init(properties: properties) else {
return
}
var request = URLRequest(url: url)
var newCookies: [HTTPCookie] = [cookie]
var newHeaders = HTTPCookie.requestHeaderFields(with: newCookies)
request.allHTTPHeaderFields = newHeaders
let dataTask = session.dataTask(with: request) { (data, response, err) in {
...
}
}
dataTask.resume()
cookie是可以禁止的, 而且可以通过日志分析或其他方式来实现大部分跟踪记录, 所以cookie自身并不是很大的安全隐患. 实际上, 可以通过提供一个标准的审查方法在远程数据库中保存个人信息, 并将匿名cookie作为键值, 来降低客户端到服务器的敏感数据传输频率.
认证
认证就是要给出一些身份信息, 当出示像护照或驾照那样有照片的身份证件时, 就给出了一些证据, 说明你就是你所声称的那个人, 在自动取款机上输入PIN码, 或在计算机系统的对话框中输入了密码时, 也是在证明你就是你所声称的那个人.
HTTP提供了一个原生的质询 / 响应(challenge / response)框架, 简化了对用户的认证过程.
iOS URLProtectionSpace
_ = URLProtectionSpace(host: "localhost",
port: 3001, protocol: NSURLProtectionSpaceHTTP,
realm: "moblie",
authenticationMethod: NSURLAuthenticationMethodDefault)
最佳实践是使用URLProtectionSpace验证手机银行应用的用户与安全的银行服务器进行通信, 特别是在发出的请求会操纵后端数据时更是如此. URLProtectionSpace是要认证的服务器或域, 是多有进来的
URLAuthenticationChallenges的一个属性.
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
let defaultSpace = URLProtectionSpace(host: "localhost", port: 3001, protocol: NSURLProtectionSpaceHTTP, realm: "mobile", authenticationMethod: NSURLAuthenticationMethodDefault)
let trustSpace = URLProtectionSpace(host: "localhost", port: 3001, protocol: NSURLAuthenticationMethodDefault, realm: "mobile", authenticationMethod: NSURLAuthenticationMethodClientCertificate)
let validSpaces = [defaultSpace, trustSpace]
if !validSpaces.contains(challenge.protectionSpace) {
let msg = "We're unable to establish a secure connection. Please check your network connection and try again"
DispatchQueue.main.async {
let alert = UIAlertController(title: "Unsecure Connection", message: msg, preferredStyle: .alert)
alert .addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
challenge.sender?.cancel(challenge)
}
}
上述代码片段添加了额外的保护空间, 这位后端提供了一些灵活性. 当确定要支持的保护控件后, 请创建它们, 然后将它们添加到数组中以便与进来的认证挑战相比较. 实际上, 你应该定义有效的保护控件作为模型层的一部分, 这样就可以在所有网络中重用它们了, 如果认证挑战的保护控件与所有支持的空间不匹配, 那么你应该通知用户取消认证质询.
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic {
if challenge.previousFailureCount == 0 {
let creds = URLCredential(user: "Castie!", password: "******", persistence: URLCredential.Persistence.forSession)
challenge.sender?.use(creds, for: challenge)
} else {
challenge.sender?.cancel(challenge)
DispatchQueue.main.async {
let alert = UIAlertController(title: "Unsecure Connection", message: msg, preferredStyle: .alert)
alert .addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
}