この記事では実践的なコードであっさりまとめます。
JWT関連のJSONなどの細かいのは下記記事でまとめています。
もくじ
Gmailの利用
開発用のメールサーバとして、Gmailサーバを送信サーバとして利用します。
// 本番環境はAWS SES, SendGridあたりを利用するのがおすすめです。
Gmailにログイン状態で下記にアクセスする
- https://accounts.google.com/DisplayUnlockCaptcha
- 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