Commit 48a12559 by Qiang Xue

rest api WIP

parent 62665602
<?php
namespace common\models;
use yii\base\NotSupportedException;
use yii\db\ActiveRecord;
use yii\helpers\Security;
use yii\web\IdentityInterface;
......@@ -72,6 +73,14 @@ class User extends ActiveRecord implements IdentityInterface
}
/**
* @inheritdoc
*/
public static function findIdentityByToken($token)
{
throw new NotSupportedException('"findIdentityByToken" is not implemented.');
}
/**
* Finds user by username
*
* @param string $username
......
......@@ -8,6 +8,7 @@ class User extends \yii\base\Object implements \yii\web\IdentityInterface
public $username;
public $password;
public $authKey;
public $apiKey;
private static $users = [
'100' => [
......@@ -15,12 +16,14 @@ class User extends \yii\base\Object implements \yii\web\IdentityInterface
'username' => 'admin',
'password' => 'admin',
'authKey' => 'test100key',
'apiKey' => '100-apikey',
],
'101' => [
'id' => '101',
'username' => 'demo',
'password' => 'demo',
'authKey' => 'test101key',
'apiKey' => '101-apikey',
],
];
......@@ -33,6 +36,19 @@ class User extends \yii\base\Object implements \yii\web\IdentityInterface
}
/**
* @inheritdoc
*/
public static function findIdentityByToken($token)
{
foreach (self::$users as $user) {
if ($user['apiKey'] === $token) {
return new static($user);
}
}
return null;
}
/**
* Finds user by username
*
* @param string $username
......
......@@ -5,7 +5,7 @@ Authentication is the act of verifying who a user is, and is the basis of the lo
In Yii, this entire process is performed semi-automatically, leaving the developer to merely implement [[yii\web\IdentityInterface]], the most important class in the authentication system. Typically, implementation of `IdentityInterface` is accomplished using the `User` model.
You can find a full featured example of authentication in the
You can find a fully featured example of authentication in the
[advanced application template](installation.md). Below, only the interface methods are listed:
```php
......@@ -25,6 +25,17 @@ class User extends ActiveRecord implements IdentityInterface
}
/**
* Finds an identity by the given token.
*
* @param string $token the token to be looked for
* @return IdentityInterface|null the identity object that matches the given token.
*/
public static function findIdentityByToken($token)
{
return static::find(['api_key' => $token]);
}
/**
* @return int|string current user ID
*/
public function getId()
......
Implementing RESTful Web Service APIs
=====================================
Yii provides a whole set of tools to greatly simplify the task of implementing RESTful Web Service APIs.
In particular, Yii provides support for the following aspects regarding RESTful APIs:
* Quick prototyping with support for common APIs for ActiveRecord;
* Response format (supporting JSON and XML by default) and API version negotiation;
* Customizable object serialization with support for selectable output fields;
* Proper formatting of collection data and validation errors;
* Efficient routing with proper HTTP verb check;
* Support `OPTIONS` and `HEAD` verbs;
* Authentication via HTTP basic;
* Authorization;
* Caching via `yii\web\HttpCache`;
* Support for HATEOAS: TBD
* Rate limiting: TBD
* Searching and filtering: TBD
* Testing: TBD
* Automatic generation of API documentation: TBD
A Quick Example
---------------
Let's use a quick example to show how to build a set of RESTful APIs using Yii.
Assume you want to expose the user data via RESTful APIs. The user data are stored in the user DB table,
and you have already created the ActiveRecord class `app\models\User` to access the user data.
First, check your `User` class for its implementation of the `findIdentityByToken()` method.
It may look like the following:
```php
class User extends ActiveRecord
{
...
public static function findIdentityByToken($token)
{
return static::find(['api_key' => $token]);
}
}
```
This means your user table has a column named `api_key` which stores API access keys for the users.
Pick up a key from the table as you will need it to access your APIs next.
Second, create a controller class `app\controllers\UserController` as follows,
```php
namespace app\controllers;
use yii\rest\ActiveController;
class UserController extends ActiveController
{
public $modelClass = 'app\models\City';
}
```
Third, modify the configuration about the `urlManager` component in your application configuration:
```php
'urlManager' => [
'enablePrettyUrl' => true,
'enableStrictParsing' => true,
'showScriptName' => false,
'rules' => [
['class' => 'yii\rest\UrlRule', 'controller' => 'user'],
],
]
```
With the above minimal amount of effort, you have already finished your task of creating the RESTful APIs
for accessing the user data. The APIs you have created include:
* `GET /users`: list all users page by page;
* `HEAD /users`: show the overview information of user listing;
* `POST /users`: create a new user;
* `GET /users/123`: return the details of the user 123;
* `HEAD /users/123`: show the overview information of user 123;
* `PATCH /users/123` and `PUT /users/123`: update the user 123;
* `DELETE /users/123`: delete the user 123;
* `OPTIONS /users`: show the supported verbs regarding endpoint `/users`;
* `OPTIONS /users/123`: show the supported verbs regarding endpoint `/users/123`.
You may access your APIs with the `curl` command like the following,
```
curl -i -u "Your-API-Key:" -H "Accept:application/json" -d "_method=GET" "http://localhost/users"
```
> Tip: You may also access your API via Web browser. You will be asked
> to enter a username and password. Fill in the username field with the API key you obtained
> previously and leave the password field blank.
Try changing the acceptable content type to be `application/xml`, and you will see the result
is returned in XML format.
Using the `fields` and `expand` parameters, you can request to return a subset of the fields in the result.
For example, the following URL will only return the `id` and `email` columns in the result:
```
http://localhost/users?fields=id,email
```
You may have noticed that the result of `http://localhost/users` includes some sensitive columns,
such as `password_hash`, `auth_key`. You certainly do not want these to appear in your API result.
To filter these data out, modify the `User` class as follows,
```php
class User extends ActiveRecord
{
public function fields()
{
$fields = parent::fields();
unset($fields['auth_key'], $fields['password_hash'], $fields['password_reset_token']);
return $fields;
}
}
```
In the following subsections, we will explain in more details about implementing RESTful APIs.
TBD
......@@ -121,7 +121,6 @@ class ActiveController extends Controller
'create' => ['POST'],
'update' => ['PUT', 'PATCH'],
'delete' => ['DELETE'],
'options' => ['OPTIONS'],
];
}
......
......@@ -19,7 +19,7 @@ use yii\web\VerbFilter;
* Controller implements the following steps in a RESTful API request handling cycle:
*
* 1. Resolving response format and API version number (see [[supportedFormats]], [[supportedVersions]] and [[version]]);
* 2. Validating request method (see [[verbs()]]);
* 2. Validating request method (see [[verbs()]]).
* 3. Authenticating user (see [[authenticate()]]);
* 4. Formatting response data (see [[serializeData()]]).
*
......@@ -150,10 +150,16 @@ class Controller extends \yii\web\Controller
/**
* Authenticates the user.
* This method implements the user authentication based on HTTP basic authentication.
* @throws UnauthorizedHttpException if the user is not authenticated successfully
*/
protected function authenticate()
{
$apiKey = Yii::$app->getRequest()->getAuthUser();
if ($apiKey === null || !Yii::$app->getUser()->loginByToken($apiKey)) {
Yii::$app->getResponse()->getHeaders()->set('WWW-Authenticate', 'Basic realm="api"');
throw new UnauthorizedHttpException($apiKey === null ? 'Please provide an API key.' : 'You are requesting with an invalid API key.');
}
}
/**
......
......@@ -20,11 +20,12 @@ class OptionsAction extends \yii\base\Action
/**
* @var array the HTTP verbs that are supported by the collection URL
*/
public $collectionOptions = ['GET', 'POST'];
public $collectionOptions = ['GET', 'POST', 'HEAD', 'OPTIONS'];
/**
* @var array the HTTP verbs that are supported by the resource URL
*/
public $resourceOptions = ['GET', 'PUT', 'PATCH', 'DELETE'];
public $resourceOptions = ['GET', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
/**
* Responds to the OPTIONS request.
......@@ -32,6 +33,9 @@ class OptionsAction extends \yii\base\Action
*/
public function run($id = null)
{
if (Yii::$app->getRequest()->getMethod() !== 'OPTIONS') {
Yii::$app->getResponse()->setStatusCode(405);
}
$options = $id === null ? $this->collectionOptions : $this->resourceOptions;
Yii::$app->getResponse()->getHeaders()->set('Allow', implode(', ', $options));
}
......
......@@ -13,7 +13,7 @@ use yii\helpers\Inflector;
use yii\web\CompositeUrlRule;
/**
* UrlRule is provided to simplify creation of URL rules for RESTful API support.
* UrlRule is provided to simplify the creation of URL rules for RESTful API support.
*
* The simplest usage of UrlRule is to declare a rule like the following in the application configuration,
*
......@@ -28,11 +28,11 @@ use yii\web\CompositeUrlRule;
*
* - `'PUT,PATCH users/<id>' => 'user/update'`: update a user
* - `'DELETE users/<id>' => 'user/delete'`: delete a user
* - `'GET,HEAD users/<id>' => 'user/view'`: return the details of a user (or the overview for HEAD requests)
* - `'OPTIONS users/<id>' => 'user/options'`: return the supported methods for `users/<id>`
* - `'GET,HEAD users/<id>' => 'user/view'`: return the details/overview/options of a user
* - `'POST users' => 'user/create'`: create a new user
* - `'GET,HEAD users' => 'user/index'`: return a list of users (or the overview for HEAD requests)
* - `'OPTIONS users' => 'user/options'`: return the supported methods for `users`
* - `'GET,HEAD users' => 'user/index'`: return a list/overview/options of users
* - `'users/<id>' => 'user/options'`: process all unhandled verbs of a user
* - `'users' => 'user/options'`: process all unhandled verbs of user collection
*
* You may configure [[only]] and/or [[except]] to disable some of the above rules.
* You may configure [[patterns]] to completely redefine your own list of rules.
......@@ -98,23 +98,24 @@ class UrlRule extends CompositeUrlRule
* @see patterns
*/
public $tokens = [
'{id}' => '<id:\\d+[\\d,]*>',
'{id}' => '<id:\\d[\\d,]*>',
];
/**
* @var array list of possible patterns and the corresponding actions for creating the URL rules.
* The keys are the patterns and the values are the corresponding actions.
* The format of patterns is `Verbs Path`, where `Verbs` stands for a list of HTTP verbs separated
* by comma (without space). `Path` is optional. It will be prefixed with [[prefix]]/[[controller]]/,
* The format of patterns is `Verbs Pattern`, where `Verbs` stands for a list of HTTP verbs separated
* by comma (without space). If `Verbs` is not specified, it means all verbs are allowed.
* `Pattern` is optional. It will be prefixed with [[prefix]]/[[controller]]/,
* and tokens in it will be replaced by [[tokens]].
*/
public $patterns = [
'GET,HEAD {id}' => 'view',
'PUT,PATCH {id}' => 'update',
'DELETE {id}' => 'delete',
'OPTIONS {id}' => 'options',
'GET,HEAD' => 'index',
'GET,HEAD {id}' => 'view',
'POST' => 'create',
'OPTIONS' => 'options',
'GET,HEAD' => 'index',
'{id}' => 'options',
'' => 'options',
];
public $ruleConfig = [
'class' => 'yii\web\UrlRule',
......@@ -156,7 +157,7 @@ class UrlRule extends CompositeUrlRule
$prefix = trim($this->prefix . '/' . $urlName, '/');
foreach ($this->patterns as $pattern => $action) {
if (!isset($except[$action]) && (empty($only) || isset($only[$action]))) {
$rules[] = $this->createRule($pattern, $prefix, $controller . '/' . $action);
$rules[$urlName][] = $this->createRule($pattern, $prefix, $controller . '/' . $action);
}
}
}
......@@ -172,23 +173,61 @@ class UrlRule extends CompositeUrlRule
*/
protected function createRule($pattern, $prefix, $action)
{
if (($pos = strpos($pattern, ' ')) !== false) {
$verbs = substr($pattern, 0, $pos);
$pattern = strtr(substr($pattern, $pos + 1), $this->tokens);
$verbs = 'GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS';
if (preg_match("/^((?:($verbs),)*($verbs))(?:\\s+(.*))?$/", $pattern, $matches)) {
$verbs = explode(',', $matches[1]);
$pattern = isset($matches[4]) ? $matches[4] : '';
} else {
$verbs = $pattern;
$pattern = '';
$verbs = [];
}
$config = $this->ruleConfig;
$config['verb'] = explode(',', $verbs);
$config['pattern'] = rtrim($prefix . '/' . $pattern, '/');
$config['verb'] = $verbs;
$config['pattern'] = rtrim($prefix . '/' . strtr($pattern, $this->tokens), '/');
$config['route'] = $action;
if (strcasecmp($verbs, 'GET')) {
if (!in_array('GET', $verbs)) {
$config['mode'] = \yii\web\UrlRule::PARSING_ONLY;
}
$config['suffix'] = $this->suffix;
return Yii::createObject($config);
}
/**
* @inheritdoc
*/
public function parseRequest($manager, $request)
{
$pathInfo = $request->getPathInfo();
foreach ($this->rules as $urlName => $rules) {
if (strpos($pathInfo, $urlName) !== false) {
foreach ($rules as $rule) {
/** @var \yii\web\UrlRule $rule */
if (($result = $rule->parseRequest($manager, $request)) !== false) {
Yii::trace("Request parsed with URL rule: {$rule->name}", __METHOD__);
return $result;
}
}
}
}
return false;
}
/**
* @inheritdoc
*/
public function createUrl($manager, $route, $params)
{
foreach ($this->controller as $urlName => $controller) {
if (strpos($route, $controller) !== false) {
foreach ($this->rules[$urlName] as $rule) {
/** @var \yii\web\UrlRule $rule */
if (($url = $rule->createUrl($manager, $route, $params)) !== false) {
return $url;
}
}
}
}
return false;
}
}
......@@ -25,11 +25,9 @@ class ViewAction extends Action
public function run($id)
{
$model = $this->findModel($id);
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this, $model);
}
return $model;
}
}
......@@ -52,6 +52,14 @@ interface IdentityInterface
*/
public static function findIdentity($id);
/**
* Finds an identity by the given secrete token.
* @param string $token the secrete token
* @return IdentityInterface the identity object that matches the given token.
* Null should be returned if such an identity cannot be found
* or the identity is not in an active state (disabled, deleted, etc.)
*/
public static function findIdentityByToken($token);
/**
* Returns an ID that can uniquely identify a user identity.
* @return string|integer an ID that uniquely identifies a user identity.
*/
......
......@@ -199,7 +199,7 @@ class UrlRule extends Object implements UrlRuleInterface
return false;
}
if ($this->verb !== null && !in_array($request->getMethod(), $this->verb, true)) {
if (!empty($this->verb) && !in_array($request->getMethod(), $this->verb, true)) {
return false;
}
......
......@@ -209,6 +209,15 @@ class User extends Component
return !$this->getIsGuest();
}
public function loginByToken($token)
{
/** @var IdentityInterface $class */
$class = $this->identityClass;
$identity = $class::findIdentityByToken($token);
$this->setIdentity($identity);
return $identity;
}
/**
* Logs in a user by cookie.
*
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment