PHP

Laravel6 JWT ユーザ登録+メール認証 + jwt-auth(JWT)

Laravel

 

この記事では実践的なコードであっさりまとめます。

 

JWT関連のJSONなどの細かいのは下記記事でまとめています。

Laravel JWTで認証API メール認証

 

Gmailの利用

開発用のメールサーバとして、Gmailサーバを送信サーバとして利用します。

// 本番環境はAWS SES, SendGridあたりを利用するのがおすすめです。

 

Gmailにログイン状態で下記にアクセスする

  1. https://accounts.google.com/DisplayUnlockCaptcha
  2. https://www.google.com/settings/security/lesssecureapps

 

.env

MAIL_DRIVER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=watasi@gmail.com
MAIL_PASSWORD=pas2w0rd
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=watasi@gmail.com
MAIL_FROM_NAME=テスト

 

 

jwt-auth(JWT)のインストール

 

$ php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\JWTAuthServiceProvider"
$ php-fpm php artisan jwt:secret

 

 

uuid生成ライブラリのインストール

 

$ composer require ramsey/uuid

 

 

マイグレーションファイル

 

database/migrations/xxxxx_create_users_table

<?php

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

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->bigIncrements('user_id')->comment('会員情報テーブル主キー');
            $table->string('user_name')->nullable(true)->comment('会員名');
            $table->string('email')->unique()->nullable(true)->comment('メールアドレス');
            $table->integer('point')->default(0)->comment('獲得ポイント数');
            $table->string('password')->nullable(true)->comment('パスワード');;
            $table->string('user_thumbnail_url', 1000)->nullable()->comment('プロフィール画像パス');
            $table->integer('login_challenge_count')->nullable()->comment('ログイン試行回数');
            $table->timestamp('login_challenge_date')->nullable()->comment('ログイン試行日時');
            $table->timestamp('email_verified_at')->nullable()->comment('メールアドレス変更日時');
            $table->timestamp('created_at')->nullable()->comment('作成日時');
            $table->timestamp('updated_at')->nullable()->comment('更新日時');
            $table->timestamp('deleted_at')->nullable()->comment('退会日時');
            $table->boolean('invalid_flag')->default(0)->comment('利用停止フラグ');
            $table->timestamp('invalid_at')->nullable()->comment('利用停止日時');

            $table->index('email');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
        DB::statement('ALTER TABLE `users` auto_increment = 1;');
    }
}

 

 

$ php artisan make:model Models/Activation -m

 

database/migrations/xxxxx_create_activations_table

<?php

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

class CreateActivationsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('activations', function (Blueprint $table) {
            $table->bigIncrements('activation_id')->comment("アクティベーションテーブル主キー");
            $table->string('user_name')->comment("ユーザ名");
            $table->string('password')->comment("パスワード");
            $table->string('email')->comment("メールアドレス");
            $table->string('code')->comment("認証用コード");
            $table->datetime('email_verified_at')->nullable(true)->comment("メール認証完了日時");
            $table->timestamps();

            $table->index('code');
            $table->index('email');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('activations');
        DB::statement('ALTER TABLE `activations` auto_increment = 1;');
    }
}

 

 

マイグレーションを反映させる

$ php artisan migrate:fresh

 

 

モデルの作成

 

$ mkdir app/Http/Models

フォルダを作ってモデルをまとめちゃうのが好きだよ。

 

app/Http/Models/Activation.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Activation extends Model
{
    //
}

 

app/Http/Models/User.php

<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Authenticatable implements JWTSubject
{
    use Notifiable;

    protected $carbon;
    protected $now;

    protected $primaryKey = 'user_id';

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $guarded = [
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];


    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
        return [];
    }
}

 

注意

JWTSubjectインターフェイスを実装しているので、下記の関数を定義しないとエラーになるのだ

    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
        return [];
    }

 

 

 

maillableクラス作成

 

$ php artisan make:mail ActivationCreated --markdown=emails.activations.created

 

 

app/Mail/ActivationCreated.php

