tohokuaikiのチラシの裏

技術的ネタとか。

Debitn10でApache2.4+PHP-fpmを動かす

PHP5.6を使いたい。参考にしたサイト

aptでパッケージ先を追加

普通にapt installできないので、https://packages.sury.org/php/ をパッケージ先に追加する。

GPG認証するパッケージを入れて、キーを追加して、設定ファイルを作る。

# apt install ca-certificates apt-transport-https 
# wget -q https://packages.sury.org/php/apt.gpg -O- | apt-key add -
# echo "deb https://packages.sury.org/php/ stretch main" | tee /etc/apt/sources.list.d/php.list

で、パッケージ情報を追加

# apt update
# apt upgrade

適当にPHP5.6周りをインストール

dpkg -l |grep php5.6 で出てきたのから適当に必要そうなのをインストール

#  apt install -y php5.6-mysql php5.6-mbstring php5.6-xml php5.6-zip php5.6-cli

PHP5.6がApacheからモジュールとして実行されないようにphp5.6はインストールしない。

次にphp5.6-fpmをインストール。さっきのと同時でもいいのだけど。

# apt install -y php5.6-fpm

VirtualHostごとに実行するPHPを変更する。

php-fpm5.6がserviceとして起動しているのを確認。

# systemctl status php5.6-fpm.service

/etc/php/5.6/fpm/pool.d/www.conf を見ると、

listen = /run/php/php5.6-fpm.sock

Unixソケットで接続するようになっているので、sites-available/hogehoge.conf で

<VirtualHost *:80>
        <Directory /path/to/document/root>
            require all granted
        </Directory>
        <FilesMatch \.(html|php)$>
            SetHandler "proxy:unix:/run/php/php5.6-fpm.sock|fcgi://localhost"

としておく。HTMLとPHP拡張子で動く。

require all granted は、いつものように「あー、AH01797: client denied by server configuration」で403を何度も繰り返すため…何度設定しても1度はミスる。

Apacheのモジュールを適宜IN/OUTしておく

これやり忘れてて「SetHandlerのproxyが効いてくれない・・・」とハマりました。

 a2dismod mpm_prefork
 a2enmod mpm_event
 a2enmod proxy_fcgi

.htaccessphp_value, php_flagの書き方を変える。

php_value auto_prepend_file /path/to/document/root/auto_prepend.php
php_flag error_log Off

とあるのを

SetEnv PHP_VALUE "auto_prepend_file=/path/to/document/root/auto_prepend.php"
SetEnv PHP_FLAG "error_log=Off"

とかする。

またAccess Denied

ってエラーがindex.htmlにアクセスすると出る。

 AH01071: Got error 'Access to the script '/var/www/htdocs/index.html' has been denied (see security.limit_extensions)\n'

/etc/php/5.6/fpm/pool.d/www.conf で

security.limit_extensions = .php .html

として、/etc/init.d/php5.6-fpm restart で動いた。

で、またハマる

VirtualHostにして、他のサイトでinclude_pathやauto_prepend_fileを設定すると、他のサイトでもこの指定が残る。

んー、なんやねんこれは。

結局、

<VirtualHost>
SetEnv PHP_VALUE "auto_prepend_file=/path/to/document/root/auto_prepend.php"
</VirtualHost>

<VirtualHost>
SetEnv PHP_VALUE "auto_prepend_file="
</VirtualHost>

みたいにして逐一書き直す羽目になった…なんやねんこれは…

ミライコンパスを使ってる学校のイベント情報をRSSで取得する

全国の私立中高校で説明会などの予約に使われているミライコンパスというのがあるんだけど、これを学校をまとめてRSS化するようにした。

Nodejsで作ってる。

$ node -v
v10.16.0

コード

これを

$ node mirai-compass-rss-index.js

として、見たいのが

