Django异常处理

基于django技术栈实现的WEB应用,生产环境中都会关闭DEBUG选项,Django默认只会输出很少的错误信息,不利于开发人员快速定位、解决问题。为了解决此问题,考虑定制Django默认的错误处理,还原错误现场,配合错误日志、邮件报警快速发现、解决BUG。

本文档不包含错误日志、邮件报警等内容

Django异常内容调用栈帧

WSGI处理函数

django.core.handlers.base.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class BaseHandler(object):
# Changes that are always applied to a response (in this order).
... ...

def get_response(self, request):
"Returns an HttpResponse object for the given HttpRequest"
# Setup default url resolver for this thread, this code is outside
# the try/except so we don't get a spurious "unbound local
# variable" exception in the event an exception is raised before
# resolver is set
# 初始化路由解析器
urlconf = settings.ROOT_URLCONF
urlresolvers.set_urlconf(urlconf)
resolver = urlresolvers.RegexURLResolver(r'^/', urlconf)

try:
response = None
... ...
except: # Handle everything else.
# Get the exception info now, in case another exception is thrown later.
signals.got_request_exception.send(sender=self.__class__, request=request)
# 调用处理未捕获的异常方法
response = self.handle_uncaught_exception(request, resolver, sys.exc_info())
... ...
# 处理未捕获的异常
def handle_uncaught_exception(self, request, resolver, exc_info):
"""
Processing for any otherwise uncaught exceptions (those that will
generate HTTP 500 responses). Can be overridden by subclasses who want
customised 500 handling.
Be *very* careful when overriding this because the error could be
caused by anything, so assuming something like the database is always
available would be an error.
"""
if settings.DEBUG_PROPAGATE_EXCEPTIONS:
raise
logger.error('Internal Server Error: %s', request.path,
exc_info=exc_info,
extra={
'status_code': 500,
'request': request
}
)
if settings.DEBUG:
return debug.technical_500_response(request, *exc_info)
# If Http500 handler is not installed, re-raise last exception
if resolver.urlconf_module is None:
six.reraise(*exc_info)
# Return an HttpResponse that displays a friendly error message.
# 查找注册的错误处理函数
callback, param_dict = resolver.resolve_error_handler(500)
# 调用错误处理函数
return callback(request, **param_dict)
class WSGIHandler(base.BaseHandler):
def __call__(self, environ, start_response):
... ...
response = self.get_response(request)

URL路由解析

django.core.urlresolvers.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class RegexURLPattern(LocaleRegexProvider):
def __init__(self, regex, callback, default_args=None, name=None):
LocaleRegexProvider.__init__(self, regex)
# callback is either a string like 'foo.views.news.stories.story_detail'
# which represents the path to a module and a view function name, or a
# callable object (view).
if callable(callback):
self._callback = callback
else:
self._callback = None
self._callback_str = callback
self.default_args = default_args or {}
self.name = name

# 解析错误处理函数
def resolve_error_handler(self, view_type):
callback = getattr(self.urlconf_module, 'handler%s' % view_type, None)
if not callback:
# No handler specified in file; use default
# Lazy import, since django.urls imports this file
from django.conf import urls
callback = getattr(urls, 'handler%s' % view_type)
return get_callable(callback), {}

注册Django默认错误处理函数

django.confs.urls.__init__.py

1
2
3
4
handler400 = 'django.views.defaults.bad_request'
handler403 = 'django.views.defaults.permission_denied'
handler404 = 'django.views.defaults.page_not_found'
handler500 = 'django.views.defaults.server_error'

定义Django默认错误处理函数

django.views.defaults.py

1
2
3
4
5
6
7
8
9
10
11
12
@requires_csrf_token
def server_error(request, template_name='500.html'):
"""
500 error handler.
Templates: :template:`500.html`
Context: None
"""
try:
template = loader.get_template(template_name)
except TemplateDoesNotExist:
return http.HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html')
return http.HttpResponseServerError(template.render())

定制Django500异常处理

定制WSGIHandler

project/wsgi.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class CustomWSGIHandler(WSGIHandler):
"""
定制WSGIHandler
"""
def handle_uncaught_exception(self, request, resolver, exc_info):
"""
重载夫类异常处理函数
"""
_logger = logging.getLogger('app.wsgi')
if settings.DEBUG_PROPAGATE_EXCEPTIONS:
raise
_logger.error('Internal Server Error: %s', request.path,
exc_info=exc_info,
extra={
'status_code': 500,
'request': request
}
)
# 不再返回500的异常HTML报文
# if settings.DEBUG:
# return debug.technical_500_response(request, *exc_info)
# If Http500 handler is not installed, re-raise last exception
if resolver.urlconf_module is None:
six.reraise(*exc_info)
# Return an HttpResponse that displays a friendly error message.
callback, param_dict = resolver.resolve_error_handler(500)
return callback(request, **param_dict)
def get_wsgi_application():
django.setup()
return CustomWSGIHandler()

定制500错误处理函数

project/error_handlers.py

1
2
3
4
5
6
7
@requires_csrf_token
def server_error(request, template_name='500.html'):
"""
定制500异常函数
"""
_logger.error('server_error.')
return http.HttpResponseServerError('Server Error')

注册500错误处理函数

protject/urls.py

1
handler500 = 'error_handlers.server_error'

注册ROOT_URLCONF

project/settings.py

1
ROOT_URLCONF = 'project.urls'

参考

MacOS安装PIL

MacOS安装PIL库

安装libjpeg

1
2
3
4
5
6
7
curl -O http://www.ijg.org/files/jpegsrc.v8c.tar.gz
tar -xvzf jpegsrc.v8c.tar.gz
cd jpeg-8c
./configure
make
sudo make install
cd ../

安装freetype

1
2
3
4
5
6
7
curl -O http://ftp.igh.cnrs.fr/pub/nongnu/freetype/freetype-2.4.5.tar.gz
tar -xvzf freetype-2.4.5.tar.gz
cd freetype-2.4.5
./configure
make
sudo make install
cd ../

安装PIL

1
2
3
4
5
6
curl -O -L http://effbot.org/media/downloads/Imaging-1.1.7.tar.gz
# extract
tar -xzf Imaging-1.1.7.tar.gz
cd Imaging-1.1.7
python setup.py install
cd ..

参考

SQL

MySQL数据库远程访问

1
2
GRANT ALL PRIVILEGES ON *.* TO 'USERNAME'@'%' IDENTIFIED BY 'PASSWORD' WITH GRANT OPTION;
FLUSH PRIVILEGES;

创建数据库

1
CREATE DATABASE database_name CHARACTER SET utf8;

导出数据库表

1
2
3
4
5
mysqldump -h127.0.0.1 -uusername -ppassword
database_name table_name > database_name.table_name.sql;

mysqldump -h127.0.0.1 -uusername -ppassword
database_name table_name --where="id>100" > database_name.table_name.sql;

导出数据库表结构

1
mysqldump  -hhost -P3306 -uusername -ppassword  database_name --no-data > db_name_schema.sql

导入数据库

1
2
3
mysql -h127.0.0.1 -uusername -ppassword database_name < database_name.table_name.sql;
mysql -uusername -ppassword;
> source sql_file_path;

数据从一张表导出到另一个张表

1
2
3
4
5
6
INSERT INTO database_name.table_name
(field1,
field2)
SELECT field1,
field2
FROM database_name.table_name;

根据一张表更新另一张表

1
2
3
4
UPDATE kdreader.library_book
INNER JOIN store_book
set kdreader.library_book.author_id=store_book.author_id
WHERE kdreader.library_book.name=store_book.titl;

修改表结构:增加Column

1
ALTER TABLE table_name_example ADD COLUMN field_name field_type DEFAULT default_value;

修改表结构:更新Column

1
ALTER TABLE table_name_example ALTER COLUMN field_name field_type DEFAULT default_value;

修改表结构:删除Column

1
ALTER TABLE table_name_example DROP COLUMN field_name;

修改表结构:删除unique约束

1
ALTER TABLE table_name_example DROP INDEX unique_key_name;

表重命名

1
ALTER TABLE old_name RENAME TO new_name;

增加Unique约束

1
ALTER TABLE Persons ADD CONSTRAINT uni_PersonID UNIQUE (Id_P,LastName);

分组查询

1
2
3
4
5
6
SELECT book_labels.label_id,
Count(book_labels.book_id) cnt
FROM store_book_labels AS book_labels
GROUP BY book_labels.label_id
ORDER BY cnt DESC
LIMIT 20;

Reference

安装Node.js

Linux

1
2
curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -;
sudo apt-get install -y nodejs;

Mac OSX

1
2
curl "https://nodejs.org/dist/latest/node-${VERSION:-$(wget -qO- https://nodejs.org/dist/latest/ | sed -nE 's|.*>node-(.*)\.pkg</a>.*|\1|p')}.pkg" > "$HOME/Downloads/node-latest.pkg" && sudo installer -store -pkg "$HOME/Downloads/node-latest.pkg" -target "/";
brew install node;

参考

旅行,写作,编程

花了10个月的时间做世界环游,途经非洲,东南亚,澳洲,中南美洲里的17个国家和地区。这次旅行的主题就是冲浪和摄影。
出席在香港,日本,美国和伦敦举行的会议
启程时给O’Reilly出版公司写了一本书,书名叫做《JavaScript Web Applications》
另外写了一本关于CoffeeScript的书,很快就会由O’Reilly公司出版。
写了大量的开源库,例如Spine, Spine.Mobile, GFX, 和 Juggernaut.
筹划了一个创业公司的框架
出席伦敦2011FOWA会议
最后,我在Twitter公司找到了一份工作
那么,让我从一年前开始,那是2010年9月,我刚好从一个我合作创办的公司里出来,尽管这段经历是很有价值的,但无休无止的长时间苦干让我精疲力 尽。我回到了英格兰,需要对未来做一些思考。我一直有一个梦想——移居美国(几年就好),所以,我在Google记事本上写了下面的话:

人生的选择:
去纽约哥伦比亚大学深造
坏处 - 非常昂贵,并不一定能学到什么真正有用的东西,无聊?
好处 - 那是一个纽约的大学!
写一本书,申请 01 签证
坏处 - 需要大量的时间,有风险
好处 - 对事业有好处,有趣
等待。去纽约度一次假(3个月)。等待创业签证。
很容易 - 不是那么有趣

也许选第二个,不行就选3?
最终我选择了2,我已经对JavaScript web应用研究了很久,我要写一本这方面的书,为什么不边做环游世界的旅行、边写书呢?这也是我一个梦想呀。我从oneworld买了一份环游世界的机票(比你们想象的要便宜),决定下周去我的第一站,南非。

如果你从来没有到过非洲,你应该去一次。那里的景色原始而美丽,对那些没有体验过这种景色的人,你很难用言语描绘明白。几年前我就喜欢上了南 方,那时我在东海岸做了一个为期3个月的冲浪旅行。这次,我只有一个月的时间,穿越特兰斯凯,从开普敦到德班。当我在南非旅行时,我的写作也开始了,把早 期向O’Reilly提交的书的框架里的数章填充了材料。

特兰斯凯是南非非常具有乡野特色的地方,到处是连绵的小山,一些小村庄和土堆的茅屋。他们仍然沿袭着酋长制度,有一个首领,大多数的当地人靠捕 鱼为生。我们在高低不平的土路上颠了两天才到达我心仪的地方,一个美丽的海湾,叫做咖啡湾(Coffee Bay)。在那里,我休整了一下,从网上下载了一些相关资料,为更远的海湾远征做准备。

我还清晰的记得我们走了数里地来到那个未开垦的海滩,我们从那些一个个被黄沙和小丘孤立的村庄穿行而过。有一个地方,我们要过一条大河,我们需 要游过去,我把背包举过头顶,以免里面的相机和iPod遇到水。非洲是一个让你脱离尘世的地方,解放你的思想,重新认识人生最重要的东西是什么。

下一站是香港,在那里,我度过了我的21岁生日,接着,我从陆路由新加坡到越南河内。很多人不相信香港70%的面积由自然公园覆盖,我徒步走了几条精彩的景观路线,非常的精彩壮观,比如:香港龙脊。有几天,我在boot.hk这个网站上闲逛,这是一个协作工作的网站,我顺便教了一个同行的游客如何使用ruby。然后,到了夜里,我跟Soho里的一些冲浪爱好者狂欢到凌晨。

从泰国到柬埔寨到越南是我这次旅行中做喜欢的部分,如果你从没有到过亚洲,你绝对应该去一次。这些国家非常的漂亮,气候非常的好,食物美味可口,人们非常友善。吴哥窟是世上最神奇的地方之一,每个人都应该去看看。是Trey Ratcliff的照片把我吸引到了那里,我的很多其它旅游目的地也是受了他的影响。那个家伙是很多旅游地的第一宣传者。

在一些无名的小博客中,我听有人说过一个很远的美丽的小岛,在柬埔寨的海边。说小岛的Sihanoukville这个地方有个酒吧,说只能坐小 渔船到那里。我,还有几个非常好的朋友,乘坐晚上的大巴,开始寻找这个传说中的酒吧。搜索差不多进行了一整天,每一个问过的酒吧都把我们指向另外一个酒 吧。最终,我们问了出来,并在第二天早晨做短程巴士去了那个地方。

上面的照片上是海岸边一个10美元一晚的小木屋。从当地居民区离开后,我们的队伍像小岛上唯一的人,我们随性自由的奔跑。白天我们懒懒的躺在海滩上,吃着岛上厨师准备的鲜美可口的水果沙拉,在夜晚,我们在到处是浮游生物的海里游泳。

下一站是越南,我们沿着湄公河支流来到一个边界上的小镇,我们是这里唯一的西方人,交流成了最大的问题。幸运的是,我们发现一个也许是镇上唯一会说英语的人,他骑车当我们的向导。当我的信用卡被那里的一个自动取款机吞掉了后,他提供了我很大的帮助!

我们的队伍分成了几路,在我到达越南时,我的书正在按计划完成,进行的非常顺利。此时,我在西贡多待了几周,让我在书的好几章上有了重大的进展,正好是中国旧历新年,气氛非常的壮观热闹。

接着是日本,澳大利亚,新西兰和夏威夷。我很难把我所有的感受都在这篇文章里写出来,但说这是此生难忘的一段历程是不为过的。把如此多的美景都 放到一个国家里,太让人赞叹了,我说的正是新西兰。我最喜爱的一段记忆是沿着Wanaka的一个湖边在阳光下跑步,还有就是背着食物和生活用品,徒步数天 穿越Routeburn的大山。在这个国家的旅途中,我结识了好几个值得一生相伴的好友。这是一个真正的天堂。

就在我环绕新西兰的南部岛屿时,我的书终于完成了,提交给了技术编辑校对。

接下来是纽约和旧金山,这两个神奇的地方到处是天才的程序员,有些人我很幸运的认识。Techcrunch Disrupt办的很精彩(我高度推荐hackathon)。

在从纽约到旧金山的中途停留期间,我在各种公司了进行了不少的求职面试,最终在Twitter公司找到了一份做前端开发的工作。要在那里和杰出的团队一起工作,我不能不高兴的颤抖,而去旧金山,同样也是我此生的一个梦想。

当签证的事办下来了后,我去了中、南美洲旅行,同时开发了我的一个小工程:一个JavaScript MVC框架库,叫做Spine。我到了哥斯达黎加,巴拿马,秘鲁,Bolvia,和阿根廷。 秘鲁是我的最爱,尽管那里的海拔给我带来了不少麻烦,我大部分的时间都在探险。下面的图片是哥斯达黎加传说中神奇猎鹰,是在我爬下世界最深的峡谷时拍到的。

当我在哥斯达黎加时,微博上有个叫Roberto的家伙给我发了条信息,说他读了我的书,问我是否有兴趣一起冲浪。我欣然同意,坐上去圣何塞的 汽车,在几天后和他会了面。那天我们一起在他海边的公寓里开发Spine和Ruby项目,使用移动硬盘,用汽车电源给笔记本充电。当电量不足后,让太阳能 板补充能量,我们去冲浪。

我推荐大家去写一本书,特别是边旅游边写书。可以想象,如果我不去旧金山去看一看,我可能还在旅途中,做顾问,去创业。当作家并不能让你直接的 挣到很多钱,但它绝对能提升你的身份地位,给你带来很多潜在的机会。事实上,写作过程让我真正享受的是,我可以认真深入的研究一个题目。

这一年是我这辈子目前为止最好的一年,而我感觉今后的一年会更好。当我如今定居下来后,我并没有感觉旅行对我的吸引力减少了;我始终把签证放到一个口袋里,而另一个口袋里装着钱包,当召唤降临,随时准备离开。

可是,这篇文章并不是关于我的旅行,它是要发送一个信号:

对于程序员来说,有个得天独厚的条件,就是这种职业可以远程工作或边旅游边工作,这是其它职业办不到的。当然,也不都是这样,在我的旅途中,我 没有碰到第二个跟我的做法相似的程序员。这种情况让人悲哀。我想向程序员们送出的信息是,不要再找借口了,行动起来,你可以做到。一个人只有一生,我可以 向你保证,这样的生活才不枉世间走这一遭。

就像我,我感到极度的幸运,能这样的生活,去发现我的热情所在,去做每天我喜欢做的事情。你可以看出,大部分我现在的境遇并非偶然或侥幸,这是计划,追求,工作的结果。

一份汗水,一份收成。

这篇文章的目标不是做一些自我陶醉似的炫耀和大话,而是向大家演示如何立下目标,鼓励大家去做相似的事情。想清楚你现在的处境,这一年内你想得到什么,制定出一系列具体的能让你到达这些目标的步骤。追随你的梦想。

原文地址:https://www.oschina.net/news/23952/traveling_writing_programming

Django QuerySet

字段查找过滤

操作符 含义
__exact 精确等于 like ‘aaa’
__iexact 精确等于 忽略大小写 ilike ‘aaa’
__contains 包含 like ‘%aaa%’
__icontains 包含 忽略大小写 ilike ‘%aaa%’,但是对于sqlite来说,contains的作用效果等同于icontains。
__gt 大于
__gte 大于等于
__lt 小于
__lte 小于等于
__in 存在于一个list范围内
__startswith 以…开头
__istartswith 以…开头 忽略大小写
__endswith 以…结尾
__iendswith 以…结尾,忽略大小写
__range 在…范围内
__year 日期字段的年份
__month 日期字段的月份
__day 日期字段的日

Reference

当我跑步时,我谈些什么

在肉体上时痛苦的,在精神上,令人沮丧的局面有时也会出现。不过,“痛苦”对于这一运动,乃是前提条件的东西。不伴随着痛苦,还有谁来挑战铁人三项赛事和全程马拉松这种费时耗力的运动呢?正因为痛苦,正因为刻意经历者这种痛苦,我才从这个过程中发现自己还活着的感觉,至少是发现了一部分。我现在意识到:生存的质量并非成绩、数字、名次之类固定的东西,而是含于运动中流动性的东西。

