
この記事では実践的なコードであっさりまとめます。
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