学校 イベントURL
女子美術大学付属中学校(東京都) https://mirai-compass.net/usr/joshibij/event/evtIndex.jsf
成立学園中学校(東京都) https://mirai-compass.net/usr/seiritzj/event/evtIndex.jsf
かえつ有明中学校(東京都) https://mirai-compass.net/usr/kariakej/event/evtIndex.jsf
国士舘高等学校(東京都) https://mirai-compass.net/usr/kokushih/event/evtIndex.jsf
常総学院中学校(茨城県 https://mirai-compass.net/usr/josogj/event/evtIndex.jsf
淑徳与野中学校(埼玉県) https://mirai-compass.net/usr/shukuynj/event/evtIndex.jsf

だったら、
http://localhost:3000/?school=seiritzj&school=kariakej&school=kokushih&school=josogj&school=shukuynj
というURLにアクセスする。

最初、どっかのサーバーにおいてサービス提供するかなと思ったけど、「サービス落ちてて説明会予約できなかった!(怒)」とか言われると嫌なのでやめた。

Laravel-AdminLTEを使ってみたメモ

AdminLTEを使いたかったのでこちらを使った。以前は違うの使ってた

インストールとかは簡単なので略

@extends('adminlte::master')

adminlte::は、ServiceProviderで付け加えてるっぽい。

vendor/jeroennoten/laravel-adminlte/src/ServiceProvider.php
<?php
    private function loadViews()
    {
        $viewsPath = $this->packagePath('resources/views');

        $this->loadViewsFrom($viewsPath, 'adminlte');

のadminlteがこのパッケージ(jeroennoten/Laravel-AdminLTE)のresources/viewsを使うPrefixになるみたい。

リンク先とかFormのAction先の変更

あとは、URLとかrouteを変えたら

{{ url(config('adminlte.dashboard_url', 'home')) }}

とかあるんだけど、これは固定なのでポチポチと変更をしていく。んだけど、これって

{{ route('user.login') }}

とかのRouteのname使った方が良いんじゃないかな?

Laravelのauthを追ってみたメモ

$artisan make:auth
したという前提で。

Laravel5.4でマルチ認証(userとadmin)を実装する方法 | 大分のITコンサルタント | 高橋商店 というのをやってみて、認証の中で振り分けをしたかったというケース。

普通にファーストアクセスで認証必要なURLにアクセスした場合

URL→routes→Controllerなので…

最初はController

認証を掛けたいController内でAuthを呼ぶ。

<?php
class UserController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth:user,user1,user2,user3');
    }

ってやると、MiddleWareで登録されたauthが起動する。

app/Http/Kernel.php

app/Http/Kernel.php
<?php
protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,

のauthにあるauthとさっきの$this->middleware('authのauthが対応している。auth:user, user1,user2,user3のuser1,user2,user3はMiddleWare(この場合Authenticate)の$guards引数になる。

ちなみに、$routeMiddleWareというのは$middleWareと違ってControllerでこうやって$this->middleware()して任意に起動させるもので、$middleWareは全てのrouteで起動する。

Authenticate.php

で、この起動するインスタンスは app/Http/Middleware/Authenticate.php なので、このインスタンスのhandleメソッドが実行される。

これは、Illuminate\Auth\Middleware\Authenticateを継承しているので、

Illuminate\Auth\Middleware\Authenticate
<?php
    public function handle($request, Closure $next, ...$guards)
    {
        $this->authenticate($request, $guards);

        return $next($request);
    }

がメソッド。で、この$guardsがさっきの$this->middleware('auth:user,user1,user2,user3')した引数になってて、

<?php
$guards = ['user', 'user1', 'user2', 'user3', ];

となっている。

Illuminate\Auth\Middleware\Authenticate

この $guardsはIlluminate\Auth\Middleware\Authenticate->authenticate()で使われて、

Illuminate\Auth\Middleware\Authenticate
<?php
    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);
            }
        }

        throw new AuthenticationException(
            'Unauthenticated.', $guards, $this->redirectTo($request)
        );
    }

となっている。このforeach($guardsに $guards = ['user', 'user1', 'user2', 'user3', ]; が入ってくる。この場合4つのGuardsを使うことになる。

で、この$this->authは Illuminate/Auth/AuthManager.phpインスタンス

Illuminate\Auth\AuthManager と config/auth.php

ここで、check()メソッドを持つインスタンスをconfig/auth.phpに基づいて返す。

auth.phpでは、'guards' => に入っているもので

config/auth.php
<?php
    'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],
   ///////////////////////
    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'user' => [
          'driver' => 'session',
          'provider' => 'custom_users',
        ],
   ///////////////////////
    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\User::class,
        ],

