Laravel SanctumのAPI Tokenの動作をコードベースで完全理解する
Laravelの認証機能のパッケージの一つであるLaravel SanctumにはSPAの認証機能とAPI Tokenを利用した認証機能の2つを持っています。本文書ではAPI Tokenに注目し、コードを実際に読み進めていくことでAPI Tokenがどのような処理を行なっているか確認し理解を深めることができます。
本文書ではLaravel9を利用して動作確認を行なっています。Laravel9ではLaravel Sanctumは事前にインストールされているため追加でパッケージをインストールする必要がありません。
目次
API Tokenの基本
API Tokenは非常にシンプルな仕組みなのでわかりやすいですコードを確認する前に大まかな流れを確認しておきます。
- Token保存用のテーブルの作成
- ユーザ毎にTokenを作成しToken保存用テーブルに保存
- api.phpファイルに設定したルーティングにmiddlewareでsanctumを設定
- 作成したTokenをheaderに設定してリクエストを送信
- 送られてきたTokenが保存されているTokenと一致するかチェック(有効期限のチェック含む)
- Tokenに問題なければそのまま処理へ、Tokenに問題がある場合はJSONでエラーメッセージを返却
Laravelのソースコードを読む場合、処理がいくつものファイルにまたがっているため混乱する箇所があるかもしれないので注意してください。
Laravel9での動作確認
Laravel環境の構築
Laravel Sanctumの動作確認を行うmacOSにLaravel環境の構築を行います。laravel newコマンドを利用してLaravelプロジェクトの作成を行います。ここではlaravel_sanctum_tokenというプロジェクト名をつけていますが任意の名前をつけてください。
% laravel new laravel_sanctum_token
簡易的に利用できるSQLiteデータベースを利用します。databaseフォルダにdatabase.sqliteファイルを作成します。
% touch database/database.sqlite
データベースファイルが作成できたら環境変数の更新を行うため.envファイルを開きます。.envファイルのDB_CONNECTIONにはデフォルトでmysqlが設定されていますがsqliteに変更します。さらにDB_が先頭についた環境変数をDB_CONNECTIONを残してすべて削除します。
ユーザのログイン認証機能を利用するためBreezeパッケージをインストールします。
% composer require laravel/breeze --dev
Breezeパッケージを利用するためにインストールを行います。
% php artisan breeze:install
インストール後、JavaScript関連のファイルをインストールするためnpm installコマンドを実行し、開発サーバを起動するためにnpm run devコマンドを実行します。
% npm install
% npm run dev
Breezeの設定が完了したらphp artisan migrateコマンドでテーブルの作成を行います。コマンド実行後のメッセージの最後に表示されているpersonal_access_tokenがToken保存用のテーブルです。データベースの設定に不備がある場合はここでエラーメッセージが表示されます。
% php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_000000_create_users_table (3.59ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated: 2014_10_12_100000_create_password_resets_table (2.18ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated: 2019_08_19_000000_create_failed_jobs_table (2.60ms)
Migrating: 2019_12_14_000001_create_personal_access_tokens_table
Migrated: 2019_12_14_000001_create_personal_access_tokens_table (3.03ms)
テーブルの作成が完了したらphp artisan serveコマンドでLaravelの開発サーバを起動します。
% php artisan serve
Starting Laravel development server: http://127.0.0.1:8000
開発サーバにアクセスを行うとLaravelの初期画面が表示されます。右上に表示されているRegisterのリンクからユーザの登録を行ってください。
Tokenの作成
ユーザの作成が完了したらTokenの作成を行います。Tokenの作成はcreateTokenメソッドを利用して行います。ここでは/(ルート)にアクセスするとユーザのidが1のユーザのTokenが作成できるようにweb.phpファイルに以下を記述します。ユーザを複数作成した場合や削除した場合などにはidが1ではない場合があるので存在しているユーザのIDを利用するように注意してください。ユーザのidの確認は$user = User::all()で取得することができます。
Route::get('/', function () {
// return view('welcome');
$user = Auth::loginUsingId(1);
$token = $user->createToken('test');
dd($token);
});
ブラウザで/(ルート)にアクセスすると作成されたTokenが表示されます。plainTextTokenに設定されている値を利用してTokenの動作確認を行います。各自の環境でTokenの値は異なります。
$userのcreateTokenメソッドを利用してTokenを作成していますがUser.phpファイルで設定されているトレイトのHasApiTokenがcreateTokenメソッドを持っているため実行することが可能です。デフォルトで設定されていますがTokenを利用する場合はHasApiTokenが必須です。
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
//略
リクエストの送信
Tokenによる認証を行うためにはルーティングにmiddlewareのauth:sanctumを設定する必要があります。api.phpファイルではデフォルトで/userルーティングに設定されているのでそのまま利用します。
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
Tokenを利用してリクエストを送信するためにVSCodeのExtensionsのREST Clientを利用します。REST Clientを利用しない場合はPostmanやcurl関数などを利用してください。
REST Clientを利用するためにプロジェクトフォルダの直下にtest.httpファイルを作成します。作成後以下のコードを記述します。AuthorizationにBearerと作成したTokenの文字列を設定します。
GET http://127.0.0.1:8000/api/user/
Content-Type: application/json
Accept: application/json
Authorization: Bearer 1|JInSTb0c7e50VUeE7qawJh1nSMsfTcCi6GGesfR9
/api/userに対してGETリクエストを送信することができます。正しいTokenが設定さている場合にはTokenを作成したユーザ情報が戻されます。
Tokenに問題がある場合はJSONでメッセージが戻されます。
確認した通りAPI Tokenの機能はシンプルでTokenを作成しHeaderにTokenを追加しリクエスを送信、Tokenで認証を行いルーティングにmiddlewareを設定するだけです。
Laravelの環境構築、Laravel SanctumのAPI Tokenの動作が理解できたと思うのでコードの確認を行なっていきます。
コードの確認
middlewareの確認
API Tokenで認証を行いたい場合にはmiddlewareでauth:sanctumの設定を行う必要があります。先ほど確認した通りapi.phpファイルにはデフォルトでルーティングの/userには設定が行われています。
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
middlewareに慣れていない人であればmiddleware(‘auth:sanctum’)を設定するとどのような処理が行われているかわからないかもしれません。middlewareに設定されているauthはKernel.phpファイルに設定されています。authのコロンの後ろについているsanctumパラメータとして後ほど利用します。
//略
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
//略
];
}
kernel.phpファイルからauthについてはApp¥Http¥MiddlewareのAuthenticate.phpが設定されていることがわかります。Authenticateの中身を確認するとredirectToメソッドのみしか記述させていませんがAuthenticateクラスはMiddlewareクラスを継承(extends)していることがわかります。
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @return string|null
*/
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return route('login');
}
}
}
継承しているMiddlewareはIlluminate\Auth\Middleware\Authenticate.phpの別名として設定が行われているのでIlluminate\Auth\Middleware\Authenticate.phpファイルの中身を確認します。middlewareでは送信されてくるリクエストに対して行いたい処理はhandleメソッドに記述します。middlewareでauth:sanctumを設定するとAuthenticate.phpファイルのhandleメソッドの中身が実行されることになります。
namespace Illuminate\Auth\Middleware;
use Closure;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Contracts\Auth\Factory as Auth;
use Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests;
class Authenticate implements AuthenticatesRequests
{
/**
* The authentication factory instance.
*
* @var \Illuminate\Contracts\Auth\Factory
*/
protected $auth;
/**
* Create a new middleware instance.
*
* @param \Illuminate\Contracts\Auth\Factory $auth
* @return void
*/
public function __construct(Auth $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string[] ...$guards
* @return mixed
*
* @throws \Illuminate\Auth\AuthenticationException
*/
public function handle($request, Closure $next, ...$guards)
{
$this->authenticate($request, $guards);
return $next($request);
}
/**
* Determine if the user is logged in to any of the given guards.
*
* @param \Illuminate\Http\Request $request
* @param array $guards
* @return void
*
* @throws \Illuminate\Auth\AuthenticationException
*/
protected function authenticate($request, array $guards)
{
if (empty($guards)) {
$guards = [null];
}
foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
return $this->auth->shouldUse($guard);
}
}
$this->unauthenticated($request, $guards);
}
/**
* Handle an unauthenticated user.
*
* @param \Illuminate\Http\Request $request
* @param array $guards
* @return void
*
* @throws \Illuminate\Auth\AuthenticationException
*/
protected function unauthenticated($request, array $guards)
{
throw new AuthenticationException(
'Unauthenticated', $guards, $this->redirectTo($request)
);
}
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @return string|null
*/
protected function redirectTo($request)
{
//
}
}
handleメソッドの引数の確認
middlewareではhandleメソッドが実行されることがわかったのでhandleメソッドの中身を確認していきます。
public function handle($request, Closure $next, ...$guards)
{
$this->authenticate($request, $guards);
return $next($request);
}
handleメソッドには$request, $nextの他に3つ目の引数…$guardsが設定されていることがわかります。3つ目の引数の…guardsには何が入るのか気になる人もいるかと思います。ここにはauthの後ろに設定したパラメータ”sanctum”が入ります。
$guardsの中身を確認して見ると配列にsanctumが入っていることが確認できます。
^ array:1 [▼
0 => "sanctum"
]
handleメソッドの中ではauthenticateメソッドに$request, $guardsが渡されています。$guardsの中には”sanctum”が入っているのでmiddlewareのauthのパラメータにつけた値が重要であることがわかります。
public function handle($request, Closure $next, ...$guards)
{
$this->authenticate($request, $guards);
return $next($request);
}
authenticateメソッドの確認
authenticateメソッドの中では$guardsの中身が空かどうかチェックを行っていますが$guardsには”sanctum”が入っているのでforeachのループが実行され$this->auth->guardのcheckメソッドが実行されます。$guardには’sanctum’が入っているので$this->auth->guard(‘sanctum’)->check()が実行されることになります。このcheckメソッドの処理の中にTokenのチェックが含まれています。
protected function authenticate($request, array $guards)
{
if (empty($guards)) {
$guards = [null];
}
foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
return $this->auth->shouldUse($guard);
}
}
$this->unauthenticated($request, $guards);
}
checkメソッド内で行うTokenのチェックがどのような処理を行っているのか確認するために$this->auth->gurad(‘sanctum’)が何なのかを理解する必要があります。
$this->authはAuthenticate.phpのコンストラクタでインスタンス化されており中身はIlluminate\Auth\AuthManagerクラスです。
public function __construct(Auth $auth)
{
$this->auth = $auth;
}
AuthManagerの確認
Illuminate\Auth\AuthManagerクラスはguardメソッドを持っており、guardメソッドではguards配列に’sanctum’が含まれているかチェックし存在しない場合にはresolveメソッドが実行されます。
public function guard($name = null)
{
$name = $name ?: $this->getDefaultDriver();
return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
}
resolveメソッドではgetConfigメソッドに’sanctum’を指定して設定情報を取得していることがわかります。
protected function resolve($name)
{
$config = $this->getConfig($name);
if (is_null($config)) {
throw new InvalidArgumentException("Auth guard [{$name}] is not defined.");
}
if (isset($this->customCreators[$config['driver']])) {
return $this->callCustomCreator($name, $config);
}
$driverMethod = 'create'.ucfirst($config['driver']).'Driver';
if (method_exists($this, $driverMethod)) {
return $this->{$driverMethod}($name, $config);
}
throw new InvalidArgumentException(
"Auth driver [{$config['driver']}] for guard [{$name}] is not defined."
);
}
getConfigメソッドでどのような情報が取得できるか確認するとdriverとproviderを持つ配列であることがわかります。
protected function getConfig($name)
{
return $this->app['config']["auth.guards.{$name}"];
}
//
^ array:2 [▼
"driver" => "sanctum"
"provider" => null
]
これまでdriverやproviderは一切設定を行なっていません。ではどこからこれらの値を取得したのでしょう。パッケージをインストールした場合にはServiceProviderのファイルが含まれている場合があるのでSanctumに関するSanctumServiceProviderを見てみましょう。
SanctumServiceProviderの確認
SanctumServiceProviderのregisterメソッドの中でconfig関数を利用してauth.guardsの設定にsanctumを追加していることがわかります。
class SanctumServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
config([
'auth.guards.sanctum' => array_merge([
'driver' => 'sanctum',
'provider' => null,
], config('auth.guards.sanctum', [])),
]);
if (! app()->configurationIsCached()) {
$this->mergeConfigFrom(__DIR__.'/../config/sanctum.php', 'sanctum');
}
}
//略
registerメソッドの後にconfig関数を利用してconfigの中身を確認するとauth.guards.sanctumが設定されていることがわかります。
^ Illuminate\Config\Repository {#33 ▼
#items: & array:15 [▼
"app" => array:14 [▶]
"auth" => array:5 [▼
"defaults" => array:2 [▼
"guard" => "web"
"passwords" => "users"
]
"guards" => array:2 [▼
"web" => array:2 [▶]
"sanctum" => array:2 [▼
"driver" => "sanctum"
"provider" => null
]
]
確認後再度getConfig(‘sanctum’)を見ると取得できる内容が理解できます。
protected function getConfig($name)
{
return $this->app['config']["auth.guards.{$name}"];
}
//
^ array:2 [▼
"driver" => "sanctum"
"provider" => null
]
Resolveメソッドの確認
再度AuthManger.phpファイルに戻りresloveメソッドの中身を確認します。
$config[‘driver’]には’sanctum’が入っていることがわかったのでcustomCreatorsの配列の中に’sancum’が入った要素があるかチェックしていることがわかります。存在する場合はcallCustomCreatorメソッドが実行されます。先ほどのconfigと同様にcustomCreatorsがどのような値を持っているのがわかりません。
if (isset($this->customCreators[$config['driver']])) {
return $this->callCustomCreator($name, $config);
}
customCreatorsの中身については、SanctumServiceProvider.phpファイルのbootメソッドのconfigureGuardで設定が行われます。$auth->extendの部分に注目します。
<?php
namespace Laravel\Sanctum;
//略
class SanctumServiceProvider extends ServiceProvider
{
//略
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
//略
$this->defineRoutes();
$this->configureGuard();
$this->configureMiddleware();
}
//略
/**
* Configure the Sanctum authentication guard.
*
* @return void
*/
protected function configureGuard()
{
Auth::resolved(function ($auth) {
$auth->extend('sanctum', function ($app, $name, array $config) use ($auth) {
return tap($this->createGuard($auth, $config), function ($guard) {
app()->refresh('request', $guard, 'setRequest');
});
});
});
}
/**
* Register the guard.
*
* @param \Illuminate\Contracts\Auth\Factory $auth
* @param array $config
* @return RequestGuard
*/
protected function createGuard($auth, $config)
{
return new RequestGuard(
new Guard($auth, config('sanctum.expiration'), $config['provider']),
request(),
$auth->createUserProvider($config['provider'] ?? null)
);
}
//略
}
AuthManager($auth)はextendメソッドを持っているので処理を確認するとcustomCreatorsの配列のキーに’sanctum’, 値にcreateGuardメソッドの戻り値が設定されていることがわかります。
public function extend($driver, Closure $callback)
{
$this->customCreators[$driver] = $callback;
return $this;
}
createGuardの戻り値はRequestGuardインスタンスで引数でGuardインスタンスが渡されています。ここで利用されているGuardはLaravel\Sanctum\GuardのインスタンスでSanctumのTokenに関するメソッドが含まれています。
ここまでの流れを確認してようやく$this->auth->guard($guard)はRequestGuardインスタンスであることがわかります。
checkメソッドの中身
RequestGuard.phpファイルにはcheckメソッドは含まれていませんがトレイトのGuardHelpersがcheckメソッドを持っています。
class RequestGuard implements Guard
{
use GuardHelpers, Macroable;
//略
GuardHelpersのcheckメソッドではuserメソッドを実行してnullかどうかチェックしています。
public function check()
{
return ! is_null($this->user());
}
userメソッドはRequestGuard.phpファイルに記述されています。$this->userに値はないのでcall_user_func関数が実行されることになります。call_user_funcで指定されている$callbackはTokenの処理が含まれいるLaravel\Sanctum\Guardです。
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;
}
return $this->user = call_user_func(
$this->callback, $this->request, $this->getProvider()
);
}
GuardによるTokenの確認
call_user_funcによりGuardクラスにあるマジックメソッドの__invokeが実行されます。 API Tokenの場合はforeachループの内部の処理は行われず、getTokenFromRequestメソッドでRequestに含まれているTokenの情報を取得します。
public function __invoke(Request $request)
{
foreach (Arr::wrap(config('sanctum.guard', 'web')) as $guard) {
if ($user = $this->auth->guard($guard)->user()) {
return $this->supportsTokens($user)
? $user->withAccessToken(new TransientToken)
: $user;
}
}
if ($token = $this->getTokenFromRequest($request)) {
$model = Sanctum::$personalAccessTokenModel;
$accessToken = $model::findToken($token);
if (! $this->isValidAccessToken($accessToken) ||
! $this->supportsTokens($accessToken->tokenable)) {
return;
}
$tokenable = $accessToken->tokenable->withAccessToken(
$accessToken
);
event(new TokenAuthenticated($accessToken));
if (method_exists($accessToken->getConnection(), 'hasModifiedRecords') &&
method_exists($accessToken->getConnection(), 'setRecordModificationState')) {
tap($accessToken->getConnection()->hasModifiedRecords(), function ($hasModifiedRecords) use ($accessToken) {
$accessToken->forceFill(['last_used_at' => now()])->save();
$accessToken->getConnection()->setRecordModificationState($hasModifiedRecords);
});
} else {
$accessToken->forceFill(['last_used_at' => now()])->save();
}
return $tokenable;
}
}
getTokenFromRequestメソッドを見るとSanctum::$accessTokenRetrievalCallbackのチェックを行うことができますがnullなので$request->bearerTokenが実行されます。
protected function getTokenFromRequest(Request $request)
{
if (is_callable(Sanctum::$accessTokenRetrievalCallback)) {
return (string) (Sanctum::$accessTokenRetrievalCallback)($request);
}
return $request->bearerToken();
}
bearTokenメソッドはRequestのトレイトのInteractsWithInputに記述されています。RequestのHeaderに含まれるAuthorizationからTokenを取り出していることがわかります。
public function bearerToken()
{
$header = $this->header('Authorization', '');
$position = strrpos($header, 'Bearer ');
if ($position !== false) {
$header = substr($header, $position + 7);
return str_contains($header, ',') ? strstr($header, ',', true) : $header;
}
}
HeaderにTokenが含まれている場合はSanctum::$personalAccessTokenModelに設定されている’Laravel\\Sanctum\\PersonalAccessToken’モデルを利用してpersonal_access_tokensテーブルにアクセスします。アクセスする際にはHeaderから取り出したTokenを利用してToken情報を取得します。
if ($token = $this->getTokenFromRequest($request)) {
$model = Sanctum::$personalAccessTokenModel;
$accessToken = $model::findToken($token);
//略
$accessTokenをテーブルから取り出した後は取り出したTokenの有効期限が切れていないかisValidAccessTokenメソッドでチェックし、HasApiTokensのトレイトをもっているのかsupportsTokensメソッドを利用して確認しています。Tokenとして有効なのかどうかチェックしています。
if (! $this->isValidAccessToken($accessToken) ||
! $this->supportsTokens($accessToken->tokenable)) {
return;
}
protected function isValidAccessToken($accessToken): bool
{
if (! $accessToken) {
return false;
}
$isValid =
(! $this->expiration || $accessToken->created_at->gt(now()->subMinutes($this->expiration)))
&& $this->hasValidProvider($accessToken->tokenable);
if (is_callable(Sanctum::$accessTokenAuthenticationCallback)) {
$isValid = (bool) (Sanctum::$accessTokenAuthenticationCallback)($accessToken, $isValid);
}
return $isValid;
}
supportsTokensメソッド内のget_classではApp¥Models¥Userモデルが戻され、class_uses_recursive関数によってUserモデルが持っているトレイトをすべて取得しています。
protected function supportsTokens($tokenable = null)
{
return $tokenable && in_array(HasApiTokens::class, class_uses_recursive(
get_class($tokenable)
));
}
Token情報を利用してTokenに紐づいたユーザ情報を取得します。$tokenableにはユーザの情報が含まれています。
$tokenable = $accessToken->tokenable->withAccessToken(
$accessToken
);
Tokenの認証が完了するとeventが発火し、リスナーを設定することでeventを取得することができます。さらにTokenを利用した時刻をpersonal_access_tokensテーブルの’last_userd_at’列に書き込んで$tokeable(ユーザ情報)を戻しています。
event(new TokenAuthenticated($accessToken));
if (method_exists($accessToken->getConnection(), 'hasModifiedRecords') &&
method_exists($accessToken->getConnection(), 'setRecordModificationState')) {
tap($accessToken->getConnection()->hasModifiedRecords(), function ($hasModifiedRecords) use ($accessToken) {
$accessToken->forceFill(['last_used_at' => now()])->save();
$accessToken->getConnection()->setRecordModificationState($hasModifiedRecords);
});
} else {
$accessToken->forceFill(['last_used_at' => now()])->save();
}
return $tokenable;
RequestGuard.phpのuserメソッドには$tokeableが戻されるので$this->userにはユーザ情報が保存されることになります。
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;
}
return $this->user = call_user_func(
$this->callback, $this->request, $this->getProvider()
);
}
最終的にRequestGuard.phpのトレイトGuardHelpers.phpのcheckメソッドによってtrueが戻されます。
public function check()
{
return ! is_null($this->user());
}
Tokenが存在しない場合やTokenが一致しない場合にはunauthenticatedメソッドが実行されます。
protected function authenticate($request, array $guards)
{
if (empty($guards)) {
$guards = [null];
}
foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
return $this->auth->shouldUse($guard);
}
}
$this->unauthenticated($request, $guards);
}
unauthenticatedメソッドではAuthenticationExceptionが発生します。
protected function unauthenticated($request, array $guards)
{
throw new AuthenticationException(
'Unauthenticated.', $guards, $this->redirectTo($request)
);
}
Token認証に失敗したクライアント側ではJSONでメッセージが戻されます。メッセージの”Unauthenticated.”はAuthenticationExceptionの第一引数に設定した文字列です。
{
"message": "Unauthenticated."
}
まとめ
コードにすると少し混乱する部分もあったかと思いますが仕組みがシンプルなだけにそれほど難しくはなかったかと思います。API Tokenのコードが理解できるようになった後はぜひSanctumのSPA機能ではどのように認証が行われているかコード読みにチャレンジしてみてください。