diff --git a/docs/guide-zh-CN/README.md b/docs/guide-zh-CN/README.md index 0a9d686..c439cc0 100644 --- a/docs/guide-zh-CN/README.md +++ b/docs/guide-zh-CN/README.md @@ -119,6 +119,7 @@ RESTful Web 服务 * **已定稿** [快速入门](rest-quick-start.md) * **已定稿** [资源](rest-resources.md) +* **已定稿** [控制器](rest-controllers.md) * **已定稿** [路由](rest-routing.md) * **已定稿** [格式化响应](rest-response-formatting.md) * **已定稿** [授权验证](rest-authentication.md) diff --git a/docs/guide-zh-CN/db-active-record.md b/docs/guide-zh-CN/db-active-record.md index 18ff81b..a327daf 100644 --- a/docs/guide-zh-CN/db-active-record.md +++ b/docs/guide-zh-CN/db-active-record.md @@ -37,7 +37,7 @@ $db->createCommand('INSERT INTO customer (name) VALUES (:name)', [ * Microsoft SQL Server 2010 及以上:通过 [[yii\db\ActiveRecord]] * Oracle: 通过 [[yii\db\ActiveRecord]] * CUBRID 9.1 及以上:通过 [[yii\db\ActiveRecord]] -* Sphnix:通过 [[yii\sphinx\ActiveRecord]],需求 `yii2-sphinx` 扩展 +* Sphinx:通过 [[yii\sphinx\ActiveRecord]],需求 `yii2-sphinx` 扩展 * ElasticSearch:通过 [[yii\elasticsearch\ActiveRecord]],需求 `yii2-elasticsearch` 扩展 * Redis 2.6.12 及以上:通过 [[yii\redis\ActiveRecord]],需求 `yii2-redis` 扩展 * MongoDB 1.3.0 及以上:通过 [[yii\mongodb\ActiveRecord]],需求 `yii2-mongodb` 扩展 @@ -1004,4 +1004,4 @@ TODO ------------------- - [模型(Model)](model.md) -- [[yii\db\ActiveRecord]] \ No newline at end of file +- [[yii\db\ActiveRecord]] diff --git a/docs/guide-zh-CN/rest-authentication.md b/docs/guide-zh-CN/rest-authentication.md new file mode 100644 index 0000000..d2a9aaa --- /dev/null +++ b/docs/guide-zh-CN/rest-authentication.md @@ -0,0 +1,116 @@ +认证 +============== + +和Web应用不同,RESTful APIs 通常是无状态的,也就意味着不应使用sessions 或 cookies, +因此每个请求应附带某种授权凭证,因为用户授权状态可能没通过sessions 或 cookies维护, +常用的做法是每个请求都发送一个秘密的access token来认证用户,由于access token可以唯一识别和认证用户, +**API 请求应通过HTTPS来防止man-in-the-middle (MitM) 中间人攻击**. + +下面有几种方式来发送access token: + +* [HTTP 基本认证](http://en.wikipedia.org/wiki/Basic_access_authentication): access token + 当作用户名发送,应用在access token可安全存在API使用端的场景,例如,API使用端是运行在一台服务器上的程序。 +* 请求参数: access token 当作API URL请求参数发送,例如 + `https://example.com/users?access-token=xxxxxxxx`,由于大多数服务器都会保存请求参数到日志, + 这种方式应主要用于`JSONP` 请求,因为它不能使用HTTP头来发送access token +* [OAuth 2](http://oauth.net/2/): 使用者从认证服务器上获取基于OAuth2协议的access token,然后通过 + [HTTP Bearer Tokens](http://tools.ietf.org/html/rfc6750) 发送到API 服务器。 + +Yii 支持上述的认证方式,你也可很方便的创建新的认证方式。 + +为你的APIs启用认证,做以下步骤: + +1. 配置`user` 应用组件: + - 设置 [[yii\web\User::enableSession|enableSession]] 属性为 `false`. + - 设置 [[yii\web\User::loginUrl|loginUrl]] 属性为`null` 显示一个HTTP 403 错误而不是跳转到登录界面. +2. 在你的REST 控制器类中配置`authenticator` 行为来指定使用哪种认证方式 +3. 在你的[[yii\web\User::identityClass|user identity class]] 类中实现 [[yii\web\IdentityInterface::findIdentityByAccessToken()]] 方法. + +步骤1不是必要的,但是推荐配置,因为RESTful APIs应为无状态的,当[[yii\web\User::enableSession|enableSession]]为false, +请求中的用户认证状态就不能通过session来保持,每个请求的认证通过步骤2和3来实现。 + +> 提示: 如果你将RESTful APIs作为应用开发,可以设置应用配置中 `user` 组件的[[yii\web\User::enableSession|enableSession]], + 如果将RESTful APIs作为模块开发,可以在模块的 `init()` 方法中增加如下代码,如下所示: + +```php +public function init() +{ + parent::init(); + \Yii::$app->user->enableSession = false; +} +``` + +例如,为使用HTTP Basic Auth,可配置`authenticator` 行为,如下所示: + +```php +use yii\filters\auth\HttpBasicAuth; + +public function behaviors() +{ + $behaviors = parent::behaviors(); + $behaviors['authenticator'] = [ + 'class' => HttpBasicAuth::className(), + ]; + return $behaviors; +} +``` + +如果你系那个支持以上3个认证方式,可以使用`CompositeAuth`,如下所示: + +```php +use yii\filters\auth\CompositeAuth; +use yii\filters\auth\HttpBasicAuth; +use yii\filters\auth\HttpBearerAuth; +use yii\filters\auth\QueryParamAuth; + +public function behaviors() +{ + $behaviors = parent::behaviors(); + $behaviors['authenticator'] = [ + 'class' => CompositeAuth::className(), + 'authMethods' => [ + HttpBasicAuth::className(), + HttpBearerAuth::className(), + QueryParamAuth::className(), + ], + ]; + return $behaviors; +} +``` + +`authMethods` 中每个单元应为一个认证方法名或配置数组。 + + +`findIdentityByAccessToken()`方法的实现是系统定义的, +例如,一个简单的场景,当每个用户只有一个access token, 可存储access token 到user表的`access_token`列中, +方法可在`User`类中简单实现,如下所示: + + +```php +use yii\db\ActiveRecord; +use yii\web\IdentityInterface; + +class User extends ActiveRecord implements IdentityInterface +{ + public static function findIdentityByAccessToken($token, $type = null) + { + return static::findOne(['access_token' => $token]); + } +} +``` + +在上述认证启用后,对于每个API请求,请求控制器都会在它的`beforeAction()`步骤中对用户进行认证。 + +如果认证成功,控制器再执行其他检查(如频率限制,操作权限),然后再执行操作, +授权用户信息可使用`Yii::$app->user->identity`获取. + +如果认证失败,会发送一个HTTP状态码为401的响应,并带有其他相关信息头(如HTTP 基本认证会有`WWW-Authenticate` 头信息). + + +## 授权 <a name="authorization"></a> + +在用户认证成功后,你可能想要检查他是否有权限执行对应的操作来获取资源,这个过程称为 *authorization* , +详情请参考 [Authorization section](security-authorization.md). + +如果你的控制器从[[yii\rest\ActiveController]]类继承,可覆盖 [[yii\rest\Controller::checkAccess()|checkAccess()]] 方法 +来执行授权检查,该方法会被[[yii\rest\ActiveController]]内置的操作调用。 diff --git a/docs/guide-zh-CN/rest-controllers.md b/docs/guide-zh-CN/rest-controllers.md new file mode 100644 index 0000000..cb50f04 --- /dev/null +++ b/docs/guide-zh-CN/rest-controllers.md @@ -0,0 +1,143 @@ +控制器 +=========== + +在创建资源类和指定资源格输出式化后,下一步就是创建控制器操作将资源通过RESTful APIs展现给终端用户。 + +Yii 提供两个控制器基类来简化创建RESTful 操作的工作:[[yii\rest\Controller]] 和 [[yii\rest\ActiveController]], +两个类的差别是后者提供一系列将资源处理成[Active Record](db-active-record.md)的操作。 +因此如果使用[Active Record](db-active-record.md)内置的操作会比较方便,可考虑将控制器类 +继承[[yii\rest\ActiveController]],它会让你用最少的代码完成强大的RESTful APIs. + +[[yii\rest\Controller]] 和 [[yii\rest\ActiveController]] 提供以下功能,一些功能在后续章节详细描述: + +* HTTP 方法验证; +* [内容协商和数据格式化](rest-response-formatting.md); +* [认证](rest-authentication.md); +* [频率限制](rest-rate-limiting.md). + +[[yii\rest\ActiveController]] 额外提供一下功能: + +* 一系列常用操作: `index`, `view`, `create`, `update`, `delete`, `options`; +* 对操作和资源进行用户认证. + + +## 创建控制器类 <a name="creating-controller"></a> + +当创建一个新的控制器类,控制器类的命名最好使用资源名称的单数格式,例如,提供用户信息的控制器 +可命名为`UserController`. + +创建新的操作和Web应用中创建操作类似,唯一的差别是Web应用中调用`render()`方法渲染一个视图作为返回值, +对于RESTful操作直接返回数据,[[yii\rest\Controller::serializer|serializer]] 和 +[[yii\web\Response|response object]] 会处理原始数据到请求格式的转换,例如 + +```php +public function actionView($id) +{ + return User::findOne($id); +} +``` + + +## 过滤器 <a name="filters"></a> + +[[yii\rest\Controller]]提供的大多数RESTful API功能通过[过滤器](structure-filters.md)实现. +特别是以下过滤器会按顺序执行: + +* [[yii\filters\ContentNegotiator|contentNegotiator]]: 支持内容协商,在 [响应格式化](rest-response-formatting.md) 一节描述; +* [[yii\filters\VerbFilter|verbFilter]]: 支持HTTP 方法验证; + the [Authentication](rest-authentication.md) section; +* [[yii\filters\AuthMethod|authenticator]]: 支持用户认证,在[认证](rest-authentication.md)一节描述; +* [[yii\filters\RateLimiter|rateLimiter]]: 支持频率限制,在[频率限制](rest-rate-limiting.md) 一节描述. + +这些过滤器都在[[yii\rest\Controller::behaviors()|behaviors()]]方法中声明, +可覆盖该方法来配置单独的过滤器,禁用某个或增加你自定义的过滤器。 +例如,如果你只想用HTTP 基础认证,可编写如下代码: + +```php +use yii\filters\auth\HttpBasicAuth; + +public function behaviors() +{ + $behaviors = parent::behaviors(); + $behaviors['authenticator'] = [ + 'class' => HttpBasicAuth::className(), + ]; + return $behaviors; +} +``` + + +## 继承 `ActiveController` <a name="extending-active-controller"></a> + +如果你的控制器继承[[yii\rest\ActiveController]],应设置[[yii\rest\ActiveController::modelClass||modelClass]] 属性 +为通过该控制器返回给用户的资源类名,该类必须继承[[yii\db\ActiveRecord]]. + + +### 自定义操作 <a name="customizing-actions"></a> + +[[yii\rest\ActiveController]] 默认提供一下操作: + +* [[yii\rest\IndexAction|index]]: 按页列出资源; +* [[yii\rest\ViewAction|view]]: 返回指定资源的详情; +* [[yii\rest\CreateAction|create]]: 创建新的资源; +* [[yii\rest\UpdateAction|update]]: 更新一个存在的资源; +* [[yii\rest\DeleteAction|delete]]: 删除指定的资源; +* [[yii\rest\OptionsAction|options]]: 返回支持的HTTP方法. + +所有这些操作通过[[yii\rest\ActiveController::actions()|actions()]] 方法申明,可覆盖`actions()`方法配置或禁用这些操作, +如下所示: + +```php +public function actions() +{ + $actions = parent::actions(); + + // 禁用"delete" 和 "create" 操作 + unset($actions['delete'], $actions['create']); + + // 使用"prepareDataProvider()"方法自定义数据provider + $actions['index']['prepareDataProvider'] = [$this, 'prepareDataProvider']; + + return $actions; +} + +public function prepareDataProvider() +{ + // 为"index"操作准备和返回数据provider +} +``` + +请参考独立操作类的参考文档学习哪些配置项有用。 + + +### 执行访问检查 <a name="performing-access-check"></a> + +通过RESTful APIs显示数据时,经常需要检查当前用户是否有权限访问和操作所请求的资源, +在[[yii\rest\ActiveController]]中,可覆盖[[yii\rest\ActiveController::checkAccess()|checkAccess()]]方法来完成权限检查。 + +```php +/** + * Checks the privilege of the current user. 检查当前用户的权限 + * + * This method should be overridden to check whether the current user has the privilege + * to run the specified action against the specified data model. + * If the user does not have access, a [[ForbiddenHttpException]] should be thrown. + * 本方法应被覆盖来检查当前用户是否有权限执行指定的操作访问指定的数据模型 + * 如果用户没有权限,应抛出一个[[ForbiddenHttpException]]异常 + * + * @param string $action the ID of the action to be executed + * @param \yii\base\Model $model the model to be accessed. If null, it means no specific model is being accessed. + * @param array $params additional parameters + * @throws ForbiddenHttpException if the user does not have access + */ +public function checkAccess($action, $model = null, $params = []) +{ + // 检查用户能否访问 $action 和 $model + // 访问被拒绝应抛出ForbiddenHttpException +} +``` + +`checkAccess()` 方法默认会被[[yii\rest\ActiveController]]默认操作所调用,如果创建新的操作并想执行权限检查, +应在新的操作中明确调用该方法。 + +> 提示: 可使用[Role-Based Access Control (RBAC) 基于角色权限控制组件](security-authorization.md)实现`checkAccess()`。 diff --git a/docs/guide-zh-CN/rest-resources.md b/docs/guide-zh-CN/rest-resources.md new file mode 100644 index 0000000..a3c01da --- /dev/null +++ b/docs/guide-zh-CN/rest-resources.md @@ -0,0 +1,196 @@ +资源 +========= + +RESTful 的 API 都是关于访问和操作 *资源*,可将资源看成MVC模式中的 +[模型](structure-models.md) + +在如何代表一个资源没有固定的限定,在Yii中通常使用 [[yii\base\Model]] 或它的子类(如 [[yii\db\ActiveRecord]]) +代表资源,是为以下原因: + +* [[yii\base\Model]] 实现了 [[yii\base\Arrayable]] 接口,它允许你通过RESTful API自定义你想要公开的资源数据。 +* [[yii\base\Model]] 支持 [输入验证](input-validation.md), 在你的RESTful API需要支持数据输入时非常有用。 +* [[yii\db\ActiveRecord]] 提供了强大的数据库访问和操作方面的支持,如资源数据需要存到数据库它提供了完美的支持。 + +本节主要描述资源类如何从 [[yii\base\Model]] (或它的子类) 继承并指定哪些数据可通过RESTful API返回,如果资源类没有 +继承 [[yii\base\Model]] 会将它所有的公开成员变量返回。 + + +## 字段 <a name="fields"></a> + +当RESTful API响应中包含一个资源时,该资源需要序列化成一个字符串。 +Yii将这个过程分成两步,首先,资源会被[[yii\rest\Serializer]]转换成数组, +然后,该数组会通过[[yii\web\ResponseFormatterInterface|response formatters]]根据请求格式(如JSON, XML)被序列化成字符串。 +当开发一个资源类时应重点关注第一步。 + +通过覆盖 [[yii\base\Model::fields()|fields()]] 和/或 [[yii\base\Model::extraFields()|extraFields()]] 方法, +可指定资源中称为 *字段* 的数据放入展现数组中,两个方法的差别为前者指定默认包含到展现数组的字段集合, +后者指定由于终端用户的请求包含 `expand` 参数哪些额外的字段应被包含到展现数组,例如, + + +``` +// 返回fields()方法中申明的所有字段 +http://localhost/users + +// 只返回fields()方法中申明的id和email字段 +http://localhost/users?fields=id,email + +// 返回fields()方法申明的所有字段,以及extraFields()方法中的profile字段 +http://localhost/users?expand=profile + +// 返回回fields()和extraFields()方法中提供的id, email 和 profile字段 +http://localhost/users?fields=id,email&expand=profile +``` + + +### 覆盖 `fields()` 方法 <a name="overriding-fields"></a> + +[[yii\base\Model::fields()]] 默认返回模型的所有属性作为字段, +[[yii\db\ActiveRecord::fields()]] 只返回和数据表关联的属性作为字段。 + +可覆盖 `fields()` 方法来增加、删除、重命名、重定义字段,`fields()` 的返回值应为数组,数组的键为字段名 +数组的值为对应的字段定义,可为属性名或返回对应的字段值的匿名函数,特殊情况下,如果字段名和属性名相同, +可省略数组的键,例如 + +```php +// 明确列出每个字段,适用于你希望数据表或模型属性修改时不导致你的字段修改(保持后端API兼容性) +public function fields() +{ + return [ + // 字段名和属性名相同 + 'id', + // 字段名为"email", 对应的属性名为"email_address" + 'email' => 'email_address', + // 字段名为"name", 值由一个PHP回调函数定义 + 'name' => function ($model) { + return $model->first_name . ' ' . $model->last_name; + }, + ]; +} + +// 过滤掉一些字段,适用于你希望继承父类实现同时你想屏蔽掉一些敏感字段 +public function fields() +{ + $fields = parent::fields(); + + // 删除一些包含敏感信息的字段 + unset($fields['auth_key'], $fields['password_hash'], $fields['password_reset_token']); + + return $fields; +} +``` + +> 警告: 模型的所有属性默认会被包含到API结果中,应检查数据确保没包含敏感数据,如果有敏感数据, +> 应覆盖`fields()`过滤掉,在上述例子中,我们选择过滤掉 `auth_key`, `password_hash` 和 `password_reset_token`. + + +### 覆盖 `extraFields()` 方法 <a name="overriding-extra-fields"></a> + +[[yii\base\Model::extraFields()]] 默认返回空值,[[yii\db\ActiveRecord::extraFields()]] 返回和数据表关联的属性。 + +`extraFields()` 返回的数据格式和 `fields()` 相同,一般`extraFields()` 主要用于指定哪些值为对象的字段, +例如,给定以下字段申明 + +```php +public function fields() +{ + return ['id', 'email']; +} + +public function extraFields() +{ + return ['profile']; +} +``` + +`http://localhost/users?fields=id,email&expand=profile` 的请求可能返回如下JSON 数据: + +```php +[ + { + "id": 100, + "email": "100@example.com", + "profile": { + "id": 100, + "age": 30, + } + }, + ... +] +``` + + +## 链接 <a name="links"></a> + +[HATEOAS](http://en.wikipedia.org/wiki/HATEOAS), 是Hypermedia as the Engine of Application State的缩写, +提升RESTful API 应返回允许终端用户访问的资源操作的信息,HATEOAS 的目的是在API中返回包含相关链接信息的资源数据。 + +资源类通过实现[[yii\web\Linkable]] 接口来支持HATEOAS,该接口包含方法 [[yii\web\Linkable::getLinks()|getLinks()]] 来返回 +[[yii\web\Link|links]] 列表,典型情况下应返回包含代表本资源对象URL的 `self` 链接,例如 + +```php +use yii\db\ActiveRecord; +use yii\web\Link; +use yii\web\Linkable; +use yii\helpers\Url; + +class User extends ActiveRecord implements Linkable +{ + public function getLinks() + { + return [ + Link::REL_SELF => Url::to(['user/view', 'id' => $this->id], true), + ]; + } +} +``` + +当响应中返回一个`User` 对象,它会包含一个 `_links` 单元表示和用户相关的链接,例如 + +``` +{ + "id": 100, + "email": "user@example.com", + // ... + "_links" => [ + "self": "https://example.com/users/100" + ] +} +``` + + +## 集合 <a name="collections"></a> + +资源对象可以组成 *集合*,每个集合包含一组相同类型的资源对象。 + +集合可被展现成数组,更多情况下展现成 [data providers](output-data-providers.md). +因为data providers支持资源的排序和分页,这个特性在 RESTful API 返回集合时也用到,例如This is because data providers support sorting and pagination +如下操作返回post资源的data provider: + +```php +namespace app\controllers; + +use yii\rest\Controller; +use yii\data\ActiveDataProvider; +use app\models\Post; + +class PostController extends Controller +{ + public function actionIndex() + { + return new ActiveDataProvider([ + 'query' => Post::find(), + ]); + } +} +``` + +当在RESTful API响应中发送data provider 时, [[yii\rest\Serializer]] 会取出资源的当前页并组装成资源对象数组, +[[yii\rest\Serializer]] 也通过如下HTTP头包含页码信息: + +* `X-Pagination-Total-Count`: 资源所有数量; +* `X-Pagination-Page-Count`: 页数; +* `X-Pagination-Current-Page`: 当前页(从1开始); +* `X-Pagination-Per-Page`: 每页资源数量; +* `Link`: 允许客户端一页一页遍历资源的导航链接集合. + +可在[快速入门](rest-quick-start.md#trying-it-out) 一节中找到样例.