となっているので、$this->middleware('auth:user')とすると、driverにsession/providerにcustom_usersを使ったものがcheck()を実行する。
ちなみに、$this->middleware('auth')だと、defaultsのguardは'web'なのでこれまたdriverにsession/providerにusersを使ったものが実行される。

AuthのProviderは、Modelとつながっている

と、ここまでで、普通にControllerで$this->middleware('auth')としたときにはguardsのwebが呼ばれて、そのコードはproviderで示されるusersなのである。

で、Illuminate\Auth\Middleware\Authenticate::authenticate()の

Illuminate\Auth\Middleware\Authenticate::authenticate()
<?php
            if ($this->auth->guard($guard)->check()) {
                return $this->auth->shouldUse($guard);
            }

では、$this->authがIlluminate\Auth\AuthManagerなので、そのguardメソッドを見るとAuthManager::resolve()で$guardに沿ったインスタンスを作っている。

'web'の場合、driverがsessionなのでAuthManager::createSessionDriver()である。これは$this->auth->guard($guard) がIlluminate\Auth\SessionGuardを作っている。

このSessionGuardインスタンスは、以下の経緯で作られる$providerを持っている。

Illuminate\Auth\CreatesUserProviders::createUserProvider($provider)   
で、$providerがeloquentであり、特に$providerに対するカスタムなCreatorが無い   
  ↓  
Illuminate\Auth\CreatesUserProviders::createEloquentProvider($config)   
これはEloquentUserProviderインスタンスをnewする。  
  ↓  
new EloquentUserProvider($this->app['hash'], $config['model']);  

また、SessionGuardインスタンスは、識別子$name(この場合は'user')と$sessionと(Http)Request $requestを持っている。

Illuminate\Auth\SessionGuard のcheck()

は、Illuminate\Auth\GuardHelpers::check()のtraitで、SessionGuard::user()の返り値がNULLでないかをチェックしている。
このuser()メソッドは

  • ログアウトのタイミングならnull
  • SessionGuard::$user (Illuminate\Contracts\Auth\Authenticatable) があればそれを返す。これは既にログイン処理がされている場合にはSessionからRestoreされるっぽい。

とかまぁなんかそんな感じ。

ここまでで、このSessionGuard::$userが得られれば認証通過。

ログインが認可されているセッションCOOKIEを持っている場合

SessionGuard::user()で$this->userを作成する処理を行う。

Illuminate\Auth\SessionGuard::user()
<?php
        if (! is_null($id) && $this->user = $this->provider->retrieveById($id)) {
            $this->fireAuthenticatedEvent($this->user);
        }

の部分。以降では逐一DBにアクセスする必要は無くなる。

「永続ログイン」のチェックを入れてログインしていると、

