Ezio's Blog

record


  • Home

  • Categories

  • Archives

  • Tags

  • About

回放机制

Posted on 2016-10-15   |  

最近把《深入理解计算机系统》又看了一遍,除了复习了下编译链接和存储系统的知识外,还注意到一个很有意思的点:一个通用的回放机制。

系统底层的回放机制:换页


计算机处于平衡读写速度和存储容量的目的,把存储划分为类似金字塔的各个层次,每一层都可以看作是对下层的缓存,这样,存储系统利用程序的时间和空间局部性,在能提供足够的容量的前提下也能保持高速的读写速度。

其中,操作系统会把内存和磁盘看做一个整体:虚拟内存。用户空间下的内存读写不会直接操作物理内存,而是通过虚拟内存来管理的。虚拟内存会根据需要在物理内存和磁盘中做不同的操作。

另外,出于节省物理内存以便同时运行多个任务的目的,操作系统在加载程序创建进程的时候不会把整个程序一次性都加载到物理内存中,而是只加载当前需要的一部分。于是,当需要读取的数据还没加载到物理内存中,操作系统就需要提供一套机制来尽快把需要的数据从磁盘加载到物理内存中。

这套机制就叫做换页(paging),而需要读取的数据还没加载到物理内存这种情况就叫做缺页。

当发生缺页时,也就是CPU发送的内存读写指令发现要读写的内存地址不在物理内存中,会出发一个异常而进入一个内核的异常处理流程:系统会把暂时不需要的物理内存中的数据存入磁盘,把目的内存地址所在的页(操作系统一般把大小为4K的内存数据划分为一页)加载进内存,同时会重新执行之前CPU发送的内存读写指令,这时需要的内存数据就已经在物理内存中了,同时相邻的内存数据也可以直接读取了。

“重新执行执行之前CPU发送的内存读写指令”。同样,OC也有类似的回放机制。

OC runtime的回放机制:消息发送

OC 的runtime是这门语言的精髓,各种特性都离不开runtime。其中,runtime中的消息发送也有类似的回放机制。

当执行

@interface EZObject : NSObject
@end

@implementation EZObject
static void lightup_imp(id self, SEL sel) {
    NSLog(@"it's on fire!");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if ([NSStringFromSelector(sel) isEqualToString:@"lightup"]) {
        class_addMethod([self class], sel, (IMP)lightup_imp, "v@:");
    }

    return true;
}

@end

EZObject *ez_instance = [[EZObject alloc] init];
SEL lightup_sel = NSSelectorFromString(@"lightup");
[ez_instance performSelector:lightup_sel];

时,无论是在EZObject还是NSObject,无论是类的cache还是method_list中都找不到lightup的实现,OC会调用EZObject的resolveInstanceMethod方法,来再次确认是否可以找出该方法,于是这里也就提供了一个机会让我们可以利用runtime的class_addMethod来创建一个OC的方法,创建好之后告诉OC现在有了light方法,于是OC 再次向ez_instance发送lightup消息,这次就可以成功找到并调用该方法了。

具体的实现由于NSObject所在的Foundation框架并没有开源,并不能确定,但参考cocoa的开源实现 GNUStep ,可以有一些收获。

在GSFFIInvocation.m中有这么一段代码:

/**
 * Runtime hook used to provide message redirections with libobjc2.
 * If lookup fails but this function returns non-nil then the lookup
 * will be retried with the returned value.
 *
 * Note: Every message sent by this function MUST be understood by the
 * receiver.  If this is not the case then there is a potential for infinite
 * recursion.  
 */
static id gs_objc_proxy_lookup(id receiver, SEL op) {
    id cls = object_getClass(receiver);
    BOOL resolved = NO;

    /* Note that __GNU_LIBOBJC__ implements +resolveClassMethod: and
       +resolveInstanceMethod: directly in the runtime instead.  */

    /* Let the class try to add a method for this thing. */
    if (class_isMetaClass(cls)) {
        if (class_respondsToSelector(cls, @selector(resolveClassMethod: ))) {
            resolved = [receiver resolveClassMethod: op];
        }
    } else {
        if (class_respondsToSelector(object_getClass(cls),
                @selector(resolveInstanceMethod: ))) {
            resolved = [cls resolveInstanceMethod: op];
        }
    }
    if (resolved) {
        return receiver;
    }
    if (class_respondsToSelector(cls, @selector(forwardingTargetForSelector: ))) {
        return [receiver forwardingTargetForSelector: op];
    }
    return nil;
}

