2018年2月24日 星期六

Laravel 將 axios 的 header 預設包含 Authorization

方便每一頁都能自動包含 api_token 我們採取這樣的做法:

先在最基本的 layout 裡面加入 meta data,可以直接修改
resources/views/layouts/app.blade.php
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">

@if (auth()->check())
<meta name="api-token" content="{{ auth()->user()->api_token }}">
@endif

當使用者是認證過的,每一頁就會塞 token 在裡面。

另外在 bootstrap.js 裡面加入 header 資訊
resources/assets/js/bootstrap.js
let token = document.head.querySelector('meta[name="csrf-token"]');

if (token) {
    window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
    console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}

let api_token = document.head.querySelector('meta[name="api-token"]');

if (api_token) {
  window.axios.defaults.headers.common['Authorization'] = 'Bearer ' + api_token.content;
}

修改過了 bootstrap.js 所以我們要重新 compile 來產生 public/js/app.js 之後頁面才會正常作用。生產的方式請參考下方的資料。

新的 app.js 生成之後記得重新載入頁面,以免因為 cache 的關係而導致結果不如預期。

接著我們使用的 axios 送 request 就會自動在 header 裡面帶上 api_token 囉。

參考資料:
https://laravel.com/docs/5.6/mix



Laravel 使用 auth:api 來做 route api 的認證

在 config/auth.php 裡面就已經描述好兩種認證方式:
'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver' => 'token',
        'provider' => 'users',
    ],
],

如果我們建立 project 之後做了 make:auth 就直接建立 users 資料表的話。
不管怎麼樣 Route::middleware('auth:api') 都是回應 401 認證不會過的。

可以先看到 vendor/laravel/framework/src/Illuminate/Auth/TokenGuard.php 有描述。

public function __construct(UserProvider $provider, Request $request)
{
    $this->request = $request;
    $this->provider = $provider;
    $this->inputKey = 'api_token';
    $this->storageKey = 'api_token';
}

public function user()
{
    // If we've already retrieved the user for the current request we can just
    // return it back immediately. We do not want to fetch the user data on
    // every call to this method because that would be tremendously slow.
    if (! is_null($this->user)) {
        return $this->user;
    }

    $user = null;

    $token = $this->getTokenForRequest();

    if (! empty($token)) {
        $user = $this->provider->retrieveByCredentials(
            [$this->storageKey => $token]
        );
    }

    return $this->user = $user;
}
$token = 這邊會從 request 那裡面找附帶的 token

public function getTokenForRequest()
{
    $token = $this->request->query($this->inputKey);

    if (empty($token)) {
        $token = $this->request->input($this->inputKey);
    }

    if (empty($token)) {
        $token = $this->request->bearerToken();
    }

    if (empty($token)) {
        $token = $this->request->getPassword();
    }

    return $token;
}
找尋 token 的順序為:
1. 從 query 找 $this->inputKey 參數
2. 從 input 找 $this->inputKey 參數
3. 從 header 找 bearer token
4. 從 header 找 password。註 1

1 和 2 很容易處理的,只要在送 request 之前把參數加一加就可以。
以 1 為例,我們只 url 加上 api_token 這個參數即可。
http://lara.loc/api/v1/stock?api_token=97531

回應的結果:
出現的是 QueryException,原因是 users 的資料表裡面沒有 api_token 這個欄位。
到 users 的資料表裡面加上這個欄位,並且賦予一個 unique 的值就可以了。
Schema::table('users', function (Blueprint $table) {
    $table->string('api_token', 60)->unique();
});

app/Http/Controllers/Auth/RegisterController.php
在 User::create 的時候直接亂數產生一個 api_token 給他。
protected function create(array $data)
{
    return User::create([
        'name' => $data['name'],
        'email' => $data['email'],
        'api_token' => str_random(60),
        'password' => bcrypt($data['password']),
    ]);
}

並且記得在 app/User.php 裡面,在 $fillable 添加。
protected $fillable = [
    'name', 'email', 'password', 'api_token',
];
之後註冊的使用者就會自動產生一組 api_token。

最後再回到我們送出 request 的部分:
axios.post('/api/v1/stock?api_token={{ Auth::user()->api_token }}', form)
.then(function (response) {
    console.log(response);
});
就可以順利通過認證。

也可以把 api_token 放在 header 裡面傳送:
axios.defaults.headers.common['Authorization'] = 'Bearer {{ Auth::user()->api_token }}';
axios.post('/api/v1/stock', form)
.then(function (response) {
    console.log(response);
});

註 1
vendor/symfony/http-foundation/Request.php
/**
 * Returns the password.
 *
 * @return string|null
 */
public function getPassword()
{
    return $this->headers->get('PHP_AUTH_PW');
}

參考資料:
https://gist.github.com/gaillafr/02dc370c120062cf5f23896465f65fd9

2018年2月23日 星期五

Laravel 的 api route 使用 csrf 的認證

當前的 app 可以使用的認證 (auth guard) 都描述在:
app/Http/Kernel.php 裡面..

每一個 guard 對應到的認證 php 描述在:$routeMiddleware

第一個 $middleware 裡面描述的是不管哪一種方式認證都會載入的 class

第二個 $middlewareGroups 是有指定到對應的 group 才會載入的。

Laravel 已經做好了 CsrfToken 的認證,所以我們只要知道怎麼加入使用即可。

首先修改:app/Http/Kernel.php
在 $routeMiddleware 增加一項認證方式。
protected $routeMiddleware = [
    'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
    'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
    'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
    'can' => \Illuminate\Auth\Middleware\Authorize::class,
    'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
    'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,

    // your authenticate
    'myapi' => \App\Http\Middleware\ApiAuth::class,
    'csrf' => \App\Http\Middleware\VerifyCsrfToken::class,
];