Illuminate\Auth\SessionGuard::user()
<?php
        if (is_null($this->user) && ! is_null($recaller = $this->recaller())) {
            $this->user = $this->userFromRecaller($recaller);

の部分で$this->userの復帰をしてくれる。

LaravelはSessionを自前で実装している。

…とここまで読んで、LaravelはPHPの生のセッション機能を使ってなくて独自でSession構築しているということに気が付いた。えー。秘密情報をCOOKIEに入れてるのかな?…あれ?これ以前も「すげー」って思った記憶あるな…いや、それはRailsだったっけ?Laravelのデフォルトだとセッション情報はファイルで storage/framework/sessions に保存してる。

ログインの時の処理。

DBに接続する処理があるのはここの部分だけですね。

Auth\LoginController@login

メソッドとしてはこのメソッドなので
Illuminate\Foundation\Auth\AuthenticatesUsers::login()を見に行く。

Illuminate\Foundation\Auth\AuthenticatesUsers
<?php
   public function login(Request $request)
    {
        $this->validateLogin($request);

        // If the class is using the ThrottlesLogins trait, we can automatically throttle
        // the login attempts for this application. We'll key this by the username and
        // the IP address of the client making these requests into this application.
        if (method_exists($this, 'hasTooManyLoginAttempts') &&
            $this->hasTooManyLoginAttempts($request)) {
            $this->fireLockoutEvent($request);

            return $this->sendLockoutResponse($request);
        }

        if ($this->attemptLogin($request)) {
            return $this->sendLoginResponse($request);
        }

        // If the login attempt was unsuccessful we will increment the number of attempts
        // to login and redirect the user back to the login form. Of course, when this
        // user surpasses their maximum number of attempts they will get locked out.
        $this->incrementLoginAttempts($request);

        return $this->sendFailedLoginResponse($request);
    }

validateLogin()

ここで、ユーザー名とパスワードの入力チェック。独自のチェックを入れたかったら、ControllerでOverrideすればいい。

attemptLogin()

hasTooManyLoginAttempts()は、多重ログインPOSTの防止かな。あまり読んでない。
ログイン処理のメインはattemptLogin()で

Illuminate\Foundation\Auth\AuthenticatesUsers
<?php
    protected function attemptLogin(Request $request)
    {
        return $this->guard()->attempt(
            $this->credentials($request), $request->filled('remember')
        );
    }

で、guard()で得たインスタンス(SessionGuard)のattempt()を実行する。これは

Illuminate\Auth\SessionGuard
<?php
        $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);

        if ($this->hasValidCredentials($user, $credentials)) {
            $this->login($user, $remember);

            return true;
        }

DBにつなぐところは、 $this->provider->retrieveByCredentials()の所。このproviderはlluminate\Auth\EloquentUserProviderである。このretrieveByCredentialsメソッドが

lluminate\Auth\EloquentUserProvider
<?php
   public function retrieveByCredentials(array $credentials)
    {
        if (empty($credentials) ||
           (count($credentials) === 1 &&
            array_key_exists('password', $credentials))) {
            return;
        }

        // First we will add each credential element to the query as a where clause.
        // Then we can execute the query and, if we found a user, return it in a
        // Eloquent User "model" that will be utilized by the Guard instances.
        $query = $this->newModelQuery();

        foreach ($credentials as $key => $value) {
            if (Str::contains($key, 'password')) {
                continue;
            }

            if (is_array($value) || $value instanceof Arrayable) {
                $query->whereIn($key, $value);
            } else {
                $query->where($key, $value);
            }
        }

        return $query->first();
    }

となっている。newModelQuery()でQueryを取っているが、このORMはauth.phpに記述した 'providers' => ['users' => [ のmodelのApp\User::classとなる。

ちなみにこの定義が無いとデフォルトのグローバルの\Illuminate\Database\Eloquent\Builderがnewされて作られる。…が、ほとんど意味が無いだろう。テーブル持って無いし。ちゃんと定義しろということですね。これで、メールアドレスを指定でDBから1行を抜き出す。

で、その後は更にhasValidCredentials()に流れて、

Illuminate\Auth\SessionGuard
<?php
    protected function hasValidCredentials($user, $credentials)
    {
        return ! is_null($user) && $this->provider->validateCredentials($user, $credentials);
    }

で、この$this->providerはIlluminate\Auth\EloquentUserProviderのインスタンスで、validateCredentialsでパスワードと比較している。

Illuminate\Auth\EloquentUserProvider
<?php
    public function validateCredentials(UserContract $user, array $credentials)
    {
        $plain = $credentials['password'];

        return $this->hasher->check($plain, $user->getAuthPassword());
    }

ちなみに、この時UserNameとして何を使いたいかというと、Illuminate\Foundation\Auth\AuthenticatesUsersのusername()メソッドをControllerで上書きすればよい。デフォルトだと以下のようにemailが使われる。

Illuminate\Foundation\Auth\AuthenticatesUsers
<?php
    public function username()
    {
        return 'email';
    }

ログインに失敗した時

単純に権限のない所に入ってしまった場合

これは、Illuminate\Auth\Middleware\Authenticate->authenticate()で例外AuthenticationExceptionを投げられる。

これは、app\Exceptions\Handler::unauthenticated() でキャッチされる。

app\Exceptions\Handler
<?php
    protected function unauthenticated($request, AuthenticationException $exception)
    {
      if ($request->expectsJson()) {
        return response()->json(['error' => 'Unauthenticated.'], 401);
      }

      //追加
      if (in_array('admin', $exception->guards())) {
        return redirect()->guest('admin/login');
      }
      //追加

      return redirect()->guest('login');
    }

エラーハンドリング

で、なんでAuthenticationException エラーの時だけunauthenticatedメソッドが使われるかというと、最終的にはapp\Exceptions\Handler::render()でキャッチされるのだけど、 Illuminate\Foundation\Exceptions\Handlerで、

Illuminate\Foundation\Exceptions\Handler
<?php
    public function render($request, Exception $e)
    {
        if (method_exists($e, 'render') && $response = $e->render($request)) {
            return Router::toResponse($request, $response);
        } elseif ($e instanceof Responsable) {
            return $e->toResponse($request);
        }

        $e = $this->prepareException($e);

        if ($e instanceof HttpResponseException) {
            return $e->getResponse();
        } elseif ($e instanceof AuthenticationException) {
            return $this->unauthenticated($request, $e);
        } elseif ($e instanceof ValidationException) {
            return $this->convertValidationExceptionToResponse($e, $request);
        }

        return $request->expectsJson()
                        ? $this->prepareJsonResponse($request, $e)
                        : $this->prepareResponse($request, $e);
    }

となっていて、AuthenticationExceptionの例外の場合はunauthenticated()メソッドが実行されるため。

権限のないところに入ったら、所定のログイン場所に飛ばしたい

投げられる認証の例外AuthenticationExceptionのnewはapp\Http\Middleware\Authenticateの継承元である

Illuminate\Auth\Middleware\Authenticate
<?php
   protected function authenticate($request, array $guards)
    {
        // 省略
        throw new AuthenticationException(
            'Unauthenticated.', $guards, $this->redirectTo($request)
        );
    }

なので、$this->redirectTo($request)がキーになるので、app\Http\Middleware\AuthenticateのredirectTo先を変更してやると良い。

…が、このMiddlewareの$thisは自分がadminなのかuserなのかが分からないので、どこにredirectするべきかが分からない…

というのは、

Illuminate\Auth\Middleware\Authenticate
<?php
    protected function authenticate($request, array $guards)
    {
        /// 省略
        throw new AuthenticationException(
            'Unauthenticated.', $guards, $this->redirectTo($request)
        );
    }

とnewしているところで$guardsを渡してくれてない…

ということで、Http\Middleware\Authenticateにguardsを渡してその先頭を取る

どのguardsで引っかかったかというのはコアのAuthenticateでは取れないので、

app\Http\Middleware\Authenticate
<?php
    /**
     * @var Array
     */
    private $guards ;

    public function handle($request, Closure $next, ...$guards)
    {
        $this->guards = $guards;

として、handle時に持たせてやり、configのauthにredirectoToを生やして

config/auth.php
<?php
   'guards' => [
        'user' => [
            'driver' => 'session',
            'provider' => 'users',
            'redirectTo' => 'user.login'
        ],

redirectoToで使用した。

app\Http\Middleware\Authenticate
<?php
    protected function redirectTo($request)
    {
        $redirectTo = sprintf('auth.guards.%s.redirectTo', current($this->guards));
        $redirectTo = \Config::get($redirectTo) ?? '';
        if (!$request->expectsJson()) {
            return route($redirectTo);
        }
    }

んー。まぁ、こんな感じで。

Auth::routes()でのRouteの追加

routes/web.phpに Auth::routes(); を記載すると、Illuminate\Routing\Router->auth()が実行される。

Illuminate\Routing\Router
<?php
    public function auth(array $options = [])
    {
        // Authentication Routes...
        $this->get('login', 'Auth\LoginController@showLoginForm')->name('login');
        $this->post('login', 'Auth\LoginController@login');
        $this->post('logout', 'Auth\LoginController@logout')->name('logout');

        // Registration Routes...
        if ($options['register'] ?? true) {
            $this->get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
            $this->post('register', 'Auth\RegisterController@register');
        }

        // Password Reset Routes...
        if ($options['reset'] ?? true) {
            $this->resetPassword(); // このメソッドで以下の4つ追加
//        $this->get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request');
//        $this->post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email');
//        $this->get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset');
//        $this->post('password/reset', 'Auth\ResetPasswordController@reset')->name('password.update');
        }

        // Email Verification Routes...
        if ($options['verify'] ?? false) {
            $this->emailVerification(); // このメソッドで以下の3つ追加
//        $this->get('email/verify', 'Auth\VerificationController@show')->name('verification.notice');
//        $this->get('email/verify/{id}', 'Auth\VerificationController@verify')->name('verification.verify');
//        $this->get('email/resend', 'Auth\VerificationController@resend')->name('verification.resend');
        }
    }

となっているので、手動で追加したい場合はこのrouteを記載する。

Login時のSessionGuardをうまく働かせる

ログインした状態で認証をパスする場合、Illuminate\Auth\SessionGuard->user()でセッションを見るんだけど、この場合

$this->middleware('auth:user')

で保存しなければならない。チェックする時が「auth:user」でなので。これを指定せずにいると、デフォルトのwebが使われてセッションに保存されてしまって認証状態ではないと判断してしまう。

SessionGuard::login() でSessionにデータを保存する処理を追ってみる

結局、SessionGuardをnewする所で$nameを与えているので、

Illuminate\Foundation\Auth\AuthenticatesUsers.php
<?php
    protected function guard()
    {
        return Auth::guard();
    }

ここで$nameを与えてないので仕方ない…

ということで、このAuth::gurad($name)っていう感じのClassを作ってOverrideする。

app\Http\Controllers\Auth\LoginController
<?php
// use Illuminate\Foundation\Auth\AuthenticatesUsers
class LoginController extends Controller
{
    use AuthenticatesUsers;

    protected $guard_name = 'user';

とControllerで$nameを指定できるようにしオリジナルのを使わないようにして、自前のAuthenticatesUsersを作る。

で、同じディレクトリに

app\Http\Controllers\Auth\AuthenticatesUsers
<?php
namespace App\Http\Controllers\Auth;

use Illuminate\Foundation\Auth\AuthenticatesUsers as AuthenticatesUsersOrig;
use Illuminate\Support\Facades\Auth;

trait AuthenticatesUsers
{
    use  AuthenticatesUsersOrig;

    protected function guard()
    {
        return Auth::guard($this->guard_name);
    }
}

とやっておく。

2019年度 卓球日本リーグ選手データ

チーム紹介 | 日本卓球リーグ実業団連盟からデータを取得してまとめた。一覧はこちら

利き手

男子は4人に1人が左。意外と多い。

全体 206名 男子 111名 女子 95名
165 80% 86 77% 79 83%
41 20% 25 23% 16 17%

ラケット

もはやペンは絶滅寸前。しかし、ペン平均年齢は26歳(全体平均年齢24歳)と意外と年寄りばかりではない。*1

全体 206名 男子 111名 女子 95名
シェーク 199 97% 107 96% 92 97%
ペン 7 3% 4 4% 3 3%

ラバー

燦然と輝く一枚表は、森美紗樹選手。

全体 206名 男子 111名 女子 95名
裏裏 150 73% 99 89% 51 54%
裏表 38 18% 4 4% 34 36%
表裏 5 2% 2 2% 3 3%
裏粒 10 5% 4 4% 6 6%
2 1% 2 2% 0 0%
一枚表 1 0% 0 0% 1 1%

戦型

女子の異質攻撃は鈴木理彩選手。 安藤みなみ選手はJTTAサイトでは「攻撃」と書いてあって、なんだかよくわからない…

全体 206名 男子 111名 女子 95名
ドライブ 158 77% 102 92% 56 59%
カット 18 9% 5 5% 13 14%
前陣速攻 28 14% 4 4% 24 25%
異質攻撃 1 1% 0 0% 1 1%
攻撃 1 1% 0 0% 1 1%

*1:吉田海偉選手がひとりで2歳おしあげているので実質24歳で変わらない。