gs_objc_proxy_lookup函数就是完成方法查询的工作。大致意思就是如果查询失败但返回的是一个非nil对象,那么就会在这个非nil对象上重试查询。这样,就完成了消息发送时的回放机制。

总结

应用层的很多东西都是从操作系统学习过来的,比如类似kafka和rabbitmq等消息系统,都受到了操作系统的事件通知系统的启发,比如linux的inotify,毕竟普通应用要解决的问题基本上操作系统都已经解决过了,并且经过长时间的优化改进,架构设计都已经足够成熟和优雅,因此,了解底层的操作系统对解决上层应用碰到的问题还是很有帮助的。

iOS面试题

Posted on 2016-10-08   |  

之前帮朋友的公司面试iOS开发,于是列了个问题清单

技术

使用什么版本工具?git使用经历?

查看代码风格

聊下内存管理

说下多线程的认识和使用经历

面向对象思想

更多问题:
http://www.cnblogs.com/ios8/p/ios-oc-test.html

UI布局

View的生命周期
Autolayout
Core Animation
POP
UICollectionView
UITableView
reveal

数据存储

sqlite
core data
userdefaults
file

网络交互

AFNetworking
NSURLConnection
NSURLSession
SDWebImage
如何封装以适应具体业务

调试思路

哪些调试手段?
如何使用哪些工具优化内存?

项目

拿到一个需求后,如何开始做?

使用CoreLocation时遇到的问题

SDWebImage可以调整过期时间么?怎么调?

沟通

说下你最近一个项目的经历?哪些让人不爽的情况?你是如何处理的?越详细越好

视野

有github账号么?

平时浏览哪些网站来学习iOS?

看过哪些iOS书籍?

架构意识与设计

设计模式

有看过什么架构类书籍么?

一个列表,需要从cocoaChina上获取数据后显示,从UI布局到网络请求,如何设计?

职业规划

能描述下3年后你希望自己在技术上达到什么样的程度?在公司的位置是怎么样?

沟通是一种重要能力

Posted on 2016-10-03   |  

现在要完成一个大一点的目标,经常需要多人协作,协作首先意味沟通,如果沟通不顺畅或者传达了错误的信息,那么就不可避免增加了协作的成本。

同样,和亲人朋友的相处是我们日常生活中很重要的一部分,因为我们的生活很大一部分都是与他们相关,频繁的接触意味着可能的摩擦,因此也就更需要良好的沟通能力才能维护好应有的亲密关系。

良好的沟通意味着准确的表达能力和优先善意地诠释他人传达的信息的能力。

准确的表达能力

准确地传达你的信息,意味着:即使对方的诠释能力还有待提高或者不太愿意沟通,但也能更容易地理解你的意图。著名的“电梯测试”就是训练如何在乘坐电梯的这一小段时间内清晰准确地表达出你的信息。

作家通过文字来传达自己的意图,而文字作为视觉内容的一部分,比起电影电视剧,显然更难以表现出原本的意图和情感,所谓的“词不达意”就是形容文字没有很好地表达原本的内涵。由此,优秀的作家必须拥有比常人优秀的表达能力,最终才能通过文字这种单一的媒介描绘出复杂的世界、变幻的人心。我们喜爱读他们的作品也是部分因为我们能轻松地理解。

工作同样如此。好的工作环境,除了同事的平均技术能力和可口的零食水果,也应该包含和睦的同事关系和良好的沟通习惯。除了家人朋友,与同事的相处占据着我们生活中很大一部分时间,更多的相处意味着更可能的摩擦,如果能和同事保持愉快的合作关系,无论对工作效率的提升还是个人幸福感的提升,都是大有帮助。保持愉快的合作是一个需要双方努力的目标,除了对方的理解,还有自己的表达和付出。互惠原则很好地说明人性的特点,但其实互惠原则的本质在于对方接受了你的善意,因此我们不是一定需要某些物品才能让互惠原则生效,只要我们能让对方感受到我们友好的姿态也能让合作高效地进行下去。

当然,表达的形式不限于语言、文字,除了文学,音乐、绘画、影视等借助的方式都可以成为表达我们所思所想的工具。

表达的方式同样不止一种,除了直截了当,我们还可以委婉含蓄地表达自己的意图。除了准确性,表达的同时也应该照顾到对方的情感,不同的场景应该采用不同的方式。一般场景下,幽默的方式自然是他人最容易接受的方式,但如果是葬礼上,幽默一旦驾驭不好,很可能就会惹人不快。

鸡和蛋

能否准确地表达,我觉得,不仅仅是沟通能力的一个方面,其实也是一个人层次高低的一个重要表现。

高层次的人普遍能抓住其表达主题的重点,并结合对方的关注点,有选择地调整表达的内容和重点,从而在对方兴趣消失前,持久地抓住对方的注意力。这种表达能力和表达技巧,会有助于获取更多的资源和帮助,从而获得更好的机会和结果。华尔街的路演和希特勒的演讲,都是优秀表达能力的例子。

那么,高层次的人更容易拥有优秀的表达能力?还是,优秀的表达能力帮助我们上升到更高层次?又是一个先有鸡还是先有蛋的故事。

善意的诠释能力

我一直认同“人性本恶”的说法。也许是因为经历了现实的挫折以及看了太多电影,目睹了太多人性的丑恶面,让我总是以一种防卫的姿态面对环境。直到近几年认识了一些朋友才让我扭转了这个消极的观念。

哪来那么多坏人

回想起小时候,有一次和亲人坐火车出远门,路上遇到一个和我年龄差不多的小女孩,小孩子很容易就混熟了,于是就了解到她经常一个人坐火车往返于几千公里的两个城市,我当时就问她害怕不害怕路上遇到坏人,现在尽管早以忘记她的容貌,但她的回答我至今记得:“哪来那么多坏人,我碰到的都是好人啊”。

小孩子生来认为人人都应该是诚实善良,因为他们是小孩子。我们都曾经是个小孩子,都拥有一颗赤子之心。但随着年龄的增长,我们从周围从书本认识到世界上还有不怀好意,还有各种丑恶,天生的自卫机制让我们慢慢地失去了优先善意地诠释他人意图的能力。

但是“哪来那么多坏人?”。的确,现在是一个物欲横流的社会,我们想要的都太多,但由此就认为人人唯利是图,即使是成天周旋商界的最成功的商人也不会同意。每个人,只要不是穷凶极恶,内心都有自我认同机制,这种机制会根据自己的的行为在潜意识中塑造自己的自我形象:you are what you do。

人性本善。

之乎者也

除了善意,我们还需要准确全面的诠释能力。

我们中学其实就已经在刻意地训练诠释能力了。阅读理解、作文是考试时必考的两项。除此之外,我觉得诗词的学习其实是更能训练我们的诠释能力。唐诗宋词,古人的这些作品,都是根据当时各自的背景和情境,以一种我们看来奇异的语法,用我们在日常生活中几乎早已不用的词句堆砌起来的,自然比普通的文章更让人难以理解和诠释。

了解作者当时的社会背景,对比他的个人情境,学习每个生僻的字词,拆分每个怪异的语法结构,我们最终能成功地准确诠释作者想要表达的意图。当然,每个人的诠释都是有偏差的,但重要的是我们刻意努力地想要理解这个早已不在人世的陌生人。

我们需要主动去理解他人以及他人的意图,锻炼自己的诠释能力。

总结

作为技术人的我们,很容易走入“硬技术”的死胡同,但其实世界不仅仅有技术,单纯的技术也不能“制霸”,历史上曾经就有很多牛逼的技术被埋没。换句话说,我们首先是人,然后才是技术人,而作为人,沟通能力在与人相处协作中是必不可缺的重要能力。

Popclip的JSON格式化扩展

Posted on 2016-09-29   |  

作为一个MAC党,不好好利用MAC的神兵利器,简直就是罪过。Alfred、Dash、Ulysses、SnippetsLib、Mindnode等大名鼎鼎的效率神器自然不用提了,Popclip更是一个每天都会使用上百遍的好帮手。

Popclip?

简单来说,Popclip就是一个对选中的内容作快速处理的工具,比如直接搜索选中的内容、从选中的内容生成二维码、计算选中的内容的字数等,除此之外,还能自定义扩展来实现你想要的功能。

