2019年1月31日 星期四

RESTful API設計

RESTful API設計:

*建立新專案
*.env的APP_KEY問題
*vs code找專案中的檔案
*設定.env
*建立model和migration
*設定migration
*建立資料庫table與問題處理
*設定model
*自動產生測試用資料:seeder
*建立controller
*設定controller
*設定路由
*時區、語系設定
*語系檔案設定
*異常處理(app\Exceptions\Handler.php)
*API認證
*添加api_token欄位到users表
*創建註冊介面
*創建登錄介面
*創建退出接口
*使用middleware限制訪問:(方法2)
*相關程式
*用php session
*如何使用Trait?

=========================
*建立新專案:

laravel new 專案名稱

laravel new laravel_test2

========================
*.env的APP_KEY問題:

RuntimeException
No application encryption key has been specified.

解決:
php artisan key:generate

========================
*vs code找專案中的檔案:

ctrl + P

========================
*設定.env:

DB_DATABASE=apidemo
DB_USERNAME=root
DB_PASSWORD=

========================
*建立model和migration:

php artisan make:model Article -m

========================
*設定migration:
database\migrations\2019_01_21_052638_create_articles_table.php

public function up()
{
    Schema::create('articles', function (Blueprint $table) {
        $table->increments('id');
        $table->string('title',30);
        $table->text('body');
        $table->timestamps();
    });
}

========================
*建立資料庫table與問題處理:

php artisan migrate

---------------------
*出現類似以下的問題:(Specified key was too long)

Migrating: 2014_10_12_000000_create_users_table

Illuminate\Database\QueryException  : SQLSTATE[42000]:
Syntax error or access violation: 1071 Specified key was too long;
max key length is 1000 bytes
(SQL: alter table `users` add unique `users_email_unique`(`email`))

*解決方法:
在每一個 migration 的 $table->string 加入長度(要小於191)

例如:
$table->string('name',30);
$table->string('email',50)->unique();

下面2個預設的檔案也要處理:
database\migrations\2014_10_12_000000_create_users_table.php
database\migrations\2014_10_12_100000_create_password_resets_table.php

========================
*設定model:

class Article extends Model
{
    protected $fillable = ['title', 'body'];
}

========================
*自動產生測試用資料:seeder

產生seeder檔案: ArticlesTableSeeder

php artisan make:seeder ArticlesTableSeeder

檔案位置:

/database/seeds

-----------------
設定seeder:

database\seeds\ArticlesTableSeeder.php

<?php
use Illuminate\Database\Seeder;
use App\Article;

class ArticlesTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        DB::table('articles')->truncate();

        for ($i=0; $i<50; $i++) {
            Article::create([
                'title'=>str_random(10),
                'body'=>str_random(255)
            ]);
        }
    }
}

-----------------
執行seeder: ArticlesTableSeeder

php artisan db:seed --class=ArticlesTableSeeder

-----------------
-----------------
重複上述過程建立一個 UsersTableSeeder 檔案:

php artisan make:seeder UsersTableSeeder

檔案位置:

/database/seeds

-----------------
設定seeder:

database\seeds\UsersTableSeeder.php

<?php

use Illuminate\Database\Seeder;
use App\User;

class UsersTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        //清除users table
        User::truncate();

        $faker=\Faker\Factory::create();

        $password=Hash::make('toptal');

        User::create([
            'name'=>'Administrator',
            'email'=>'admin@test.com',
            'password'=>$password
        ]);

        for ($i=0; $i<10 ; $i++) {
            User::create([
                'name'=>$faker->name,
                'email'=>$faker->email,
                'password'=>$password
            ]);
        }
    }
}

-----------------
設定DatabaseSeeder.php:

<?php
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call(UsersTableSeeder::class);
        $this->call(ArticlesTableSeeder::class);
    }
}

-----------------
執行全部的seeder檔案:

php artisan db:seed

========================
*建立controller:

php artisan make:controller api/ArticleController -r

========================
*設定controller:

app\Http\Controllers\api\ArticleController.php

<?php

namespace App\Http\Controllers\api;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Article;

class ArticleController extends Controller
{
    //查全部資料
    public function index()
    {
        // return Article::all();
        return response()->json(['status' => 'success', 'posts' => Article::all()]);
    }

    //查單筆資料
    public function show(Article $article)
    {
        // return $article;

        return response()->json(['status' => 'success', 'post' => $article]);

        //異常處理要寫在 app\Exceptions\Handler.php
        // return response()->json(['status' => 'error', 'message' => 'Id Not Found']);
    }

    //新增資料
    public function store(Request $request)
    {
        $article=Article::create($request->all());

        // return response()->json($article,201);
        return response()->json(['status' => 'success', 'posts' => $article]);
    }

    //修改資料
    public function update(Request $request, Article $article)
    {
        $article->update($request->all());

        // return response()->json($article,200);
        return response()->json(['status' => 'success', 'posts' => $article]);

        //異常處理要寫在 app\Exceptions\Handler.php
        // return response()->json(['status' => 'error', 'message' => 'Id Not Found']);
    }

    //刪除資料
    public function delete(Article $article)
    {
        $article->delete();

        // return response()->json(null,204);
        return response()->json(['status' => 'success']);

        //異常處理要寫在 app\Exceptions\Handler.php
        // return response()->json(['status' => 'error', 'message' => 'Id Not Found']);
    }
}

========================
*設定路由:

routes/api.php

Route::get('articles','api\ArticleController@index');
Route::get('articles/{article}','api\ArticleController@show');
Route::post('articles','api\ArticleController@store');
Route::put('articles/{article}','api\ArticleController@update');
Route::delete('articles/{article}','api\ArticleController@delete');

========================
*HTTP 狀態碼:

200 :OK,標準的響應成功狀態碼
201 :Object created,用於 store 操作
204 :No content,操作執行成功,但是沒有返回任何內容
206 :Partial content,返回部分資源時使用
400 :Bad request,請求驗證失敗
401 :Unauthorized,用户需要認證
403 :Forbidden,用户認證通過但是沒有權限執行該操作
404 :Not found,請求資源不存在
500 :Internal server error,通常我們並不會顯示返回這個狀態碼,除非進程異常中斷
503 :Service unavailable,一般也不會顯示返回,通常用於排查問題用

========================
*時區、語系設定:

config/app.php

時區:
'timezone' => 'Asia/Taipei',

語系:
'locale' => 'zh-TW',
'fallback_locale' => 'zh-TW',

-----------------
*語系檔案設定:

resources/lang

新增資料夾: zh-TW
resources/lang/zh-TW

複製英文版(en)的檔案validation.php到zh-TW資料夾:
resources/lang/zh-TW/validation.php

改validation.php內容為中文

========================
*異常處理(app\Exceptions\Handler.php):

當查資料時id不存在
查app\Http\Controllers\api\ArticleController.php
的public function show(Article $article)
要回傳json,不要回傳網頁html。

修改:
app\Exceptions\Handler.php

use Illuminate\Database\Eloquent\ModelNotFoundException;

public function render($request, Exception $exception)
{
    if ($exception instanceof ModelNotFoundException) {
        return response()->json(['status' => 'error', 'message' => 'Id Not Found']);
    }
   
    return parent::render($request, $exception); //原本設定:回傳html
}

========================
*API認證:

------------------
*添加api_token欄位到users表:

php artisan make:migration --table=users adds_api_token_to_users_table

*設定migration:
database\migrations\2019_01_25_122630_adds_api_token_to_users_table.php

public function up()
{
    Schema::table('users', function (Blueprint $table) {
        $table->string('api_token', 60)->unique()->nullable();
    });
}

public function down()
{
    Schema::table('users', function (Blueprint $table) {
        $table->dropColumn(['api_token']);
    });
}

*執行migrate:
php artisan migrate

------------------
*創建註冊介面:

我們使用 RegisterController 來根據註冊請求返回正確的響應。
儘管 Laravel 開箱提供了認證功能,但是我們還是需要對其進行調整
以便返回我們想要的響應數據。該控制器使用 RegistersUsers 來實現註冊,
實現邏輯如下:
app\Http\Controllers\Auth\RegisterController.php

use Illuminate\Http\Request; //自加
use Illuminate\Auth\Events\Registered; //自加

public function register(Request $request)
{
    // Here the request is validated. The validator method is located
    // inside the RegisterController, and makes sure the name, email
    // password and password_confirmation fields are required.

    $this->validator($request->all())->validate();

    // A Registered event is created and will trigger any relevant
    // observers, such as sending a confirmation email or any
    // code that needs to be run as soon as the user is created.

    event(new Registered($user = $this->create($request->all())));

    // After the user is created, he's logged in.

    $this->guard()->login($user);

    // And finally this is the hook that we want. If there is no
    // registered() method or it returns null, redirect him to
    // some other URL. In our case, we just need to implement
    // that method to return the correct response.

    return $this->registered($request, $user)?: redirect($this->redirectPath());
}

我們只需要在 RegisterController 中實現 registered 方法即可。
該方法接收 $request 和 $user 參數:

protected function registered(Request $request, $user)
{
    $user->generateToken();

    return response()->json(['data' => $user->toArray()], 201);
}

---------------------------------
在 routes/api.php 中註冊路由如下:
Route::post('register', 'Auth\RegisterController@register');

---------------------------------
在上面的示例代碼中,我們調用了 User model 上的生成token方法,
該方法現在不存在,需要手動添加:

app\User.php

原文才正確:
Laravel API Tutorial: How to Build and Test a RESTful API
https://www.toptal.com/laravel/restful-laravel-api-tutorial

class User extends Authenticatable
{
    ...
    public function generateToken()
    {
        $this->api_token = str_random(60);
        $this->save();

        return $this->api_token;
    }
}

至此,註冊介面編寫完成,用户現在可以通過註冊介面進行註冊了,
感謝 Laravel 開箱提供的認證字段驗證功能,如果你需要調整驗證規則
的話可以到 RegisterController 中查看 validator 方法。

---------------------------------
curl參考:

至此,註冊介面編寫完成,用户現在可以通過註冊介面進行註冊了,
感謝 Laravel 開箱提供的認證字段驗證功能,如果你需要調整驗證規則
的話可以到 RegisterController 中查看 validator 方法。

curl -X POST http://127.0.0.1:8000/api/register \
    -H "Accept: application/json" \
    -H "Content-Type: application/json" \
    -d '{"name": "學院君", "email": "admin@laravelacademy.org", "password": "test123", "password_confirmation": "test123"}'

========================
*創建登錄介面:

app\Http\Controllers\Auth\LoginController.php

和註冊接口一樣,可以編輯 LoginController 控制器來支持 API 認證。
為此,我們需要在 LoginController 覆蓋 AuthenticatesUsers trait 提供的
login 方法:

use Illuminate\Http\Request; //自加

public function login(Request $request)
{
    $this->validateLogin($request);

    if ($this->attemptLogin($request)) {
        $user = $this->guard()->user();
        $user->generateToken();

        return response()->json([
            'data' => $user->toArray(),
        ]);
    }

    return $this->sendFailedLoginResponse($request);
}

---------------------
然後在 routes/api.php 中註冊登錄路由:

Route::post('login', 'Auth\LoginController@login');

---------------------
現在,基於我們上面註冊的新用户,我們來測試下登錄接口:

curl -X POST http://apidemo.test/api/login \
    -H "Accept: application/json" \
    -H "Content-type: application/json" \
    -d "{\"email\": \"admin@laravelacademy.org\", \"password\": \"test123\" }"

---------------------
後面就可以拿着這個 api_token 作為令牌來請求需要認證的資源了。
使用我們現有的策略,請求認證資源時,如果沒有 token 或 token 錯誤,
用户將會接收到未認證響應(401)。

========================
*創建退出接口:

為了形成完整閉環,下面我們來編寫退出登錄接口,
實現思路是用户發起退出登錄請求時,我們將其對應的 token 字段值從數據庫移除。

---------------------
首先,在 routes/api.php 中註冊路由:

Route::post('logout', 'Auth\LoginController@logout');

---------------------
然後在 Auth\LoginController.php 中編寫 logout 方法:

use Illuminate\Support\Facades\Auth; //自加

public function logout(Request $request)
{
    $user = Auth::guard('api')->user();

    if ($user) {
        $user->api_token = null;
        $user->save();
    }

    session_start();
    $_SESSION['user_id']=null;

    return response()->json(['data' => 'User logged out.'], 200);
}

使用該策略,一旦退出,用户的所有令牌都會失效,
訪問需要認證的接口都會拒絕訪問(通過中間件實現),
這需要和前端配合來避免用户在沒有訪問任何內容的權限下保持登錄狀態。

========================
========================
**此段 "方法1" 有問題!!
可改用 "方法2" 取代!!

*使用middleware限制訪問:(方法1)

api_token 創建之後,我們就可以在路由文檔中應用認證中間件了:

Route::middleware('auth:api')
    ->get('/user', function (Request $request) {
        return $request->user();
    });

我們可以使用 $request->user() 或 Auth 門面訪問當前用户:

Auth::guard('api')->user(); // 登錄用户實例
Auth::guard('api')->check(); // 用户是否登錄
Auth::guard('api')->id(); // 登錄用户ID

接下來,我們將之前定義的文章相關路由進行分組:

Route::group(['middleware' => 'auth:api'], function() {
    Route::get('articles', 'ArticleController@index');
    Route::get('articles/{article}', 'ArticleController@show');
    Route::post('articles', 'ArticleController@store');
    Route::put('articles/{article}', 'ArticleController@update');
    Route::delete('articles/{article}', 'ArticleController@delete');
});

這樣就不需要為每個路由設置中間件,現在看來雖然節省不了多少時間,
但隨着應用體量的增長,這樣做的好處是保持路由的DRY(Don’t Repeat Yourself)。

再訪問文章接口就需要認證了

========================
========================
*使用middleware限制訪問:(方法2)

目標:用middleware判斷權限

1.建立一個middleware:
AuthUserMiddleware
驗證是否已登入

php artisan make:middleware AuthUserMiddleware

檔案位置:
app\Http\Middleware\AuthUserMiddleware.php