HTTP缓存

缓存是某个资源(或文档)的副本。对于私有缓存,可以服务同一个客户端的对同一个资源多次的请求,比如浏览器缓存。对于共享缓存(或代理缓存),可以服务多个客户端对于同一个资源的请求,比如CDN。为什么需要缓存,缓存有哪些优点:

  • 减少网络延迟,更快的加载内容,提高用户体验
  • 减少冗余数据传输,降低网络负载
  • 降低服务器负载,服务器可以更快的处理请求

不同类型的缓存

  • 私有缓存 - 服务于单个用户
  • 共享缓存 -存储响应被很多用户复用

其他缓存,gateway cache, CDN, reverse proxy cache 和 部署在服务器上的负载均衡器,以获得更好的可靠性和性能.

HTTP Cache TypeHTTP Cache Type

私有浏览器缓存(Private browser cache)

私有缓存服务于单个用户.

共享代理缓存(Shared proxy caches)

共享缓存服务于多个用户.比如ISP或者企业可以配置一个web代理,作为本地网络基础架构,服务于多个用户.热点资源可以被多个用户复用,减少网络流量和延迟. 

常用的缓存条目的形式:

  • 成功的检索请求结果 - 200 OK响应,比如HTML 文档,图片或者文件
  • 永久重定向 - 301(Moved Permanently)响应
  • 错误响应 - 404(Not Found)结果页
  • 未完成的结果 - 206(Partial Content)响应
  • 不仅仅是GET请求的响应 - 定义一个合适的cache key

缓存处理步骤

对于一个HTTP GET请求报文,基本缓存处理包含7个步骤:

  1. 接收-缓存从网络中读取请求报文
  2. 解析-缓存对报文进行解析,提取HTTP首部信息
  3. 查询-缓存查询是否命中,如果没有,则从源服务器获取,并缓存到本地
  4. 新鲜度检测-检查副本是否新鲜,如果不新鲜,则与源服务器进行验证
  5. 响应-缓存用新的首部和缓存主体创建响应报文
  6. 发送-缓存通过网络将响应发送给客户端
  7. 日志-缓存记录这次请求的日志

缓存处理流程图:
HTTP_CACHE_GET_FLOW_CHARTHTTP_CACHE_GET_FLOW_CHART

缓存控制

服务器可以通过HTTP定义文档过期之前可以将其缓存多长时间。

  • Cache-Control: no-store
  • Cache-Control: no-cache
  • Cache-Control: must-revalidate
  • Cache-Control: max-age
  • Expires

no-store与no-cache

HTTP/1.1提供了几种限制对象缓存方式。 no-store和no-cache首部可以防止缓存未经证实的已缓存对象:

1
2
3
Pragma: no-cache
Cache-Control: no-store
Cache-Control: no-cache

标识为no-store的响应回禁止缓存对响应进行复制。标识为no-cache的响应可以在本地缓存中,只是在与服务器进行新鲜度再验证之前,缓存不能提供给客户端使用。

Cache-Control: max-age=3600表示收到服务器响应文档处于新鲜状态的秒数。max-age=0表示不缓存文档。

Expires: Fri, 05 Jul 2017, 12:00:00 GMT表示文档绝对过期时间。不推荐使用Expires,HTTP设计者后来任务,由于服务器之间的时间不同步或不正确,会导致文档新鲜度计算错误。

如果源服务器希望缓存严格遵守过期时间,可以在加Cache-Control: must-revalidate的HTTP首部。Cache-Control: must-revalidate响应告诉缓存,在事先没有跟源服务器再验证之前,不能提供这个对象的过期副本。缓存仍然可以提供新鲜的副本。如果缓存进行must-revalidate新鲜度是,源服务器不可用,缓存必须返回一条Gateway Timeout从错误。

缓存命中

缓存命中、未命中和再验证:
HTTP Cache HitHTTP Cache Hit

缓存命中率

由缓存缓存提供服务的请求所占的比例成为称为缓存命中率(cache hit rate)。命中率在0到1之间,0表示缓存全部未命中,1表示缓存全部命中。缓存服务提供者希望缓存的命中是100%,而实际的缓存命中率与缓存大小,缓存内容变化,请求者兴趣相似度等因素相关。

缓存新鲜度

文档过期

就像牛奶过期一样,文档也有过期时间。

1
2
3
HTTP/1.1 200 OK
Content-Type: text/plain
Cache-Control: max-age=484200
1
2
3
HTTP/1.1 200 OK
Content-Type: text/plain
Expires: Fri, 05 Jul 2017, 12:00:00 GMT

Cache-Control: max-age=484200是一个相对过期时间,max-age定义了文档的最大使用期,从第一次生成文档到文档不再新鲜为止,以秒为单位。

Expires:Fri, 05 Jul 2017, 12:00:00 GMT是一个绝对过期时间,如果过期时间已经过了,则文档不再新鲜。该首部要时钟同步