这是我的Popclip扩展:

如果你还没装Popclip,马上停下来,去安装一个,再继续看下去;

如果你不知道Popclip是什么,马上停下来,去看下这篇测评,再继续看下去。

JSONizer的来由

平时经常需要对一坨字符进行格式化,那时每次都需要复制、打开jsbeautifier.org、粘贴、点击格式化按钮,碰到网络不好的情况还要等半天,如果没网络,更是头疼。

后来改用sublime的插件CodeFormatter,也能比较方便地快速格式化,但还是有个点让我不开心:CodeFormatter要求必须先把需要格式化的内容保存在一个后缀为.json的文件中,才能识别并格式化。

由于用Popclip已经好一段时间了,很享受它提供的便利,于是就想装个JSON格式化扩展,搜了一下,发现竟然没有,于是就萌生了自己写一个的想法。

动手

JSON格式化的lib都已经很成熟了,正好在jsbeautifier.org上看到有提供python的一个lib。

Popclip的扩展没有Alfred的workflow能提供的功能多而复杂,相应地也容易上手。参照TUTS上的这篇教程,几分钟就搞定了大致框架。

接下来就简单了,把依赖的几个lib依赖配好,基本文件布局如下:

注:editorconfig、six.py是jsbeautifier的依赖项。

最后,测试效果完美:

下载入口 ,希望能帮到需要的朋友。

总结

目前,需要先将需要格式化的内容拷贝到编辑器中,然后再选中才能格式化。其实还能改进一下,不需要拷贝,直接在内容来源上,比如浏览器中,选中需要格式化的字符并格式化,直接把格式化后的内容写入系统剪贴板。后续有时间可以研究下。

格局

Posted on 2016-09-25   |  

最近杭州房价快速攀升,同一套房子,去年还是80万,今年120万还不一定买得到。考虑到一系列因素,我也开始准备买房了。

买房

经过几天的看房-谈价格-房东加价-同意-房东不卖了,最终房子还是买到了。

虽然最终房子是买到了,价格确比去年高了几十万。忍不住想:要是去年买了就好了。

去年身边朋友和我一样都没买房,所以我没意识到需要在杭州买一套?不是的,来杭州已经快5年了,这5年间家人也催过买房要抓紧,同事也建议说现在可以入手一套,但每次都想都没想就以各种理由说不想买房。直到最近看了一本书,《软技能》,才意识到房地产投资是可以极大增加自己的个人资产。然后随着杭州房价的快速攀升,也忍不住出手了。

格局

身边的朋友去年买的房子现在已经涨了一倍了,如果我和他们同时买,现在也是身价好几百万的人了。后悔错过机遇是徒劳的,向前看才是最重要的。如果以后碰到类似的机遇,该如何才能把握住?

经常听人说类似的话:一个人的格局决定了他能达到的高度。这话听起来挺虚的,但其实往往是这类抽象而模棱两可的话说出了事物的真相。什么是格局?

百度百科里对格局的定义是这样的:

格局,格是指以时间为格,局是指时间格子内所做事情以及事情的结果,合起来称之为格局。不同的人,在同一时间内所做的事情以及事情的结果不一样,所以说不同的人,格局不一样。
局势、态势的理解和把握。即一个人对事物所处的位置(时间和空间)及未来的变化的认知程度。

说白了,就是要在不同的阶段做正确的事情。比如指挥战斗的将领,要运筹帷幄而决胜千里之外,比如政客,要懂得审时度势。

格局对一个人的影响如此重要,怎么提高自己的格局?为什么古时那些出生名门望族的子弟的格局基本都高于汲汲营营的底层百姓(现在其实也差不多)?为什么现在我们都逃出内陆,涌向沿海,逃出中国,涌向海外?除了外面的月亮比较圆、月饼更少添加剂以外,还因为我们都知道,视野对于格局的锻炼是非常重要的。

You are what you read

如果别人说:我吃过的盐比你走的路还多,我总是经不住想,你口味挺重的啊。但其实阅历对人的格局影响也是非常重要的。都说:

要么读书要么旅行灵魂和身体必须有一个在路上

阅历的增长,其实就是一个持续的“在路上“的过程。只有经历过类似的东西,当面对貌似熟悉的事物时,我们已收集的信息才有能力支撑起足够的信心去面对它。

