商品のページのURLが例えばhttps://localhost/product/5734とかだと、5734はproductsテーブルのidの値だな、とわかってしまいます。しかしこれが、商品のIDではなく会員のIDや注文のIDなら大きな問題です。もちろん通常は認証で保護してログインした人のみしか閲覧できないようにしますが、プログラムのバグで認証が外れていたら。。。そんなときにIDがシーケンスの数字でなかったら、例えば、043a9c43-26cd-432f-bb25-a0eb045815b7とかのわけのわからない数字だったら安心です。今回はidの難読化のためのuuidを紹介です。

UUIDとは

wikiによると、

UUID(Universally Unique Identifier)とは、ソフトウェア上でオブジェクトを一意に識別するための識別子である。UUIDは128ビットの数値だが、16進法による550e8400-e29b-41d4-a716-446655440000というような文字列による表現が使われることが多い。元来は分散システム上で統制なしに作成できる識別子として設計されており、したがって将来にわたって重複や偶然の一致が起こらないという前提で用いることができる。

とあります。「重複が起こらないという前提で用いる」、これがidの代わりとして使用できる理由です。

Laravelで使用するには

phpにはuuidを作成する関数はないのですが、Laravelではもちろんパッケージを通して対応しています。

まず、migrationを使用してproductsのDBテーブルの項目として追加してみます。
プライマリキーのidはキープして、ユニークなキーを持つuuidとして別に定義します。そうでないと、joinする他のDBテーブルのjoin keyも36文字の長い値を保存することになり効率的ではありません。

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateProducts extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->uuid('uuid')->unique();
            $table->char('name', 100);
            $table->decimal('price', 10, 0);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('products');
    }
}

これをmigrateして、

$ php artisan migrate

mysqlで作成されたテーブルの定義を見ると、

mysql> describe products;
+------------+---------------------+------+-----+---------+----------------+
| Field      | Type                | Null | Key | Default | Extra          |
+------------+---------------------+------+-----+---------+----------------+
| id         | bigint(20) unsigned | NO   | PRI | NULL    | auto_increment |
| uuid       | char(36)            | NO   | UNI | NULL    |                |
| name       | char(100)           | NO   |     | NULL    |                |
| price      | decimal(10,0)       | NO   |     | NULL    |                |
| created_at | timestamp           | YES  |     | NULL    |                |
| updated_at | timestamp           | YES  |     | NULL    |                |
+------------+---------------------+------+-----+---------+----------------+

36文字長のユニークなキーを持つuuidの項目が作成されています。

早速、DBレコードを作成してみます。

まず、Productのモデルを作成して、

namespace App;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $fillable = [
        'uuid', 'name', 'price',
    ];
}

そして、tinkerでレコード作成してみます。

>>> Product::create(['uuid' => (string) Str::uuid(), 'name' => '商品名', 'price' => 1000]);
=> App\Product {#3986
     uuid: "043a9c43-26cd-432f-bb25-a0eb045815b7",
     name: "商品名",
     price: 1000,
     updated_at: "2020-06-25 18:03:45",
     created_at: "2020-06-25 18:03:45",
     id: 1,
   }

Str::uuid()はLaravelのヘルパーです。ヘルパーはLaravelが使用しているパッケージのオブジェクトを返すので、stringで文字列にキャストしています。

idを隠す

せっかく、uuidの項目があるのだから、プライマリキーのidが表に出ないように隠しましょう。

モデルにおいて、hiddenの属性にidを入れます。

...
class Product extends Model
{
    protected $fillable = [
        'uuid', 'name', 'price',
    ];

    protected $hidden = [
    	'id'
    ];
}

idが隠れているか、tinkerで確認です。

>>> Product::find(1);
=> App\Product {#3996
     uuid: "043a9c43-26cd-432f-bb25-a0eb045815b7",
     name: "商品名",
     price: "1000",
     created_at: "2020-06-25 18:03:45",
     updated_at: "2020-06-25 18:03:45",
   }

idの項目表示されないですね。

しかし、直接ならアクセスできます。

>>> Product::find(1)->id;
=> 1

しかし、配列として変換などするとidの値は入ってきません。

>>> Product::find(1)->toArray();
=> [
     "uuid" => "043a9c43-26cd-432f-bb25-a0eb045815b7",
     "name" => "商品名",
     "price" => "1000",
     "created_at" => "2020-06-25 18:03:45",
     "updated_at" => "2020-06-25 18:03:45",
   ]

コントローラからuuidでアクセス

以下のように、getRouteKeyNane()で、uuidを指定すると、コントローラのURLでuuidを指定してレコードの取得可能です。

...
class Product extends Model
{
    protected $fillable = [
        'uuid', 'name', 'price',
    ];

    protected $hidden = [
    	'id'
    ];

    public function getRouteKeyName()
    {
    	return 'uuid';
    }
}

コントローラを作成して、

namespace App\Http\Controllers;

use App\Product;
use App\Http\Controllers\Controller;

class ProductController extends Controller
{
    public function show(Product $product)
    {
        return $product;
    }
}

ルートを設定します。

...
Route::resource('/product', 'ProductController')->only('show');
...

通常のように、uuidでなくidの値でhttps::/local/product/1としてアクセスすると、

>>> $option = [ 'ssl' => ['verify_peer' => false, 'verify_peer_name' => false]]; //これはSSL認証のエラーを抑えるために必要。
=> [
     "ssl" => [
       "verify_peer" => false,
       "verify_peer_name" => false,
     ],
   ]
>>> file_get_contents('https://localhost/product/1', false, stream_context_create($option));
PHP Warning:  file_get_contents(https://localhost/product/1): failed to open stream: HTTP request failed! HTTP/1.1 404 Not Found
 in Psy Shell code on line 1
=> false

404のページが見つからない、のエラーです。

しかし、https://localhost/product/043a9c43-26cd-432f-bb25-a0eb045815b7のようにuuidでアクセスすると、以下のようにレコードを取得してくれます。

>>> file_get_contents('https://localhost/product/043a9c43-26cd-432f-bb25-a0eb045815b7', false, stream_context_create($option));
=> "{"uuid":"043a9c43-26cd-432f-bb25-a0eb045815b7","name":"商品名","price":"1000","created_at":"2020-06-25 18:03:45","updated_at":"2020-06-25 18:03:45"}"
>>> 

By khino