<?php
namespace App\Http\Middleware;

use Closure;
use App\Article; //model

class AuthUserMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        //預設不允許存取
        $is_allow_access=false;

        //取得會員編號
        // $user_id=session()->get('user_id');
        session_start();
        $user_id=$_SESSION['user_id'];

        //session有會員編號,允許存取
        if(!is_null($user_id)){
            $is_allow_access=true;
        }

        //不允許存取,回傳"未登入"
        if(!$is_allow_access){
            return response()->json(['status' => '未登入']);
        }

        return $next($request);
    }
}

---------------------
2.註冊middleware:
app/Http/Kernel.php

protected $routeMiddleware = [
    'user.auth' => \App\Http\Middleware\AuthUserMiddleware::class,
];

---------------------
3.設定route:
routes\api.php

*群組指定Middleware:
Route::group(['middleware'=>'user.auth'],function(){
    Route::get('articles','api\ArticleController@index');
    Route::get('articles/{article}','api\ArticleController@show');
    Route::post('articles','api\ArticleController@store');
    Route::put('articles/{article}','api\ArticleController@update');
    Route::delete('articles/{article}','api\ArticleController@delete');
});

*個別指定Middleware:
Route::get('articles','api\ArticleController@index')->middleware(['user.auth']);

---------------------
4.LoginController的login函數要加入session:
app\Http\Controllers\Auth\LoginController.php

<?php
namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;

use Illuminate\Http\Request; //自加
use Illuminate\Support\Facades\Auth; //自加

class LoginController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles authenticating users for the application and
    | redirecting them to your home screen. The controller uses a trait
    | to conveniently provide its functionality to your applications.
    |
    */

    use AuthenticatesUsers;
    //vendor\laravel\framework\src\Illuminate\Foundation\Auth\AuthenticatesUsers.php

    /**
     * Where to redirect users after login.
     *
     * @var string
     */
    protected $redirectTo = '/home';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest')->except('logout');
    }

    //自加
    public function login(Request $request)
    {
        //vendor\laravel\framework\src\Illuminate\Foundation\Auth\AuthenticatesUsers.php

        $this->validateLogin($request);

        if ($this->attemptLogin($request)) {
            $user = $this->guard()->user();
            $user->generateToken();

            // return $user->generateToken();

            // $userx=session()->put('user_id',$user->id); //驗證登入
            session_start();
            $_SESSION['user_id']=$user->id;

            return response()->json([
                'data' => $user->toArray(),
                // 'data' => $_SESSION['user_id'],
            ]);
        }

        return $this->sendFailedLoginResponse($request);
    }

    //自加
    public function logout(Request $request)
    {
        $user = Auth::guard('api')->user();

        if ($user) {
            $user->api_token = null;
            $user->save();
        }

        session_start();
        $_SESSION['user_id']=null;

        return response()->json(['data' => 'User logged out.'], 200);
    }
}

========================
相關程式:

*route:
routes\api.php
app\Http\Middleware\AuthUserMiddleware.php
app\Http\Kernel.php

*controller:
app\Http\Controllers\api\ArticleController.php
app\Http\Controllers\Auth\RegisterController.php
app\Http\Controllers\Auth\LoginController.php
app\Exceptions\Handler.php

*model:
app\Article.php
app\User.php

*migration:
database\migrations\2019_01_21_052638_create_articles_table.php
database\migrations\2014_10_12_000000_create_users_table.php
database\migrations\2019_01_25_122630_adds_api_token_to_users_table.php

*seeder:
database\seeds\ArticlesTableSeeder.php
database\seeds\UsersTableSeeder.php
database\seeds\DatabaseSeeder.php

========================
問題:
app\Http\Controllers\Auth\LoginController.php
無法session???

用php session:
session_start();
$_SESSION['user_id']=$user->id;

Authentication:
https://learnku.com/docs/laravel/5.7/authentication/2269

**
app\Http\Controllers\Auth\LoginController.php 用的函數
與下面AuthenticatesUsers.php有關
vendor\laravel\framework\src\Illuminate\Foundation\Auth\AuthenticatesUsers.php

========================
如何使用Trait?
https://oomusou.io/php/php-trait/

trait AuthenticatesUsers:

app\Http\Controllers\Auth\LoginController.php 用的函數
與下面trait AuthenticatesUsers.php有關
vendor\laravel\framework\src\Illuminate\Foundation\Auth\AuthenticatesUsers.php

========================
Laravel API 系列教程(一): 基於 Laravel 5.5 構建 & 測試 RESTful API
https://hk.saowen.com/a/967f6c7095e9432e79d89dc428b7932c416a74b54cc8bc0b2b050d070fae3d5e

原文才正確:
Laravel API Tutorial: How to Build and Test a RESTful API

沒有留言:

張貼留言