回到买房

为什么即使在家人、同事的提醒下,我还是没动买房的心思?而同事却抓住了这个机遇?

其实是我那时一门心思花在工作和能力提升上,总觉得个人能力才是“硬通货”。这想法没错,刚毕业的小年轻本来就没太多资产,当务之急还是以工作技能为重。但就像格局的定义一样,在不同的阶段应该做不同的但正确的事情。对工作5年的我来说,还有更紧迫的事情。

同事从深圳过来,他经历过深圳房价腾飞的阶段,错过绝好的投资机会让他对投资理财有了更深刻的理解,而这时我们还是算计着网贷的收益要选8.1%还是9.3%的。

所以,其实我们都会经历过一些同样的阶段,还是那句老话:吃一堑,长一智。

Hopper最新破解版

Posted on 2016-09-18   |  

去年看完《iOS应用逆向工程》后玩了下hopper,http://bbs.iosre.com/论坛里几个人在问Hopper的最新破解版,于是找了下,附上链接:
http://xclient.info/s/hopper-disassembler.html

使用Docker创建Elasticsearch服务

Posted on 2016-09-17   |  

最近需要做的几个功能都是基于Elasticsearch,但又不想污染系统环境,所以就花了些时间研究下如何使用Docker创建Elastcisearch服务。

首先先分别了解下Docker和Elasticssearch。

Docker是什么?

Docker是一个开源工具,能将一个WEB应用封装在一个轻量级,便携且独立的容器里,然后可以运行在几乎任何服务环境下。
Docker的容器能使应用跑在任何服务器上并且表现一致。一个开发者在笔记本上建立的一个容器,能跑在很多环境下,如:测试环境,生产环境,虚拟机上,VPS,OpenStack集群,公用的电脑等等
Docker的一般使用在以下几点:
• 自动化打包和部署应用
• 创造一个轻量级的,私人的 PAAS 环境
• 自动化测试和连续的 整合/部署
• 部署WEB应用,数据库和后端服务

所以,Docker是一个系统级兼容的容器,它采用Linux Container技术构建一个虚拟环境,用户可以在这个环境下安装各种应用来提供服务,并且这个环境可以随时创建或销毁,不会影响宿主环境

Elasticsearch是什么?

Elasticsearch也使用Java开发并使用Lucene作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的RESTful API来隐藏Lucene的复杂性,从而让全文搜索变得简单。

不过,Elasticsearch不仅仅是Lucene和全文搜索,我们还能这样去描述它:
• 分布式的实时文件存储,每个字段都被索引并可被搜索
• 分布式的实时分析搜索引擎
• 可以扩展到上百台服务器,处理PB级结构化或非结构化数据

总之,ES是一个牛逼的搜索存储引擎

安装Docker

Mac上安装Docker很简单,基本跟着官网的引导就可以顺利安装了,完成后确保Docker已启动,如下图

创建Docker 镜像

Elasticsearch官方在Docker Hub上已经有提供镜像,如果没有额外需求,执行下面这个命令就可以直接使用Elasticsearch官方提供的镜像:

docker run -d -p 9200:9200 --name="es" elasticsearch:2.3.5

但我还想额外装一个Elasticsearch的插件,方便调适,所以就自己做了一个镜像, Dockerfile

FROM elasticsearch:2.3.5

RUN /usr/share/elasticsearch/bin/plugin install mobz/elasticsearch-head

EXPOSE 9200

进入Dockerfile所在的文件夹,执行以下命令:

docker build --tag=es_ezio:2.3.5 .

然后执行

docker ps

就能看到刚才创建的镜像了

启动容器及服务

上一步我们只是制作了一个Docker镜像,还没有创建Docker容器。关于Docker中镜像和容器的关系,可以类比为操作系统中的程序和进程,或者面向对象语言中的Class和Instance。我们必须从镜像创建出容器才能运行我们的服务(也就是Elasticsearch服务)。

第一次创建Docker容器,执行以下命令:

docker run -d -p 9200:9200 --name="es_ezio" es_ezio:2.3.5

Elasticsearch的默认端口是9200,我们把宿主环境9200映射到Docker容器中的9200端口,这样我们就可以直接访问宿主环境的9200端口就可以访问到Docker容器中的Elasticsearch服务了,同时我们把这个容器命名为es_ezio。

如果一切顺利,访问 http://127.0.0.1:9200/_plugin/head/

这样,我们就完成了用Docker提供Elasticsearch服务,而不污染宿主机环境了,这样还有一个好处,如果想同时启动多个不同版本的Elastcsearch或者其他服务,Docker也是一个理想的解决方案。

总结

Docker主要是面向运维部署,但其实对开发人员来说也是一个不错的playground,后续我的个人服务会尽量都采用Docker的方式部署。
Elastic search是一个功能强大的搜索存储引擎,特别是它强大的全文搜索能力,要好好研究下。

关于几个初始化shell文件

Posted on 2016-08-30   |  

shell在类linux系统中扮演的角色非常重要,连操作系统启动的入口一般都是shell,一般类linux系统启动时会涉及到这几个shell脚本,今天就来看看这几个脚本的作用

linux

/etc/profile:此文件为系统的每个用户设置环境信息,当用户第一次登录时,该文件被执行. 并从/etc/profile.d目录的配置文件中搜集shell的设置.

/etc/bashrc:为每一个运行bash shell的用户执行此文件.当bash shell被打开时,该文件被读取.

~/.bash_profile:每个用户都可使用该文件输入专用于自己使用的shell信息,当用户登录时,该 文件仅仅执行一次!默认情况下,他设置一些环境变量,执行用户的.bashrc文件.

~/.bashrc:该文件包含专用于你的bash shell的bash信息,当登录时以及每次打开新的shell时,该 该文件被读取.

另外,/etc/profile中设定的变量(全局)的可以作用于任何用户,而~/.bashrc等中设定的变量(局部)只能继承/etc/profile中的变量,他们是”父子”关系.

~/.bash_profile 是交互式、login 方式进入 bash 运行的
~/.bashrc 是交互式 non-login 方式进入 bash 运行的
通常二者设置大致相同,所以通常前者会调用后者。

MAC OS

MAC OS上的shell执行顺序有点特殊:
/etc/profile ->~/.bashrc(or .zshrc) -> /etc/bashrc

关于跨域的一些事

Posted on 2016-07-14   |  

web前端开发中,我们经常需要访问除了源服务器(输出当前页面的服务器)外的服务,这时由于浏览器同源策略https://en.wikipedia.org/wiki/Same-origin_policy的限制,我们需要做很多额外的修改才能达到目的。而在web后端开发中,我们可以直接从一个服务器发送请求到另一个服务器,由于不存在浏览器做中介,所以可以很方便地进行通信,诞生了各种RPC框架,这些框架的底层通信模块常见的基本都是使用socket、HTTP、PB(Protocol Buffers)等通信协议。这篇文章只说在浏览器参与的跨域中的情况。

跨域分两种:跨子域和跨全域。从easydb.dtstack.com发送HTTPRequest到www.dtstack.com,这样的叫跨子域;从www.baidu.com发送HTTPRequest到www.dtstack.com,这样的叫跨全域。

关于跨域通信,目前已经有了很多方法,最常见的就是JSONP,但JSONP和
Ajax并不相同。Ajaxhttps://zh.wikipedia.org/zh/AJAX通常都是通过新建XMLHTTPRequest(IE下还有类似XMLDomainRequest)来创建请求,而JSONP的本质其实是利用向DOM中插入script标签来“触发”请求,因此JSONP不会受到浏览器同源策略的影响。因此,许多web service都是通过JSONP实现。但JSONP任何人都能调用,无形中放大了服务器的潜在bug危害,因此我们还会时不时地用到Ajax。接下来我们就来聊聊Django中常见的Ajax跨域解决方案。

跨子域

解决跨子域有纯前端的方案,比如从easydb.dtstack.com跨子域访问www.dtstack.com,只需要在JS中设置
document.domain = 'dtstack.com'
然后正常发送请求就行了。

也有前后端配合的方案,可以参考跨全域的方案。

跨全域

跨全域通信的关键在于被访问的服务器允许访问方进行访问。