<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use App\Models\Activation;

class ActivationCreated extends Mailable
{
    use Queueable, SerializesModels;

    protected $activation;


    public function __construct(Activation $activation)
    {
        $this->activation = $activation;
    }

    /**
     * メール認証にて、メール本文に付与する確認用URL情報を設定
     *
     * @return $this
     */
    public function build()
    {
        $apiUrl = config('app.url');

        return $this->markdown('emails.activations.created')
        ->with([
                  'url' => $apiUrl."/users/me/verify?code={$this->activation->code}",
                  'user_name' => $this->activation->user_name
               ]);;
    }
}

 

 

メール認証コンテンツ作成

 

resources/views/emails/activations/created.blade.php

@component('mail::message')

@if (!empty($user_name))
    {{ $user_name }} さん
@endif


** 以下の認証リンクをクリックしてください。 **
@component('mail::button', ['url' => $url])
メールアドレスを認証する
@endcomponent


@if (!empty($url))
###### 「ログインして本登録を完了する」ボタンをクリックできない場合は、下記のURLをコピーしてWebブラウザに貼り付けてください。
###### {{ $url }}
@endif

---

※もしこのメールに覚えが無い場合は破棄してください。

---


ご利用有難う御座います。<br>
{{ config('app.name') }}
@endcomponent

 

 

 

コンフィグ設定

 

config/mail.php

    'message' => [
        'send_verify_mail' => 'ご指定頂いたメールアドレスに確認メールを送信しました!ご確認頂き登録を完了してください。',
        'add_user_success' => '認証が完了しました!●●アプリをご利用ください',
    ],

 

config/error.php

<?php

return [
    'mailActivationError' => [
        'code' => 401009,
        'message' => 'メール認証キーが無効、または既に認証が完了しています。'
    ],
    'databaseTransactionRollback' => [
        'code' => 500001,
        'message' => 'もう1度お試し頂くかしばらく時間を置いてからご利用ください。'
    ],
];

 

config/token.php

<?php

/**
 * トークン用の設定
 */
return [

    //有効期限の設定
    'expire' => [

        // デフォルト15分
        'accessToken' => env('ACCESS_TOKEN_EXPIRATION_SECONDS', 900),

        //デフォルト4週間
        'refreshToken' => env('REFRESH_TOKEN_EXPIRATION_SECONDS', 2419200),
    ]
];

 

 

configを操作した場合はキャッシュクリアとキャッシュ生成が必要です。

$ php artisan config:clear
$ php artisan config:cache

 

リクエストの設定

 

 

app/Http/Requests/Api/V1_0/RegistUserRequest.php

<?php

namespace App\Http\Requests\Api\V1_0;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;

class RegistUserRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required|string',                       // ユーザ名
            'email' => 'required|string|email|unique:users',   // メールアドレス
            'password' => 'required|string|min:6|max:10'       // パスワード
        ];
    }

    protected function failedValidation(Validator $validator)
    {
        $res = response()->json([
            'status' => 400,
            'errors' => $validator->errors(),
        ], 400);

        throw new HttpResponseException($res);
    }
}

 

 

app/Http/Requests/Api/V1_0/LoginRequest.php

<?php

namespace App\Http\Requests\Api\V1_0;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;

class LoginRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'email' => 'required|string|email',            // メールアドレス
            'password' => 'required|string|min:6|max:10'   // パスワード
        ];
    }

    protected function failedValidation(Validator $validator)
    {
        $res = response()->json([
            'status' => 400,
            'errors' => $validator->errors(),
        ], 400);

        throw new HttpResponseException($res);
    }
}

 

 

 

トークン生成用サービスの作成

 

app/Http/Services/Service.php

<?php

namespace App\Services;

abstract class Service
{

}

 

 

app/Http/Services/ApiTokenCreateService.php

<?php

namespace App\Services;

use App\Models\User;
use Carbon\Carbon;
use Tymon\JWTAuth\Facades\JWTAuth;
use Tymon\JWTAuth\Facades\JWTFactory;