因為 csrf 的認證也需要用到 session,所以我們選擇在 $middleware 作增加。
protected $middleware = [
    \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
    \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
    \App\Http\Middleware\TrimStrings::class,
    \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    \Illuminate\Session\Middleware\StartSession::class,
    \Illuminate\View\Middleware\ShareErrorsFromSession::class,
];
沒有加入這兩行,會跳出 Session 相關的 exception。

接下來把 route 的 middleware 指向 csrf 就可以做認證了。
//following Which require authentication ................
Route::group(
    [
        'prefix' => 'v1',
        'middleware' => 'csrf'
    ],
    function() {
        Route::get('stock',function(){
            return response([1,2,3,4],200);
        });
        Route::post('stock', 'StockController@fetch')->name('home');
    }
);

這邊要注意一點,內建的 VerifyCsrfToken 預設 GET 是直接 pass,所以如果 GET 也想要做認證的話,要自己 override public function handle($request, Closure $next)。
原始的方式在:
vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php
public function handle($request, Closure $next)
{
    if (
        $this->isReading($request) ||
        $this->runningUnitTests() ||
        $this->inExceptArray($request) ||
        $this->tokensMatch($request)
    ) {
        return $this->addCookieToResponse($request, $next($request));
    }

    throw new TokenMismatchException;
}

/**
 * Determine if the HTTP request uses a ‘read’ verb.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return bool
 */
protected function isReading($request)
{
    return in_array($request->method(), ['HEAD', 'GET', 'OPTIONS']);
}

以上

2018年2月19日 星期一

Laravel 為已存在的 table 新增欄位

首先建立一份新的 migration
php artisan make:migration add_paid_to_users --table="users"
後面加了 --table 的參數,產生出來的檔案會幫忙把 schema 加進去。
出來檔案會長這樣。



接著再把要修改的欄位加進去
Schema::table('users', function($table) {
    $table->string("title");
    $table->text("description");
    $table->timestamps();
});

最後執行 php artisan migrate 就可以了。

非常不建議在原來的 migration 檔案裡面修改欄位後執行 php artisan migrate:refresh
這會把原本的資料都給清除!!

Laravel 執行 php artisan migrate 出現 SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes

如果執行的 DB 是 mysql 5.7.7 以上的話是不會發生這問題的。

但如果是跑 MariaDB 或是比較舊版的 mysql 就會出現這個錯誤。



網路上找到的解決方法:
修改 app/Providers/AppServiceProvider.php 這個檔案。

use Illuminate\Support\Facades\Schema;
public function boot()
{
    Schema::defaultStringLength(191);
}

2018年1月23日 星期二

在 Synology NAS 上使用 Laravel Framework

我的 Synology 環境是跑 2 個版本的 PHP,
而新 5.5 的 Laravel 又要 PHP 7.0 以上才給動,
原本是採用 ssh 進去做設定,使用 php 命令就回應某些元件要 >= 7.0。
使用 php70 命令又說 phar extension 沒有啟動。

後來採用了網路磁碟機的方式來做解決。
我的環境是 mac,安裝了 composer。
連上 NAS 的網路磁碟機之後用命令列執行就可以建立好資料夾。

首先在自己的 mac 裡面下載 Laravel:
composer global require "laravel/installer"


接著在 NAS 的磁碟目錄裡面建一個 project:(這邊我用 composer 直接建立,因為我沒有為下載的 Laravel 設立 path)
composer create-project --prefer-dist laravel/laravel blog


在 Synology 的 Web Station 裡設一個虛擬主機,並把『主目錄』設在 Laravel 建立好的 project 目錄裡的 public 子目錄。另外把 PHP 設為 7.0。


連上設定好的網址,成功的話會看到這個畫面。


架設完成就可以開始進行 framework 的研究囉。

Reference:
https://laravel.com/docs/5.5
https://laravel.tw/docs/5.2

2016年10月12日 星期三

Make Philips Hue blink unlimited

There is a command to make Philips Hue Lamp blink.

command: /api/<user name>/lights/<id>/state
method: PUT
body:
{
"on": true,
        "alert": "lselect"
}

But it only persists for 15 seconds. (according to development guide)
alertstringThe alert effect, which is a temporary change to the bulb’s state. This can take one of the following values:
“none” – The light is not performing an alert effect.
“select” – The light is performing one breathe cycle.
“lselect” – The light is performing breathe cycles for 15 seconds or until an "alert": "none" command is received.
Note that this contains the last alert sent to the light and not its current state. i.e. After the breathe cycle has finished the bridge does not reset the alert to "none".

so we could leverage the scheduling function to make it infinite.
command: /api/<user name>/schedules
method: POST
body:
{
"name": "Blink",
"description": "My Blink",
"command": {
"address": "/api/<user name>/lights/<id>/state",
"method": "PUT",
"body": {"alert": "lselect"}
},
"time": "R/PT00:00:15"
}

To stop blinking just delete the schedule.
command: /api/<user name>/schedules/<id>
method: DELETE

After deleting the schedule, the bulb may keep blinking a little while.

If you want to stop blinking immediately you have to add one more command to set the alert to none.
command: /api/<user name>/lights/<id>/state
method: PUT
body:
{
        "alert": "none"
}

Reference:
Lights: http://www.developers.meethue.com/documentation/lights-api
Schedule: http://www.developers.meethue.com/documentation/schedules-api-0
Time Pattern: http://www.developers.meethue.com/documentation/datatypes-and-time-patterns#16_time_patterns

Note: To see the detail of development you must register and log in meethue.

黑...