服务器再验证

缓存文档过期并不意味着该副本与服务器文档不一致,只是意味着要与源服务器进行再验证。

  • 如果再验证内容发生了变化,缓存获取新的副本,替换过期副本
  • 如果再验证内容没有发生变化,缓存只需要获取新的首部,对缓存的副本的首部进行更新

条件再验证HTTP首部:

Header Description
If-Modify-Since: 如果从指定日期之后文档被修改过了,就执行该请求。可以与Last-Modified服务器响应首部配合使用,只有在内容被修改后,才去获取新的内容
If-None-Match: 服务器可以为文档提供特殊的标签,而不是将其与最近修改时间相匹配,这些标签就像序列号一样

If-Modify-Since: Date再验证:

  • 如果自指定日期后,文档被修改了,If-Modify-Since条件为真,源服务器会返回成功的响应,包含新的过期首部和新文档实体
  • 如果自指定日期后,文档未被修改,If-Modify-Since条件为假,源服务器会返回一个304 Not Modified的响应,不包含文档实体内容

img

If-None-Match: Tags

有些情况下,仅使用最后修改时间是不够的。

  • 文档被周期性的重写,最后修改时间发生变化,而内容未改变
  • 服务器无法判定最后修改时间

HTTP允许用户对实体打标签,进行标识。

img

什么时候使用最后修改时间和标签验证?

如果服务器返回了ETag首部,客户端必须使用标签验证。如果服务器只返回了Last-Modified首部,客户端可以使用最后修改时间验证。

新鲜度计算算法

为了分辨文档是否新鲜,需要计算两个值,文档的使用期(age)和文档的新鲜生存期(freshness lifetime)。如果文档使用期小于文档新鲜生存期,则文档是新鲜的。

1
is_fresh_enough = True if age < freshness_lifetime

使用期

使用期包含了网络传输时间和文档在缓存的停留时间。

1
2
3
4
5
6
7
apparent_time = time_got_response - date_header_value
corrent_apparent_time = max(0, apparent_time)
age_when_document_arrived_at_our_cache = corrent_apparent_time

how_long_copy_has_been_in_our_cache = current_time - got_response_time

age = age_when_document_arrived_at_our_cache + how_long_copy_has_been_in_our_cache

基于Date首部计算apparent使用期

apparen时间:

apparent时间等于获得响应时间减去服务器发送文档时间:

1
apparent_time = time_got_response - date_header_value

为了防止由于服务器时间不同步导致apparent_time为负,进行时间修正:

1
2
apparent_time = max(0, apparent_time)
age_when_document_arrived_at_our_cache = apparent_time

对网络时延对补偿:

如果文档在网络或服务器中阻塞了很长时间,相对使用期的计算可能会极大的低估文档使用期。缓存知道文档请求时间,以及文档到达时间。HTTP/1.1会在这些网络延迟上加上整个网络时延,一遍对其进行保守校正。这个从缓存到服务器到缓存高估了服务器到缓存延迟,它是保守的。如果出错,只会使文档看起来比实际使用期要老,并会引发不必要的验证。

1
2
response_delay_estimate = time_got_response - time_issued_request
age_when_document_arrived_at_our_cache = apparent_time + response_delay_estimate

Note:该时延补偿会导致最后计算文档使用期大于实际的文档使用期。apparent_time是包含网络时延的,对网络时延补偿是否必要?在服务器负载较高,对服务器的处理时间进行补偿倒是很有必要。

缓存停留时间:

1
how_long_copy_has_been_in_our_cache = current_time - got_response_time

img

新鲜生存期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def calculate_server_freshness_limit(**kwargs):
heuristic = False
server_freshness_limit = default_cache_min_lifetime
if max_age_value_set:
server_freshness_limit = max_age_value_set
elif expires_value_set:
server_freshness_limit = expires_value_set - date_value
elif last_modified_value_set:
time_since_last_modify = max(0, date_value - last_modified_value)
server_freshness_limit = int(time_since_last_modify*lm_factor)
heuristic = True
else:
server_freshness_limit = default_cache_min_lifetime
heuristic = True

if heuristic:
if server_freshness_limit > default_cache_max_lifetime:
server_freshness_limit = default_cache_max_lifetime
if server_freshness_limit < default_cache_min_lifetime:
server_freshness_limit = default_cache_min_lifetime

return server_freshness_limit

LM-factor算法计算新鲜周期

  • 如果已缓存文档最后一次修改发生在很久以前,它可能是一份稳定的文档,不会突然发生变化,因此将其汲取保存在缓存中比较安全
  • 如果已缓存的文档最近被修改过,就说明它很可能会频繁发生变化,因此在与服务器再验证之前,只应该将其缓存很短一段时间
1
2
time_since_modify = max(0, date_value - server_last_modified)
server_freshness_limit = time_since_modify * lm_factor

img

其他

缓存相关的HTTP头部:

Header Description
Cache-Control 缓存控制
Expires 过期绝对时间
If-Modify-Since 从某个时间开始文档是否发生改变
If-None-Match 文档的标签是否发生改变
Last-Modified 最后修改时间
ETag 文档标签

参考

OAuth

OAuth客户端使用一个访问令牌(access token)来访问受保护的资源,其中访问令牌包含了特殊作用域(specific scope), 生命周期,和其他的访问属性。访问令牌由授权服务器在资源拥有者授权之后颁发(issue)。客户端通过访问令牌资源服务器上受保护资源。

比如,一个终端用户(resource-owner)可以授权打印服务器(client)访问他的存储在图片分享服务(resource server)上的受保护的图片,但不用与打印服务器分享他的用户名和密码。他可以授权图片分享服务(authorization server)颁发一个特殊的证书(access token).

Introduction

Roles

Name Description
resource owner 一个能授权访问受保护资源的实体
resource server 托管受保护资源的服务器,能处理通过访问令牌访问受保护资源的请求
client 代表资源拥有者发起访问受保护资源的请求的应用程序
authorization server 在资源拥有者认证、授权之后,能够颁发访问令牌给客户端的服务器

Protocol Flow

Authorization Grant

授权grant是一个证书表示资源拥有者已经授权,客户端可以用授权grant获取访问令牌。该规范定义4种授权类型:

  • authorization code
  • implicit
  • resource owner password credentials
  • client credentials

Access Token

访问令牌是一种证书用来访问受保护的资源。一个访问令牌就是认证服务器颁发给客户端的字符串。令牌包含了特殊作用域,过期时间,授权的资源拥有者等信息。

Refresh Token

刷新令牌也是一个证书用来获取访问令牌。刷新令牌是认证服务器发放给客户端的,在当访问令牌不可用或过期时,用来获取新的访问令牌。

刷新令牌是资源资源拥有者授权给客户端认证码。与访问令牌不同,刷新令牌只能与认证服务器通信,不能从资源服务器获取资源。

Refreshing an Expired Access TokenRefreshing an Expired Access Token

Client Registration

在使用OAuth协议之前,客户端需要在认证服务器注册应用。

客户端注册是让认证服务器能够识别、信任客户端,其中包括客户端类型,重定向URI等。

Client Types

OAuth定义了两种客户端类型

  • confidential - 客户端能够管理证书的机密性
  • public - 客户端不能管理证书的机密性

Client Identifier

认证服务器给已注册的客户端颁发一个客户端标识。对于认证服务器,客户端标识是唯一的。

Client Authentication

如果客户端类型是confidential, 客户端和认证服务器建立一个客户端认证方法。Confidential客户端会向认证服务器声明它支持证书,比如密码或公私钥对。

Obtaining Authorization

在请求访问令牌之前,客户端先要获取资源拥有者的授权。OAuth定义了4种授权类型: authorization code, implicit, resource owner password credential, 和client credentials.

Authorization Code Grant

授权码授权类型用来获取访问令牌和刷新令牌。

认证码获取流程:

Authorization Request

Content-Type: application/x-www-form-urlencoded

Parameter Description
response_type REQUIRED该值必须设置为”code”
client_id REQUIRED注册的应用的id
redirect_uri OPTIONAL重定向URI,成功获取授权码后重定向到oauth客户端的URI
scope OPTIONAL作用域
state RECOMMENDED客户端用来管理请求、回调状态的值。认证服务器的重定向到UA时,会包含这个值。这个参数应该用来防止扩展请求伪造
1
2
3
4
5
6
7
8
GET /o/authorize/?response_type=code&client_id=wt6Pvm2s3vbb8RPE7nlPlugwaMnj58UhFpk8bCPp&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Foauth%2Fauthorized%2F HTTP/1.0
Host: localhost:8000
Connection: close
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.98 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8

认证服务器会验证这个请求,保证请求中的所有参数都是有效的。如果请求是有效的,认证服务器需要资源拥有者授权。如果资源拥有者授权成功,授权服务器根据redirect_uri返回给UA一个重定向响应。

Authorization Response

Content-Type: application/x-www-form-urlencoded

Parameter Description
code REQUIRED认证服务器生成的授权码,为了防止泄露的危险,授权码必须在很短的时间过期。授权码生存期推荐为10min.客户端只能使用一次授权码。授权码与客户端id,重定向URI绑定
state REQUIRED如果客户端请求中包含state,授权服务器必须要返回该值
1
2
3
4
5
6
7
8
HTTP/1.0 302 Found
Date: Tue, 18 Jul 2017 04:12:00 GMT
Server: WSGIServer/0.1 Python/2.7.9
X-Frame-Options: SAMEORIGIN
Access-Control-Allow-Origin: *
Content-Type: text/html; charset=utf-8
Location: http://localhost:8000/oauth/authorized/?code=x1yxsYhAJ23pXNMXej4tB13dvMFTov
Vary: Cookie