class ApiTokenCreateService extends Service
{

    protected $user;
    protected $now;

    public function __construct(User $user)
    {
        $this->user = $user;
        $carbon = new Carbon();
        $this->now = $carbon->now()->timestamp;
    }

    /**
     * トークンとユーザ情報のJSONデータを返却
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function respondWithToken() :string
    {
        return response()->json([
            'token' => [
                'access_token' => $this->createAccessToken(),
                'refresh_token' => $this->createRefreshToken()
            ],
            'profile' => [
                'id' => $this->user->user_id,
                'name' => $this->user->user_name,
                'email' => $this->user->email,
                'coin' => $this->user->remaining_coin,
                'image' => [
                    'url' => $this->user->user_thumbnail_url,
                    'width' => null,
                    'height' => null
                ],
                'role' => $this->user->userRoles->user_role_name
            ]
        ]);
    }



    /**
     * API用のアクセストークンを作成
     *
     * @return string
     */
    public function createAccessToken() :string
    {
        $customClaims = $this->getJWTCustomClaimsForAccessToken();
        $payload = JWTFactory::make($customClaims);
        $token = JWTAuth::encode($payload)->get();

        return $token;
    }

    /**
     * API用のリフレッシュトークンを作成
     *
     * @return string
     */
    public function createRefreshToken() :string
    {
        $customClaims = $this->getJWTCustomClaimsForRefreshToken();
        $payload = JWTFactory::make($customClaims);
        $token = JWTAuth::encode($payload)->get();

        return $token;
    }

    /**
     * アクセストークン用CustomClaimsを返却
     *
     * @return object
     */
    public function getJWTCustomClaimsForAccessToken() :object
    {
        $data = [
            'sub' => $this->user->user_id,
            'iat' => $this->now,
            'exp' => $this->now + config('token.expire.accessToken')
        ];

        return JWTFactory::customClaims($data);
    }

    /**
     * リフレッシュトークン用CustomClaimsを返却
     *
     * @return object
     */
    public function getJWTCustomClaimsForRefreshToken() :object
    {
        $data = [
            'sub' => $this->user->user_id,
            'iat' => $this->now,
            'exp' => $this->now + config('token.expire.refreshToken')
        ];

        return JWTFactory::customClaims($data);
    }
}

 

 

 

コントローラの作成

 

app/Http/Controllers/Controller.php

<?php

namespace App\Http\Controllers;

use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;

class Controller extends BaseController
{
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}

雛形だね。

 

ユーザ登録コントローラ

 

app/Http/Controllers/Api/V1_0/RegisterController.php

<?php

namespace App\Http\Controllers\Api\V1_0;

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V1_0\RegistUserRequest;
use App\Models\User;
use App\Models\Activation;
use Illuminate\Http\Request;
use Ramsey\Uuid\Uuid;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Mail\ActivationCreated;
use App\Models\UserRole;
use App\Models\Device;


class RegisterController extends Controller
{
    private $userGuestRoleId;
    private $now;

    public function __construct()
    {
        $userRole = new UserRole();
        $this->userGuestRoleId = $userRole->getGuestRoleId();
        $this->now = Carbon::now()->format('Y-m-d H:i:s');
    }
    /**
     * 登録リクエストを受付
     *
     * @param App\Http\Requests\Api\V1_0\RegistUserRequest
     * @return void
     */
    public function register(RegistUserRequest $request): string
    {
        $this->createActivation($request);

        return response()->json([
            'message' => config('mail.message.send_verify_mail')
        ]);
    }

    /**
     * アクティベーションコードを生成して認証コードをメールで送信
     *
     * @param App\Http\Requests\Api\V1_0\RegistUserRequest
     * @return void
     */
    private function createActivation(RegistUserRequest $request): void
    {
        $activation = new Activation;
        $activation->user_name = $request->name;
        $activation->email = $request->email;
        $activation->password = bcrypt($request->password);
        $activation->code = Uuid::uuid4();
        $activation->save();

        Mail::to($activation->email)->send(new ActivationCreated($activation));
    }

    /**
     * メール認証コードを検証してユーザ情報の登録
     *
     * @param Request
     * @return string
     */
    public function verify(Request $request) :string
    {
        $code = $request->code;

        // 認証確認
        if (!$this->checkCode($code)) {

            return response()->json(config('error.mailActivationError'));

        } else {
            // ユーザ情報の登録
            DB::beginTransaction();
            try {
                $activation = Activation::where('code',$code)->first();
                $generalRoleId = $this->userGuestRoleId;

                $user = new User();
                $user->user_name = $activation->user_name;
                $user->email = $activation->email;
                $user->password = $activation->password;
                $user->save();
                Activation::where('code', $code)->update(['email_verified_at' => Carbon::now()]);

                DB::commit();
                return response()->json(config('mail.message.add_user_success'));

            } catch (\Exception $e) {
                DB::rollback();
                Log::error('WEB /user/verify - Class ' . get_class() . ' - PDOException Error. Rollback was executed.' . $e->getMessage());

                return response()->json(config('error.databaseTransactionRollback'));
            }
        }
    }

    /**
     *  メール認証コードの検証
     *
     *  1. 与えられた認証コードがActivations.codeに存在するか?
     *  2. users.emailが存在しユーザ登録が既に完了したメールアドレスかどうか?
     *  3. 認証コード発行後1日以内に発行された認証コードであるか?
     *
     * @param string $code - メール認証のURLパラメータから取得する認証コード
     * @return boolean
     */
    private function checkCode($code): bool
    {
        $activation = Activation::where('code',$code)->first();
        if (!$activation) {
            return false;
        }

        $activation_email = $activation->email;
        $latest = Activation::where('email',$activation_email)->orderBy('created_at', 'desc')
                                                              ->first();
        $user = User::where('email',$activation_email)->first();
        $activation_created_at = Carbon::parse($activation->created_at);
        $expire_at = $activation_created_at->addDay(1);
        $now = Carbon::now();

        return $code === $latest->code && !$user && $now->lt($expire_at);
    }
}

 

ログインコントローラ

 

app/Http/Controllers/Api/V1_0/LoginController.php

<?php

namespace App\Http\Controllers\Api\V1_0;

use App\Http\Controllers\Controller;
use App\Services\ApiTokenCreateService;
use Tymon\JWTAuth\Facades\JWTAuth;
use App\Models\User;
use App\Http\Requests\Api\V1_0\LoginRequest;

class LoginController extends Controller
{

    /**
     * ログインの実行
     *
     * @param App\Http\Requests\Api\LoginRequest $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function login(LoginRequest $request): string
    {
        $input = $request->only('email', 'password');
        $token = JWTAuth::attempt($input);

        if (!$token) {
            return response()->json([
                'success' => false,
                'message' => 'Invalid Email or Password',
            ], 401);
        }
        $user = User::where('email', $request->email)->first();
        $ApiTokenCreateService = new ApiTokenCreateService($user);

        return $ApiTokenCreateService->respondWithToken();
    }
}

 

 

ルーティング設定

 

routes/api.php

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/


Route::prefix('V1_0')->group(function () {
    Route::post('/users/me', 'Api\V1_0\RegisterController@register');
    Route::post('/login', 'Api\V1_0\LoginController@login');
});

prefixを設定しています。

 

routes/web.php

<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/users/me/verify', 'Api\V1_0\RegisterController@verify');

 

  • ユーザ登録(API:POST)
    http://localhost/api/V1_0/users/me
  • ログイン(API:POST)
    http://localhost/api/V1_0/login
  • メール認証(WEB:GET)
    http://localhost/users/me/verify

 

 

 

Amazonおすすめ

iPad 9世代 2021年最新作

iPad 9世代出たから買い替え。安いぞ!🐱 初めてならiPad。Kindleを外で見るならiPad mini。ほとんどの人には通常のiPadをおすすめします><

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)