因此,一般发送跨域请求时,浏览器会“询问”服务器是否允许当前请求进行访问,“询问”的方式是通过发送一个OPTIONS请求,这时被访问服务器需要返回一些信息表示是否允许,这些信息一般放在HTTP header中。这样,如果被访问服务器返回表示允许访问的HTTP header,那么浏览器就会发送用户创建的跨域请求,这样就完成了跨域通信了。

基本原理弄清楚了,接下来就是实现细节了。

Django中,我们可以添加一个中间件来为跨域请求设置恰当的HTTP header。首先创建一个cross_domain_middleware.py文件

from dtuic.settings import ALLOWED_HOSTS_FOR_MIDDLEWARE

class CrossDomainMiddleware(object):

    def process_response(self, request, response):

        origin = request.META.get('HTTP_ORIGIN')
        allowed_hosts = self.get_allowed_hosts(request)

        if origin in allowed_hosts or '*' in allowed_hosts:
            response["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept, Key, source"
            response["Access-Control-Allow-Methods"] = "POST, GET, OPTIONS, DELETE, PUT"
            response["Access-Control-Allow-Origin"] = origin
            response["Access-Control-Max-Age"] = "1000"
            response['Access-Control-Allow-Credentials'] = "true"

        return response

    def get_allowed_hosts(self, request):
        hosts = []
        for host in ALLOWED_HOSTS_FOR_MIDDLEWARE.split(','):
            host = host.strip()
            if host != '*':
                host = "{}://{}".format(request.scheme, host)

            hosts.append(host)

        return hosts

,同时在settings文件中添加

ALLOWED\_HOSTS\_FOR\_MIDDLEWARE = "www.baidu.com, www.whatever.com"

,最后在Django的middleware配置中添加我们新建的cross_domain_middleware

MIDDLEWARECLASSES = (
'dtuic.middleware.crossdomainmiddleware.CrossDomainMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
...
)

放在最前面,见https://docs.djangoproject.com/en/1.10/ref/settings/#allowed-hosts

目前ALLOWED_HOSTS_FOR_MIDDLEWARE 只支持多个域名显示配置,也可以使用正则动态匹配。

注意Django中的ALLOWED_HOSTS并不是为了跨域的配置,而是针对全局请求访问的一个配置,如果直接覆盖,可能会导致全站400,本地开发时由于Django的调适标识Debug都为True,Django这时是默认允许所有请求访问的,所以即使覆盖了ALLOWED_HOSTS也不会有问题,而到了线上,Debug切换到False,就会有“惊喜”了。

参考:
http://www.html5rocks.com/en/tutorials/cors/

Django源码分析之权限系统\_ 擒贼先擒王

Posted on 2016-06-10   |  

乍见

Django内置的权限系统已经很完善了,加上django-guardian提供的功能,基本上能满足大部分的权限需求。暂且不说django-guardian,我们先来看下Django内置的权限系统:django.contrib.auth 包。

相识

一般权限系统分为全局权限和对象权限。Django只提供了一个对象权限的框架,具体实现由第三方库django-gardian完成。我们只看全局权限。

先来看auth包暴露出哪些接口。

django.contrib.auth.__init__.py

def load_backend(path):
    return import_string(path)()


def _get_backends(return_tuples=False):
    backends = []
    for backend_path in settings.AUTHENTICATION_BACKENDS:
        backend = load_backend(backend_path)
        backends.append((backend, backend_path) if return_tuples else backend)
    if not backends:
        raise ImproperlyConfigured(
            'No authentication backends have been defined. Does '
            'AUTHENTICATION_BACKENDS contain anything?'
        )
    return backends


def get_backends():
    return _get_backends(return_tuples=False)

前三个方法都是为了加载backends。一个backend其实就是一个class,必须实现authenticate和get_user两个方法。每当我们这样验证用户时

authenticate(username='username', password='password')

django就会去调用这些backend class,用其提供的方法去验证用户权限。那django是如何知道要调用哪些backend class呢?答案就在settings.py中,默认为

AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend']

那Django是如何调用这些backend class的呢?

def authenticate(**credentials):
    """
    If the given credentials are valid, return a User object.
    """
    for backend, backend_path in _get_backends(return_tuples=True):
        try:
            inspect.getcallargs(backend.authenticate, **credentials)
        except TypeError:
            # This backend doesn't accept these credentials as arguments. Try the next one.
            continue

        try:
            user = backend.authenticate(**credentials)
        except PermissionDenied:
            # This backend says to stop in our tracks - this user should not be allowed in at all.
            return None
        if user is None:
            continue
        # Annotate the user object with the path of the backend.
        user.backend = backend_path
        return user

    # The credentials supplied are invalid to all backends, fire signal
    user_login_failed.send(sender=__name__,
            credentials=_clean_credentials(credentials))

由此可见,Django会在第一个验证正确的backend class调用完成后停止,或者碰到PermissionDenied异常也会停止,所以backend class的顺序也很重要。可以添加自定义的backend class。

def login(request, user):
    """
    Persist a user id and a backend in the request. This way a user doesn't
    have to reauthenticate on every request. Note that data set during
    the anonymous session is retained when the user logs in.
    """
    session_auth_hash = ''
    if user is None:
        user = request.user
    if hasattr(user, 'get_session_auth_hash'):
        session_auth_hash = user.get_session_auth_hash()

    if SESSION_KEY in request.session:
        if _get_user_session_key(request) != user.pk or (
                session_auth_hash and
                request.session.get(HASH_SESSION_KEY) != session_auth_hash):
            # To avoid reusing another user's session, create a new, empty
            # session if the existing session corresponds to a different
            # authenticated user.
            request.session.flush()
    else:
        request.session.cycle_key()
    request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)
    request.session[BACKEND_SESSION_KEY] = user.backend
    request.session[HASH_SESSION_KEY] = session_auth_hash
    if hasattr(request, 'user'):
        request.user = user
    rotate_token(request)
    user_logged_in.send(sender=user.__class__, request=request, user=user)