Error Response

Conent-Type: application/x-www-form-urlencoded

Parameter Description
error REQUIRED
error_description OPTIONAL
error_uri OPTIONAL
state REQUIRED
1
2
HTTP/1.1 302 Found
Location: https://client.example.com/cb?error=access_denied&state=xyz

Access Token Request

Conent-Type: application/x-www-form-urlencoded

Parameter Description
grant_type REQUIRED该值必须设置“authorization_code”
code REQUIRED授权服务器返回的code
redirect_uri REQUIRED重定向URI
client_id REQUIRED注册应用的ID
1
2
3
4
5
6
7
8
9
10
POST /o/token/ HTTP/1.0
Host: localhost:8000
Connection: close
Content-Length: 183
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: python-requests/2.18.1
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=x1yxsYhAJ23pXNMXej4tB13dvMFTov&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Foauth%2Fauthorized%2F&client_id=wt6Pvm2s3vbb8RPE7nlPlugwaMnj58UhFpk8bCPp

认证服务器必须要验证客户端的请求

  • 对于confidential客户端,需要客户端认证
  • 如果客户端包含认证信息,认证客户端
  • 验证授权码是否有效
  • 验证redirect_uri是否证券

Access Token Response

如果获取访问令牌的请求是有效的而且认证通过,认证服务器需要发放访问令牌和可选的刷新令牌。如果请求是无效的或认证失败,需要返回一个错误响应。

1
2
3
4
5
6
7
8
9
HTTP/1.0 200 OK
Date: Tue, 18 Jul 2017 04:12:00 GMT
Server: WSGIServer/0.1 Python/2.7.9
X-Frame-Options: SAMEORIGIN
Content-Type: application/json
Pragma: no-cache
Cache-Control: no-store

{"access_token": "4C1gRp2TfYKN0tO45fqa6BkFZxTJNU", "token_type": "Bearer", "expires_in": 36000, "refresh_token": "oZX6XtV2OGSVRnvyOoa7qerWddOPKw", "scope": "read write"}

Refreshing an Access Token

如果认证服务器向客户端颁发了刷新令牌,客户端可以使用刷新令牌更新访问令牌。

Content-Type: application/x-www-form-urlencoded

Parameter Description
grant_type REQUIRED该值必须设置为”refresh_token”
refresh_token REQUIRED认证服务器颁发的刷新令牌
scope OPTIONAL

刷新令牌是长期存在证书用来更新访问令牌,刷新令牌必须要与客户端id绑定。此外,客户端必须向认证服务器提供认证信息。

1
2
3
4
5
6
7
8
9
10
11
POST /o/token/ HTTP/1.0
Host: localhost:8000
Connection: close
Content-Length: 69
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: python-requests/2.18.1
Content-Type: application/x-www-form-urlencoded
Authorization: Basic d3Q2UHZtMnMzdmJiOFJQRTdubFBsdWd3YU1uajU4VWhGcGs4YkNQcDpWcUF3NDlwb3VXc0lUVU1OcWFrdzczQ0NSRVZzclMyRWd5SG9WeVJnekpWNWRlWnNKQUNBeVdkeEFJMXZRc2RaZk1JQmlCRlRFeUR5YUkyeTJJcU1vR3JEbkFNWE5ITWd4TFRJUDIxRXhIR0tkUmNTVlJ5RjRxOHA5STR5OWhqVQ==

grant_type=refresh_token&refresh_token=oZX6XtV2OGSVRnvyOoa7qerWddOPKw

Response:

1
2
3
4
5
6
7
8
9
HTTP/1.0 200 OK
Date: Tue, 18 Jul 2017 06:47:50 GMT
Server: WSGIServer/0.1 Python/2.7.9
X-Frame-Options: SAMEORIGIN
Content-Type: application/json
Pragma: no-cache
Cache-Control: no-store

{"access_token": "gebkQX71vIwmksTsvleRKMzpdysLHY", "token_type": "Bearer", "expires_in": 36000, "refresh_token": "CbpaNXAqyexTQOuCvQf1RUoEN2EZeX", "scope": "read write"}

Accessing Protected Resources

客户端可以同访问访问资源服务器受保护的资源。资源服务器必须要验证访问令牌,保证访问令牌没有过期,并且其作用域能覆盖此资源。

Access Token Types

“bearer”令牌类型,参考RFC6750

1
2
3
GET /resource/1 HTTP/1.1
Host: example.com
Authorization: Bearer mF_9.B5f-4.1JqM

“mac”令牌类型

1
2
3
4
5
GET /resource/1 HTTP/1.1
Host: example.com
Authorization: MAC id="h480djs93hd8",
nonce="274312:dj83hs9s",
mac="kDZvddkndxvhGRXZhvuDjEWhGeE="

References