login方法,顾名思义,登录用户,同时设置好session,最后发送登入成功通知

def logout(request):
    """
    Removes the authenticated user's ID from the request and flushes their
    session data.
    """
    # Dispatch the signal before the user is logged out so the receivers have a
    # chance to find out *who* logged out.
    user = getattr(request, 'user', None)
    if hasattr(user, 'is_authenticated') and not user.is_authenticated():
        user = None
    user_logged_out.send(sender=user.__class__, request=request, user=user)

    # remember language choice saved to session
    language = request.session.get(LANGUAGE_SESSION_KEY)

    request.session.flush()

    if language is not None:
        request.session[LANGUAGE_SESSION_KEY] = language

    if hasattr(request, 'user'):
        from django.contrib.auth.models import AnonymousUser
        request.user = AnonymousUser()

相对的,logout方法,负责登出用户,清理session,最后设置当前用户为匿名用户

def get_user_model():
    """
    Returns the User model that is active in this project.
    """
    try:
        return django_apps.get_model(settings.AUTH_USER_MODEL)
    except ValueError:
        raise ImproperlyConfigured("AUTH_USER_MODEL must be of the form 'app_label.model_name'")
    except LookupError:
        raise ImproperlyConfigured(
            "AUTH_USER_MODEL refers to model '%s' that has not been installed" % settings.AUTH_USER_MODEL
        )

Django不推荐直接使用User class,而是通知get_user_model方法获取当前的用户class(或者使用settins.AUTH_USER_MODEL)。这是为了防止因为开发者使用了自定义用户class而导致的信息错误。

def update_session_auth_hash(request, user):
    """
    Updating a user's password logs out all sessions for the user if
    django.contrib.auth.middleware.SessionAuthenticationMiddleware is enabled.

    This function takes the current request and the updated user object from
    which the new session hash will be derived and updates the session hash
    appropriately to prevent a password change from logging out the session
    from which the password was changed.
    """
    if hasattr(user, 'get_session_auth_hash') and request.user == user:
        request.session[HASH_SESSION_KEY] = user.get_session_auth_hash()

最后这个方法的使用场景很少。一般我们更新用户密码时,会在session中清除用户登录信息,导致用户需要重新登录。而使用update_session_auth_hash我们就可以在更新用户密码的同时更新用户的session信息,这样,用户就不需要重新登录了。

回想

擒贼先擒王,以上都是django.contrib.auth包中的__init__.py入口文件中的内容,背后还有很多“能工巧匠”,否则怎么支撑起auth整套权限系统?后续文章会一一介绍。

_

1…345
Ezio

Ezio

44 posts
© 2017 Ezio
Powered by Hexo
Theme - NexT.Mist