20210105のlaravelに関する記事は7件です。

Laravelで投稿一覧を表示、一意のURLをつけて表示するまで。備忘録

Laravelにてただいま絶賛webアプリ作成中なのですが、久しぶりに備忘録を書こうと思います。
userが投稿してそれを全体のページに表示するのと、その表示したページにアクセスして一意(表現があっているかわからないけど)のURLを作成してアクセスするところまでできたので備忘録にて記載したいと思います。
自分でも備忘録を記載しているついでに微妙なコードがあったら直したいので、、

処理の流れとしては
1.userがログイン後/articleページに移動する。
2.articleの投稿ボタンが押下されたら、自身のページとトップページに投稿内容を表示する。
3.投稿内容をクリックもしくはタッチするとその一意ページに飛ぶ。

1.userがログイン後/articleページに移動する。

article投稿.png

かなりシンプルな見た目にはなっているのですが、とりあえず動いてなんぼだと思っているので、ご愛敬という事で、、
適当にテスト!!!と打ち、記事を追加するを押下します。

user.png

全体ページの「テスト!!!」を押下すると、

するとuser自身のページと全体のページに投稿が反映されています。これがユーザー自身のページ。下が全体ページとなります。

全体.png

一意ページ.png
一意のURLを持ったページが表示されます。

とりあえず上記で使っているファイル一覧が以下

ビューファイル一覧

  • voting.blade.php(全体ページのビューファイル)
  • home.blade.php(user自身のページのビューファイル)
  • article.blade.php(投稿するためのページのビューファイル)
  • article_display.php(投稿結果の一意のURL(id)を持つビューファイル)

コントローラファイル一覧

  • VotinCotroller.php
  • UserController.php
  • ArticleController.php

テーブルファイル一覧

  • user_creates_table
  • article_creates_table
  • create_votings_table

モデルファイル一覧

  • User.php
  • Article.php
  • Voting.php

なおルーティングファイルはweb.phpとなります。

articleの処理

User.php
   public function articles(){
        /**
         * リレーション一対多の一側
         * user(1):article(多)
         * articleテーブルを新しい順で更新する。
         * 
         * 
           */
        return $this->hasMany('App\Models\Article')->latest();
    }
Article.php
    /**
     * 一対多の多側、article
     * votingとarticleのリレーション
    */
    public function voting(){
        return $this->belongsTo('App\models\Voting');
    }

articleはuserと一体多の関係なのでリレーションします。
userが一でarticleが多です。
リレーションについては別の機会に記載したいと思います。
これがリレーションの根幹となります。

UserController.php
public function show(User $user)
    {
        /**
         * $userによる投稿を取得
         * 投稿作成日が新しい順に並べる。
         * articleのpagenateをユーザー自身のページを5ページ
         * */ 
        $user->articles = $user->articles()->paginate(5);
        return view('users.show', ['user' => $user]);     
    }

$userのarticleカラムに投稿内容を追加してpagenateしています。
その後、return viewでusers.showを表示しています。

Article.Controller
public function store(Request $request){
        $article = new Article();
        /** 
        * バリデーションを設定する。
        */
        $validator = Validator::make($request->all(), [
            /* 入力必須255文字 form のarticleのバリデーションチェック*/
            'article' => 'required|max:255',
        ]);
        if ($validator->fails()) {
            return redirect('/article')
                ->withInput()
                ->withErrors($validator);    
        }
        /**
         * 以下はブレードファイルのarticleのnameを指定して値をとっている。
         * この処理はarticleの内容を保存している。
         * ->articleはカラム
         * articleのidが表示される。
         */  
        $article->user_id = $request->user()->id;
        /**
         * 上記の$article->user_idはarticleのユーザーid格納カラム。
         *articleのタイトルをテーブルから呼び出して$article->article_title 
        */
        $article->article = $request->article;
        /**articleが投稿された時点でidは発行されているのでそこに紐づけしたい。
         * 例:
         * "user_id" => 3
         * "article" => "dd"
         * "updated_at" => "2021-01-05 03:11:39"
         * "created_at" => "2021-01-05 03:11:39"
         * "id" => 34←これをURLにする。
         * 
        */  
        $article->save();    
        return redirect(("/home"));
    }

articleをnewします。
その下でバリデーションしています。ここで入力判定して文字を入力しないで投稿ボタンを押すと入力してくださいと表示されarticleにredirectします。

ルーティングweb.php
Route::post('/article',  [App\Http\Controllers\ArticleController::class, 'store'])->name('articlepost');

上記でコントローラを呼び出しarticle_post->nameしてarticle.blade.phpで呼び出しています。

$article->user_id = $request->user()->id;
article.blade.php
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="col-sm-offset-2 col-sm-8">
        <div class="panel panel-default">
            <div class="panel-body">
                @include('common.errors')
                <form action="{{ route('articlepost')}}"  method="POST" class="form-horizontal">
                   {{ csrf_field() }}
                   <div class="form-group">
                       <label for="task-name" class="col-sm-3 control-label">記事を以下に書く</label>
                     <!-- タイトル本文 -->
                      <div class="form-group">
                           <div class="col-sm-6">
                              <textarea rows="10" cols="100" name="article" name="contents" class= "form-control" id="message" style="resize:none"></textarea> 
                           </div>
                       </div>
                         <div class="form-group">
                            <div class="col-sm-offset-3 col-sm-6">
                                <button type="submit" class="btn btn-default">
                                    <i class="fa fa-plus"></i>記事を追加する
                                </button>
                            </div>
                         </div>
                    </div>
                </form>
            </div>
        </div>
    </div>
    <!-- ここからユーザー自身が書いたarticleを表示する -->
</div>
@endsection

上記が投稿するためのarticleのviewです。

home.blade.php
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card" class="col-sm-offset-2 col-sm-8">
                <div class="card-header">{{ __('Dashboard') }}</div>
                <div class="card-body">{{ Auth::user()->name }}</div>
                <div class="card-body">{{ Auth::user()->email }}</div>
                <div class="card-body">{{ Auth::user()->id }}</div>
                <!-- ここにユーザーの記事一覧を表示する -->
                <table class="table table-striped">
                <thead>
                    <tr>
                        <th>{{ __('Author') }}</th>
                        <th>{{ __('article') }}</th>
                    </tr>
                    <!-- ログイン中のユーザー=user その中のarticles -->
                    @foreach($user -> articles as $article)
                        <tr>
                            <td>
                                <!-- usernameを出力する -->
                                {{$user->name}}
                            </td>
                            <td>
                                <!-- articleテーブルのarticletitleに変更 -->

                                {{$article->article}}
                            </td>
                        </tr>
                    @endforeach
                </thead>
                </table>

                <form action="/">
                    <button type="submit" class="btn btn-light">みんなの記事一覧へ</button>
                </form>
                <a href="/article"><button type="submit" class="btn btn-light">記事作成ページへ移動する</button></a>

                <!-- ユーザー情報を削除ボタン押したらtopに戻る補足:できればパスワードを要求するusercontrolerを使うか?homeからできるならやる -->
                <!-- 削除ボタンを作る -->
                <!-- actionでurl指定しているユーザーのidを表示できる-->
                <!-- ボタンを押すと画面が遷移できるので処理はrouteは呼べているが削除ができない -->
                <!-- ここのuseridはコントローラのuseridを示している -->
                <form action="{{ route('user_delete')}}" method="post" id="user_delete_form">
                    @method('DELETE')
                    @csrf                
                    <button type="submit" class="btn btn-danger" onclick="return confirm('Okボタンを押すとすべてのユーザー情報を削除してしまい、復活もできません。よろしいですか?')">
                    <i class="fa fa-trash">{{ __('ユーザー情報を削除する。') }}</i>       
                    </button>
                </form>
                <!--  -->
                <div class="card-body">
                    @if (session('status'))
                        <div class="alert alert-success" role="alert">
                            {{ session('status') }}
                        </div>
                    @endif
                    {{ __('ログインされています。') }}
                </div>
            </div>
        </div>      
    </div>
    <div class="d-flex justify-content-center mb-5">{{$user->articles->links()}}</div>
</div>
@endsection

上記がuserのページです。home.blade.phpとなっているので少しややこしいですが、、

saveを使って保存し、自身のページにredirectします。
以上が投稿を保存しuserのページ一覧に表示する流れです。
続いて全体ページに表示する処理です。

VotingController
  public function index(User $user)
    {

        /** 
         * 新しい順で表示する。
         */
        $articles = Article::select(['article','user_id','id'])->orderBy('created_at', 'desc')->get();    
        return view('votings', [
            'articles' => $articles
            ]);
    }

これもわかりづらいですが、votingが全体ページの根幹です。
Articleの中に紐づくarticle,user_id,'id'を$articlesを代入しています。
新しい順に取得し表示するために->orderBy('created_at', 'desc')->get();

としています。

voting.blade.php
@extends('layouts.app')
@section('content')
<div class="container">
    <div class="col-sm-offset-2 col-sm-8">
        <div class="panel panel-default">
            <div class="panel-heading">
                記事一覧
            </div>

        </div>
        <!-- 記事一覧 -->
        @if (count($articles)>0)
           <div class="panel-body">
               <div class="panel-heading">
                記事一覧
               </div>
               <div class="panel-body">
                   <table class="table table-striped task-table">
                       <thead>
                           <th>記事</th>
                           <th>&nbsp;</th>
                        </thead>
                       <tbody>
                           @foreach ($articles as $article)
                              <tr>

                                  <!-- ここにurlの投稿一覧を表示する -->
                                  <!-- 以下にaタグをつかってとりあえずarticle_idをddできるようにする
                                  接続するにはまずコントローラ側で/article/{id}で指定しなければならない 
                                  'id' => $article->idこれでarticleのidを取得している
                                  -->
                                  <td class="table-text">
                                     <div class="table-text">
                                         <a href = "{{ route('article_display', ['id' => $article->id])}}">
                                            {{ $article->article }}
                                         </a>
                                     </div>
                                  <td class="table-text"><div class="table-text">{{ $article->user_id }}</div></td>
                                  <td class="table-text"><div class="table-text">{{ $article->id }}</div></td>
                             </tr>
                            @endforeach
                        </tbody>
                    </table>      
                </div>
            </div>
        @endif


    </div>
    {{-- ページネーション --}}

</div>
@endsection
VotingController
return view('votings', [
            'articles' => $articles
            ]);

ここでvotingのビューにarticlesのプロパティを渡しています。

voting.blade.php
<tbody>
                           @foreach ($articles as $article)
                              <tr>

                                  <!-- ここにurlの投稿一覧を表示する -->
                                  <!-- 以下にaタグをつかってとりあえずarticle_idをddできるようにする
                                  接続するにはまずコントローラ側で/article/{id}で指定しなければならない 
                                  'id' => $article->idこれでarticleのidを取得している
                                  -->
                                  <td class="table-text">
                                     <div class="table-text">
                                         <a href = "{{ route('article_display', ['id' => $article->id])}}">
                                            {{ $article->article }}
                                         </a>
                                     </div>
                                  <td class="table-text"><div class="table-text">{{ $article->user_id }}</div></td>
                                  <td class="table-text"><div class="table-text">{{ $article->id }}</div></td>
                             </tr>
                            @endforeach
                        </tbody>
                    </table>      

ここでarticle一覧を取得し、表示している。

とりあえず今日は以上です。コードも粗い部分がありますが、徐々に調整したいと思います。なにか間違いがあればご指摘頂ければと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelはじめました

Larave(ディレクトリ構成)

本日からLaravelを触り出したので自分の勉強記録として投稿していこうと思う。

簡単にディレクトリ構成から勉強。
(フレームワークを余り触ったことがないので混乱)

ディレクトリ構成

1 2
app アプリのメインとなるところ
bootstrap 初期処理やキャッシュ
config アプリの設定
database データベース(マイグレーション)
public 画像,JS,CSS
resources blade(HTML的な)
routes アプリのURL設定など
storage セッションやログ
tests テスト用
vendor Composerの依存内容

設定ファイル(隠しフォルダにあり)

  • codeフォルダの歯車マーク押して、show hidden filesをクリック
  • .envフォルダを開く
php
DB_CONNECTION=mysql
DB_HOST=localhost         // 編集
DB_PORT=3306
DB_DATABASE=laravel_todo  // 編集
DB_USERNAME=root
DB_PASSWORD=root          // 編集 環境によって異なる 現在はAWS


dbマイグレーションとは

  • SQLを直接使わなくても、データベースを管理できるLaravelの仕組み。

  • テーブル作成の流れ
    1.マイグレーションファイルを生成
    2.ファイルにテーブル定義を書く
    3.マイグレーションを実行し、DBに反映

※まず、1テーブル1ファイルと覚えよう

マイグレーションの生成

  • ファイルの場所とファイル名のルール database/migrations

2020_12_20_00000_todos_table.php
  生成時間     アクション名 テーブル名

- ファイル生成コマンド

tarminal
php artisan make:migration todos_table

カラム名:add_カラム名to_userstable

tarminal
php artisan migrate

全ファイルが実行される

マイグレーションの中身

  • upとdawnメソッドがある
    upが実行(作成)で、downが元に戻す(削除)

  • Schemaファザードを使っている
    例)Schema::create テーブルを作成

  • カラムの作成はBlueprintオブジェクトのtableメソッドを使う
    例)$table->string('name')名前カラムを作成!

実際にファイルを見てみる

```言語:php database/migrations/2020_12~

/**
ここでマイグレーションファイルを読み込む
*/
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTodosTable extends Migration
{
/**
* Run the migrations.
*
* @return void
/
public function up()
{
/

Schemaファザードを使っている
if tableがなかったら作る
/
if(!Schema::hasTables('todos')){
Schema::create('todos', function (Blueprint $table) {
$table->id();
$table->string('todo');
$table->date('deadline');
$table->text('comment')->nullable();
/
 100文字制限の時は('comment',100) */
$table->timestamps();
});
}
}
}

マイグレーションを実行する

 ```言語:tarminal
php artisan migrate

結果

tarminal
Migration table created successfully.
Migrating: 2014_10_12_000000_todos_table
Migrated:  2014_10_12_000000_todos_table (31.23ms)

error
言語:tarminal
$ composer dump-autoload

からの再起動

とりあえずここまで。
次回に進む。

 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel6】ユーザー登録時のパスワードのバリデーションを強くする(カスタムバリデーション)

はじめに

Laravel6系でデフォルトで用意されているユーザー登録時のパスワードのバリデーションは以下のような簡易的なものなのでそれを強化してみます。

app/Http/Controllers/Auth/RegisterController.php
<?php
// 略

class RegisterController extends Controller
{
    // 略
    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
        ]);
    }
    // 略
}

passwordのバリデーション内容はこちら

バリデーションルール 内容
required 必須(入力されているか)
string 文字列であるか
min:8 8文字以上であるか
confirmed 確認用のパスワードと同じであるか

このレベルのバリデーションだと11111111aaaaaaaaなど予測しやすいパスワードの登録が可能になり、アプリケーションの脆弱性に繋がってしまいます。

というわけで今回は
半角英字(小文字)、半角英字(大文字)、半角数字を1文字以上含む8文字以上
というバリデーションにします。

環境

Laravel 6.8

カスタムバリデーションの実装方法

Laravelの公式ドキュメントの日本語訳ReadDoubleではカスタムバリデーションの実装方法は2つ紹介されています。

1. ルールオブジェクトの使用
2. Validatorファサードのextendメソッドの使用

この記事では前者のルールオブジェクトの使用の方法で説明します。

ReadDouble:バリデーション

ルールオブジェクトの作成

まず、ルールオブジェクトを作成します。
artisanファイルが存在するディレクトリ(=PJ直下)でターミナルに以下のコマンドを打って実行します。

php artisan make:rule CustomPasswordValidation

CustomPasswordValidationは好きな名前でOK

ルールオブジェクトの概要

ルールオブジェクトについてザックリ説明します。(ReadDouble通りに説明します)

先ほどのコードを実行すると以下のファイルが作成されているはずです。

app\Rules\CustomPasswordValidation.php
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class CustomPasswordValidation implements Rule
{
    /**
     * バリデーションの成功を判定
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        return strtoupper($value) === $value;
    }

    /**
     * バリデーションエラーメッセージの取得
     *
     * @return string
     */
    public function message()
    {
        return 'The :attribute must be uppercase.';
    }
}

ReadDoubleには以下の記載があります。

ルールオブジェクトは2つのメソッドを含みます。passesとmessageです。passesメソッドは属性の値と名前を受け取り、その属性値が有効であればtrue、無効であればfalseを返します。messageメソッドは、バリデーション失敗時に使用する、バリデーションエラーメッセージを返します。

まとめるとこんな感じ

メソッド名 書く処理 返り値
passes バリデーションルール true(成功) or false(失敗)
message エラーメッセージ passesメソッドの返り値がfalseの時に使うエラーメッセージ

カスタムバリデーションの作成

では実際に半角英字(小文字)、半角英字(大文字)、半角数字を1文字以上含む8文字以上のバリデーションを定義していきます。

完成形

app\Rules\CustomPasswordValidation.php
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class CustomPasswordValidation implements Rule
{
    //最小文字数
    private $minCharacter;
    //最大文字数
    private $maxCharacter;
    //1文字以上の半角英字(大文字)
    private $includeLessThanOneUpperLetter;
    //1文字以上の半角英字(小文字)
    private $includeLessThanOneLowerLetter;
    //1文字以上の半角数字
    private $includeLessThanOneNumber;

    /**
     * Create a new rule instance.
     *
     * @return void
     * @param int $minCharacter
     * @param null $maxCharacter
     * @param boolean $includeLessThanOneUpperLetter
     * @param boolean $includeLessThanOneLowerLetter
     * @param boolean $includeLessThanOneNumber
     */
    public function __construct(
        $minCharacter = 8,
        $maxCharacter = null,
        $includeLessThanOneUpperLetter = true,
        $includeLessThanOneLowerLetter = true,
        $includeLessThanOneNumber = true
    )
    {
        $this->minCharacter = $minCharacter;
        $this->maxCharacter = $maxCharacter;
        $this->includeLessThanOneUpperLetter = $includeLessThanOneUpperLetter;
        $this->includeLessThanOneLowerLetter = $includeLessThanOneLowerLetter;
        $this->includeLessThanOneNumber = $includeLessThanOneNumber;
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        $regexOfValidation = "[a-zA-z\d]";
        // 半角英字(小文字)1文字以上
        if ($this->includeLessThanOneLowerLetter) {
            $regexOfValidation = "(?=.*?[a-z])" . $regexOfValidation;
        }
        // 半角英字(大文字)1文字以上
        if ($this->includeLessThanOneUpperLetter) {
            $regexOfValidation = "(?=.*?[A-Z])" . $regexOfValidation;
        }
        // 半角数字1文字以上
        if ($this->includeLessThanOneNumber) {
            $regexOfValidation = "(?=.*?\d)" . $regexOfValidation;
        }
        // 最大、最小文字数
        if ($this->maxCharacter || $this->minCharacter) {
            $regexOfValidation = $regexOfValidation . "{{$this->minCharacter},{$this->maxCharacter}}";
        }
        $regexOfValidation = "/\A{$regexOfValidation}+\z/";

        return preg_match($regexOfValidation, $value);
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        $validationMessage = 'パスワードは';
        $validationOneLetterMessage = [];
        if ($this->includeLessThanOneLowerLetter) {
            $validationOneLetterMessage[] = '半角英字(小文字)';
        }
        if ($this->includeLessThanOneUpperLetter) {
            $validationOneLetterMessage[] = '半角英字(大文字)';
        }
        if ($this->includeLessThanOneNumber) {
            $validationOneLetterMessage[] = '半角数字';
        }
        if ($validationOneLetterMessage) {
            $validationMessage .= implode('、', $validationOneLetterMessage) . 'を1文字以上含む';
        }
        if ($this->minCharacter) {
            $validationMessage .= "{$this->minCharacter}文字以上";
        }
        if ($this->maxCharacter) {
            $validationMessage .= "{$this->maxCharacter}文字以下";
        }
        $validationMessage .= 'で入力してください';

        return $validationMessage;
    }
}

プロパティとコンストラクタの定義

app\Rules\CustomPasswordValidation.php
<?php

//略

class CustomPasswordValidation implements Rule
{
    //最小文字数
    private $minCharacter;
    //最大文字数
    private $maxCharacter;
    //1文字以上の半角英字(大文字)
    private $includeLessThanOneUpperLetter;
    //1文字以上の半角英字(小文字)
    private $includeLessThanOneLowerLetter;
    //1文字以上の半角数字
    private $includeLessThanOneNumber;

    /**
     * Create a new rule instance.
     *
     * @return void
     * @param int $minCharacter
     * @param null $maxCharacter
     * @param boolean $includeLessThanOneUpperLetter
     * @param boolean $includeLessThanOneLowerLetter
     * @param boolean $includeLessThanOneNumber
     */
    public function __construct(
        $minCharacter = 8,
        $maxCharacter = null,
        $includeLessThanOneUpperLetter = true,
        $includeLessThanOneLowerLetter = true,
        $includeLessThanOneNumber = true
    )
    {
        $this->minCharacter = $minCharacter;
        $this->maxCharacter = $maxCharacter;
        $this->includeLessThanOneUpperLetter = $includeLessThanOneUpperLetter;
        $this->includeLessThanOneLowerLetter = $includeLessThanOneLowerLetter;
        $this->includeLessThanOneNumber = $includeLessThanOneNumber;
    }

    //略
}

そもそもこのやり方はLaravelのパスワードの複雑性バリデーションを参考にして作成しましたが、ここはわざわざコンストラクタを定義しなくても以下のコードでも良い気もしてます。

app\Rules\CustomPasswordValidation.php
<?php

//略

class CustomPasswordValidation implements Rule
{
    //最小文字数
    private const MIN_CHARACTER = 8;
    //最大文字数
    private const MAX_CHARACTER = null;
    //1文字以上の半角英字(大文字)
    private const INCLUDE_LESS_THAN_ONE_UPPER_LETTER = true;
    //1文字以上の半角英字(小文字)
    private const INCLUDE_LESS_THAN_ONE_LOWER_LETTER = true;
    //1文字以上の半角数字
    private const INCLUDE_LESS_THAN_ONE_NUMBER = true;

    //略
}

このようにクラス定数(const)を定義する方法でも良いのではないかと思います。
(これでも問題なくバリデーションは動作しました)

プロパティorクラス定数は以下の通りですね。

プロパティ
(クラス変数)
バリデーションルール 今回の設定
$minCharacter
(MIN_CHARACTER)
最小文字数 8文字以上
$maxCharacter
(MAX_CHARACTER)
最大文字数 指定なし(null)
$includeLessThanOneUpperLetter
(INCLUDE_LESS_THAN_ONE_UPPER_LETTER)
1文字以上の半角英字(大文字) チェックする(true)
includeLessThanOneLowerLetter
(INCLUDE_LESS_THAN_ONE_LOWER_LETTER)
1文字以上の半角英字(小文字) チェックする(true)
$includeLessThanOneNumber
(INCLUDE_LESS_THAN_ONE_NUMBER)
1文字以上の半角数字 チェックする(true)

このように1つ1つの項目に値を定義している理由は今後、バリデーションの強度を簡単に変えるためです。

例えば

  • 最大文字数を指定したいなと思ったら$maxCharacter(MAX_CHARACTER)に数字を入れる
  • $includeLessThanOneNumber(INCLUDE_LESS_THAN_ONE_NUMBER)falseにすれば1文字以上の半角数字を入れなくてもユーザー登録が可能になる

などのようにここを修正するだけでバリデーションルールを変えることができます。

バリデーションルールの作成(passesメソッドの中身)

app\Rules\CustomPasswordValidation.php
<?php

//略

class CustomPasswordValidation implements Rule
{
    //略

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        $regexOfValidation = "[a-zA-z\d]";
        // 半角英字(小文字)1文字以上
        if ($this->includeLessThanOneLowerLetter) {
            $regexOfValidation = "(?=.*?[a-z])" . $regexOfValidation;
        }
        // 半角英字(大文字)1文字以上
        if ($this->includeLessThanOneUpperLetter) {
            $regexOfValidation = "(?=.*?[A-Z])" . $regexOfValidation;
        }
        // 半角数字1文字以上
        if ($this->includeLessThanOneNumber) {
            $regexOfValidation = "(?=.*?\d)" . $regexOfValidation;
        }
        // 最大、最小文字数
        if ($this->maxCharacter || $this->minCharacter) {
            $regexOfValidation = $regexOfValidation . "{{$this->minCharacter},{$this->maxCharacter}}";
        }
        $regexOfValidation = "/\A{$regexOfValidation}+\z/";

        return preg_match($regexOfValidation, $value);
    }

    //略
}

ここでは各バリデーションルールごとにregexOfValidationに正規表現を追加してます。

最後のこのコードで正規表現にマッチする(バリデーションOK=true)、しないか(バリデーションNG=false)を返します。

return preg_match($regexOfValidation, $value);

PHP:preg_match

今回のバリデーションの正規表現で難しいのはこの3つだと思います。

  • (?=.*?[a-z])
  • (?=.*?[A-Z])
  • (?=.*?\d)
正規表現 内容
?= 肯定先読み
. 改行以外の任意の1文字
*? 直前の文字が 0回以上 繰り返す
[a-z] a〜zの半角英数字

上記の内容でなぜ「少なくとも1文字以上の半角英字」というバリデーションが設定できるかはパスワード向け正規表現 /^(?=.?[a-z])(?=.?\d)[a-z\d]{8,100}$/i を解読するを見ればわかると思います。
(とてもわかりやすかったです)

正規表現の完成形はこちらです。

/\A(?=.*?\d)(?=.*?[A-Z])(?=.*?[a-z])[a-zA-z\d]{8,}+\z/

エラーメッセージの作成(messageメソッドの中身)

app\Rules\CustomPasswordValidation.php
<?php

//略

class CustomPasswordValidation implements Rule
{
    //略

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        $validationMessage = 'パスワードは';
        $validationOneLetterMessage = [];
        if ($this->includeLessThanOneLowerLetter) {
            $validationOneLetterMessage[] = '半角英字(小文字)';
        }
        if ($this->includeLessThanOneUpperLetter) {
            $validationOneLetterMessage[] = '半角英字(大文字)';
        }
        if ($this->includeLessThanOneNumber) {
            $validationOneLetterMessage[] = '半角数字';
        }
        if ($validationOneLetterMessage) {
            $validationMessage .= implode('、', $validationOneLetterMessage) . 'を1文字以上含む';
        }
        if ($this->minCharacter) {
            $validationMessage .= "{$this->minCharacter}文字以上";
        }
        if ($this->maxCharacter) {
            $validationMessage .= "{$this->maxCharacter}文字以下";
        }
        $validationMessage .= 'で入力してください';

        return $validationMessage;
    }
}

エラーメッセージは特に解説しなくて良いかなと思います。

最終的に返るメッセージ($validationMessage)は
パスワードは、半角英字(小文字)、半角英字(大文字)、半角数字を1文字以上含む8文字以上で入力してください
です。

カスタムバリデーションの有効化

最後にこれまで作成したカスタムバリデーションをユーザー登録時のバリデーションに追加します。

app/Http/Controllers/Auth/RegisterController.php
<?php
// 略

class RegisterController extends Controller
{
    // 略
    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', new CustomPasswordValidation, 'confirmed'],
        ]);
    }
    // 略
}

passwordnew CustomPasswordValidationを追加すればOK。

最後に

2通りの完成形を再度掲載して終わります。

【プロパティ&コンストラクタを使う】

app\Rules\CustomPasswordValidation.php
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class CustomPasswordValidation implements Rule
{
    //最小文字数
    private $minCharacter;
    //最大文字数
    private $maxCharacter;
    //1文字以上の半角英字(大文字)
    private $includeLessThanOneUpperLetter;
    //1文字以上の半角英字(小文字)
    private $includeLessThanOneLowerLetter;
    //1文字以上の半角数字
    private $includeLessThanOneNumber;

    /**
     * Create a new rule instance.
     *
     * @return void
     * @param int $minCharacter
     * @param null $maxCharacter
     * @param boolean $includeLessThanOneUpperLetter
     * @param boolean $includeLessThanOneLowerLetter
     * @param boolean $includeLessThanOneNumber
     */
    public function __construct(
        $minCharacter = 8,
        $maxCharacter = null,
        $includeLessThanOneUpperLetter = true,
        $includeLessThanOneLowerLetter = true,
        $includeLessThanOneNumber = true
    )
    {
        $this->minCharacter = $minCharacter;
        $this->maxCharacter = $maxCharacter;
        $this->includeLessThanOneUpperLetter = $includeLessThanOneUpperLetter;
        $this->includeLessThanOneLowerLetter = $includeLessThanOneLowerLetter;
        $this->includeLessThanOneNumber = $includeLessThanOneNumber;
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        $regexOfValidation = "[a-zA-z\d]";
        // 半角英字(小文字)1文字以上
        if ($this->includeLessThanOneLowerLetter) {
            $regexOfValidation = "(?=.*?[a-z])" . $regexOfValidation;
        }
        // 半角英字(大文字)1文字以上
        if ($this->includeLessThanOneUpperLetter) {
            $regexOfValidation = "(?=.*?[A-Z])" . $regexOfValidation;
        }
        // 半角数字1文字以上
        if ($this->includeLessThanOneNumber) {
            $regexOfValidation = "(?=.*?\d)" . $regexOfValidation;
        }
        // 最大、最小文字数
        if ($this->maxCharacter || $this->minCharacter) {
            $regexOfValidation = $regexOfValidation . "{{$this->minCharacter},{$this->maxCharacter}}";
        }
        $regexOfValidation = "/\A{$regexOfValidation}+\z/";

        return preg_match($regexOfValidation, $value);
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        $validationMessage = 'パスワードは';
        $validationOneLetterMessage = [];
        if ($this->includeLessThanOneLowerLetter) {
            $validationOneLetterMessage[] = '半角英字(小文字)';
        }
        if ($this->includeLessThanOneUpperLetter) {
            $validationOneLetterMessage[] = '半角英字(大文字)';
        }
        if ($this->includeLessThanOneNumber) {
            $validationOneLetterMessage[] = '半角数字';
        }
        if ($validationOneLetterMessage) {
            $validationMessage .= implode('、', $validationOneLetterMessage) . 'を1文字以上含む';
        }
        if ($this->minCharacter) {
            $validationMessage .= "{$this->minCharacter}文字以上";
        }
        if ($this->maxCharacter) {
            $validationMessage .= "{$this->maxCharacter}文字以下";
        }
        $validationMessage .= 'で入力してください';

        return $validationMessage;
    }
}

【クラス定数を使う】

app\Rules\CustomPasswordValidation.php
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class PasswordValidation implements Rule
{
    //最小文字数
    private const MIN_CHARACTER = 8;
    //最大文字数
    private const MAX_CHARACTER = null;
    //1文字以上の半角英字(大文字)
    private const INCLUDE_LESS_THAN_ONE_UPPER_LETTER = true;
    //1文字以上の半角英字(小文字)
    private const INCLUDE_LESS_THAN_ONE_LOWER_LETTER = true;
    //1文字以上の半角数字
    private const INCLUDE_LESS_THAN_ONE_NUMBER = true;


    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        $regexOfValidation = "[a-zA-z\d]";
        // 半角英字(小文字)1文字以上
        if (self::INCLUDE_LESS_THAN_ONE_LOWER_LETTER) {
            $regexOfValidation = "(?=.*?[a-z])" . $regexOfValidation;
        }
        // 半角英字(大文字)1文字以上
        if (self::INCLUDE_LESS_THAN_ONE_UPPER_LETTER) {
            $regexOfValidation = "(?=.*?[A-Z])" . $regexOfValidation;
        }
        // 半角数字1文字以上
        if (self::INCLUDE_LESS_THAN_ONE_NUMBER) {
            $regexOfValidation = "(?=.*?\d)" . $regexOfValidation;
        }
        // 最大、最小文字数
        if (self::MAX_CHARACTER || self::MIN_CHARACTER) {
            $regexOfValidation = $regexOfValidation . "{" . self::MIN_CHARACTER . "," . self::MAX_CHARACTER . "}";
        }
        $regexOfValidation = "/\A{$regexOfValidation}+\z/";

        return preg_match($regexOfValidation, $value);
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        $validationMessage = 'パスワードは';
        $validationOneLetterMessage = [];
        if (self::INCLUDE_LESS_THAN_ONE_LOWER_LETTER) {
            $validationOneLetterMessage[] = '半角英字(小文字)';
        }
        if (self::INCLUDE_LESS_THAN_ONE_UPPER_LETTER) {
            $validationOneLetterMessage[] = '半角英字(大文字)';
        }
        if (self::INCLUDE_LESS_THAN_ONE_NUMBER) {
            $validationOneLetterMessage[] = '半角数字';
        }
        if ($validationOneLetterMessage) {
            $validationMessage .= implode('、', $validationOneLetterMessage) . 'を1文字以上含む';
        }
        if (self::MIN_CHARACTER) {
            $validationMessage .= self::MIN_CHARACTER . "文字以上";
        }
        if (self::MAX_CHARACTER) {
            $validationMessage .= self::MAX_CHARACTER . "文字以下";
        }
        $validationMessage .= 'で入力してください';

        return $validationMessage;
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

サブスクリプションD2CはどこまでEC SaaSに依存するべきか問題

こんにちは、ベースフード株式会社の技術責任者をやっておりまして、
2021年の1月でちょうど転職してから丸1年になります。 @sugaret と申します。
去年は(チームメイトのおかげで)ISUCON10の決勝に行ったりできました、楽しかったな。
https://sugaret.hatenablog.com/entry/2020/09/13/163547

サブスクリプションD2Cとは

ECサイトなどを自前で用意し、開発から販売までを一貫して行うサービスの形態のこと、といった理解でよいと思います。
ちなみに弊社は健康を当たり前にできる社会を作るというビジョンのもと、
「かんたん・おいしい・からだにいい」をコンセプトにした完全栄養の主食を販売しております。
先日も新味のメープル・シナモンが発売されまして、おかげさまで好評をいただいております! :smile_cat:
https://basefood.co.jp

image.png

D2Cサービスにおけるエンジニアの役割とは

D2Cの事業としてはそれほどエンジニアが活躍する場面は多くないように見えるかもしれません。
しかし、販売はECサイトであるため、
・いわゆるECサイト内のチューニング(流入、回遊、カートから決済の流れなど)
・webサイト上の様々なオンライン施策(友達招待やロイヤルティプログラムなど)
といった、お客様が商品を購入する体験に手を加えるためにはエンジニアの力が必要になります。

EC SaaSの存在

前提として、スタートアップで全て自前でECサイトを作り上げるのは車輪の再発明になる可能性が高く、かなり無駄が多いです。
商品管理、決済、注文管理、顧客管理、ロジとの連携など、ECサイトを作りたいとき、そのおおよその仕組みを提供しているSaaSがあり、全部、もしくは一部、それを利用するほうが効率が良いでしょう。

最近ではかなり自由度の高いECサイトのSaaSも生まれてきており、
自分たちでシステムを作らず、全てを依存してしまうというのも十分ありえる手です。自分たちで作る場合と比較して低コストで利用できますし、たとえば
・購入時にポイントを付与し、それが次回の割引として1ポイント1円で使える
・クレカでもAmazonPayでも決済したい
といった、どこかで見たような機能が利用できることもあります。

ただし、EC SaaS側の立場からすれば、基本的に最大公約数的にシステムを作るため、
「自分たちの商材のためのチューニング」を即時用意してもらうことは難しく、
実現しても2-3ヶ月後、あるいはそもそも対応などしてくれないということもあるのではないでしょうか。

たとえば基本無料で利用できる国内EC SaaSのBASEと、かなり自由度の高い海外EC SaaSのShopifyについて比較してみます。

base shopify
料金 基本無料
BASEロゴを外すために500円/月
その他機能に費用がかかることがあるが、大体の機能は無料の模様
月29ドル〜299ドル
https://www.shopify.jp/pricing
別途、Shopify plusという「多くのカスタマイズを可能にする」プランも存在する
支払い方法・手数料 * クレカ
* キャリア決済
* 振り込み
など
3.6% + 40円
* クレカ
* amazon pay
* キャリア決済
など

https://www.shopify.jp/blog/payments-list
国内発行のカード 3.25%〜3.4%
海外発行のカード / American Express 3.8%〜3.9%
JCB 4.05%〜4.15%

その他、ここにない手段を利用することもできる。ただしその場合は0.5%-2.0%の手数料を別途納める必要あり。
定期販売 可能
ただし、skipや日付変更、内容変更は不可の模様。キャンセルも事業者側が対応をする必要あり
https://apps.thebase.in/detail/62
最近公式で可能になった。
それまでは自前で用意する必要があった。
現状も細かくチューニングするには自前で作る必要がありそう
ポイントの付与 不可?見つからなかった shopifyアプリで可能。自前で作ることもできる。
チューニング 存在する機能を入れることで機能のチューニングができる。
HTML/CSSをいじることができる。
https://apps.thebase.in/detail/107
簡易的にshopifyアプリで対応することもできるし、
API経由で商品の情報を取得したり注文を作成したりなど、
おおよそあらゆるチューニングができる。

baseはより「かんたん」に、shopifyはより「チューニング可能」に振っているように見えます。このあたりはユースケースに合わせて選択するのがよいと思います。

重要なのは「どの部分を競合優位にするつもりなのか」

一般的に用意されている機能で十分なのであればSaaSにまかせたほうがよいでしょう。外部にコストをまかせられますし、彼らはその当たり前品質を守るプロなわけですから。
一方で、独自性を持っていろいろ試行錯誤すべきポイントについては自社でチューニングできるようにしておくことが肝要だと思っています。ここでは継続率を上げることについて考えてみます。

定期購入における継続率の重要性

たとえば定期購入の事業では、月次の解約率の1%が事業に与える影響がかなり大きいです。雑に説明すると、

例えば月次解約率90%で1年たつと、
(0.9)^12 = 0.28242953648 で、もともといたユーザーは28%ほどになります。
月次解約時91%と89%も比較してみると以下のようになり
(0.91)^12 = 0.32247548741
(0.89)^12 = 0.24699040356

定期購入ユーザーが1万人、平均購入単価が3000円だとしたらこんな感じの差分になります。
10,000 * 3,000 * (0.91)^12 ≒ 9,674,264
10,000 * 3,000 * (0.9)^12 ≒ 8,472,886
10,000 * 3,000 * (0.89)^12 ≒ 7,409,712

これは12ヶ月後だけの話ですが、当然2ヶ月目〜11ヶ月目にも差分があるため、
その累積を考えると更に差分が大きいことを感じるのではないでしょうか。
当然12ヶ月後の時点ではそのタイミングで新しく定期購入を始めるお客様もいるわけですから、
12ヶ月後の会員数が上記の人数になるというわけではないですが、
入ってきてもすぐいなくなってしまう、穴の空いたバケツ状態ということがわかりますね。

とはいえ現実的に解約率が0%になることはないので、どれだけの大きさの穴を許容するかという話になりますが、売り方や見せ方でこのあたりの数値はそこそこ動くため、まだまだ伸びしろあるね、ということで改修対象になっています。

商品とサービスの改善

このような話をしていると「解約率を低くするのは本質的に商品の改善によってなされるべきであり、その他の手法で行うのはどうなんだ」という話をされることがあります。
弊社の場合はパンやパスタなどをもっと美味しくすることですね。これはもっともなご指摘です。(日々開発・改善を行っておりますので2021年もご期待ください。)
一方でサービスとしては、「かんたんに」健康になるという意味で購入までの体験のストレスを減らすとか、購入後の食べ方といったところまでサポートをして、より習慣化して続けやすくなるように改善をしてゆこうと考えております。

弊社のケース

弊社では3つのドメインを利用してwebサービスを展開しております。
1.basefood.co.jp
ここはECサイトのトップページとしての役割を担っています。
裏側はLaravelが動いており、ニュースページでは一部WordPressを利用して、記事の管理などを行っています。

2.shop.basefood.co.jp
商品の一覧ページ、商品の個別ページ、カートページなど、回遊しつつカートに商品を入れるところまでを担っています。
Shopifyでホスティングされており、商品の登録などをshopifyのCMSを経由して行っています。

3.payse.basefood.co.jp
決済のための情報の入力、購入完了するところまでと、マイページでの各種情報の確認・変更する部分を担っています。裏側は Laravel + Nuxt.js で動いており、LaravelがAPIサーバーとして動作して、shopifyなどと連携しつつ決済・注文情報の登録などを行います。(ちなみにpayseというのはこのシステムの名前です。)
また、当初のshopifyには定期購入の機能が存在しなかったため、いつ次回の購入をするのかなどの情報もpayseで管理しており、日々バッチ処理にて定期の注文を行っています。

よいところ

shopifyの良い部分はそのまま使える

  • ロジとの連携
  • 商品管理(CMSなど)
  • 様々なshopifyアプリ など、用意されているECサイトでよく利用されるような部分はまるっとまかせています。(ちょっとサポートに時間がかかるというのはありますが)このあたりの機能について不備があれば公式で対応してもらえるので便利ですね。

自前でいろいろチューニングできる

  • 友達招待
  • ロイヤルティ機能(累計購入個数でのインセンティブ)
  • ポイント などは実は社内で開発しています。このあたりもshopifyのアプリを使えば対応できると思いますが、社内でいろいろ仮説を立てつつ、細かくトライアンドエラーを行うためには社内で作って運用するのがよいと判断しています。

つらいところ

割とメリットの部分と表裏一体になっているので一概に「どうにかせねば」というわけではないのですが。

SaaSに乗り切っていない部分のコスト問題

  • APIなど、連結部分のチューニングコスト SaaSを一部だけ利用することになると連結部分を運用し続けるコストがあります。いろいろな部分をいろいろなSaaSに任せていると、それだけのカウンターパートを相手にしながら対応することになります。
  • CSサポートコスト CSチームが問い合わせる窓口が社内エンジニアになることが多くなり、そのために工数をかけることとなります。(これは内部でタイムリーに対応できる強みとも言えますが)
  • 無段階に増加する負荷への対応 ありがたい悲鳴ではありますが、お客様が増えることによって今までは問題なかったシステムが「実行時間的に大丈夫?」みたいな話になってきます。このあたりも全て任せられていれば考えなくて良い部分ですね。

おわりに

少し前までは問題なかったけど半年後はわからないというような状況がしばしば起こり、なかなかエキサイティングな体験ができています。
これを読んで共感してくださった同業の方々、興味を持ったエンジニアの方々、ぜひお茶でもしましょう。(オンラインでもどうぞ)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelプロジェクトの構築メモ①

*使用するもの*
・Mac OS
・MAMP
・Visual Studio Code
・Laravel6
・phpMyAdmin

Laravelアプリケーションを立ち上げる手順

ターミナル操作

①MAMPのhtdocsに移動(ここでプロジェクトを作成するため)
Applications/MAMP/htdocs

②composerコマンドを使ってLaravelプロジェクトを作成する(今回は6系のものを使用する)(アプリ名はmyappとする)
composer create-project laravel/laravel myapp --prefer-dist "6.0.*"

③htdocsからmyappに移動する
cd myapp

④いくつか権限を変える
chmod -R 777 storage
chmod -R 777 bootstrap/cache

MAMPの設定

①MAMPを立ち上げる

②preferencesの中のServerへ移動し、Document rootを変更する
 Document rootを先ほど作成したアプリの中のpublicフォルダを指定する
Applications/MAMP/htdocs/myapp/public

③MAMPのサーバーを起動する

④ブラウザでlocalhost:8888にアクセスすると、Laravelプロジェクトが表示される

Laravelフォルダの.envファイルの初期設定

①Visual Studio Codeで先ほど作成したmyapp(Laravelプロジェクト)というファイルを開く

②その中の.envファイルを編集する
・アプリ名を変更

APP_NAME=Laravel

・データベース設定
MAMPのトップページの下にMySQLの情報が記載されているから、
その通りにデーターベース設定を行う

DB_CONNECTION=mysql
DB_HOST=localhost ←変更
DB_PORT=8889 ←変更
DB_DATABASE=laravel ←あとで編集する
DB_USERNAME=root
DB_PASSWORD=root ←追加
DB_SOCKET=/Applications/MAMP/tmp/mysql/mysql.sock ←追加

文字コードのエラーを防ぐ設定

MySQLのバージョンによって文字コードでエラーが発生する場合がある。

①Laravelフォルダにて、app/Providers/AppServiceProvider.phpへ移動

②use文を追加する

use Illuminate\Support\Facades\Schema;

を追加し、

public function boot()
    {
        Schema::defaultStringLength(191);
    }

細かい設定を行う

Laravelフォルダ内のconfig/app.phpを開く

'timezone' => 'Asia/Tokyo'
'locale' => 'ja'

以上がLaravelアプリケーション作成前準備です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【PHP】Laravelのデバッグ・変数の確認方法(var_export, dump, dd関数)

PHPのデバッグ方法がいくつかあるらしいので調べてみた。

PHPの場合

  1. var_export関数
  2. error_log関数

Laravelの場合

  1. dump関数
  2. dd関数


PHPの場合のデバッグ方法

1. var_export関数を使う

var_export関数で変数の中身を確認することができる。

よく使われるvar_dumpよりも有効性が高く、出力は有効なPHPコードになる。

var_export ( $expression [, bool $return = false ] )

php > $obj = array("a" => 1, "b" => 2, "c" => 3);
php > var_export($obj);
array (
  'a' => 1,
  'b' => 2,
  'c' => 3,
)

出力結果がarray('a' => 1, 'b' => 2, 'c' => 3)となっており、連想配列の定義そのものになっている。

var_dump, var_export, print_rの違いまとめ


2. error_log関数

エラーをログファイルに出力し、コンソールに表示する方法。

error_log($変数名);

ただし、使用にはコード冒頭でエラーログのオプションをONにし、ログの出力先ファイルを指定が必要。

冒頭への記述
ini_set("log_errors", "on")
ini_set("error_log", "ファイルパス&ファイル名")



また、変数が配列の場合は、以下のようにする必要がある。
error_log(print_r($変数名), true);


Laravelのデバッグ方法

Laravelの場合、dump関数やdd関数を使うと画面上に背景黒・文字色白で選択したデータを表示することができる。

dump関数とdd関数の違い

dump関数は通常の画面 + dump関数を記述した位置にその内容が表示される。つまり、そのページのコードをすべて読み込む。

一方で、dd関数はdd関数の内容のみの表示となる。


1. dump関数

.blade.php
@php
  dump($tdth);
@endphp

▼ブラウザの表示例

image.png

変数の内容以外のリソースも表示される。


2. dd関数

dd関数とは「dump and die」の略。

意味は、変数の内容を表示(dump)するが、それ以降は実行されない(die)。

.blade.php
@php
  dd($tdth);
@endphp

▼ブラウザの表示例

image.png

指定した変数の内容のみを表示する。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ポートフォリオのアプリを公開したらイタズラされまくった話

はじめに

私はこれまで開発未経験からサーバーサイドエンジニア転職を目指しており、
Laravel等の技術を使用して「朝活SNS」アプリを個人開発し、
AWSのEC2上で、一般公開していました。

アプリの紹介記事をQiitaにも投稿したところ、
AWS, Docker, CircleCI, Laravelでポートフォリオを作成してみた【参考リンク付き】
有難いことに300LGTMもいただくことができ、多くの方に
実際にポートフォリオアプリを触ってもらえたものの、結構いろんなイタズラもされしまっておりました。^^;

実装の詰めが甘かったところもあり、その脆弱性を突かれたイタズラ等もあったので、
アプリはデプロイして終わりなのではなく、運用フェーズに入ってからも気を抜いてはいけないのだと実感しましたね。。

こちらの記事では、そんなイタズラの数々や、それに対策できた部分について簡単にご紹介できればと思います。

1.下ネタ投稿をされまくる

これは予想はしていました・・笑
投稿機能のあるアプリなら、ありがちなイタズラかもしれませんね。
ちなみに次のような下ネタ投稿をされていました↓

スクリーンショット 2020-11-24 23.54.42.png

スクリーンショット 2020-11-25 23.33.27.png

スクリーンショット 2020-11-25 23.35.18.png

ザ・下ネタの帝王!ばりの単語が連呼されていますが、1日平均3回くらいは書き込まれていたので、毎日ちょいちょいパトロールを実施。。
書き込みを見つけては、トイレ掃除をするような心境でDBから投稿を削除していました。(´・_・`)笑

LaravelでNGワード機能を実装してみようと試みたものの、不具合があったため現在この辺は検討中です。

2.クロスサイトスクリプティングを狙われる(XSS)

Webアプリ等の脆弱性を狙って、悪意のあるスクリプトを実行させたりする攻撃としても有名なXSS。
今回はそれに近いような投稿も見られました。

スクリーンショット 2020-11-26 17.14.23.png

<script>alert("Test")</script>

思いっきりJavaScriptらしきメッセージが投稿されていますね。^^;
コードの内容としては、「Test」という文字列をダイアログボックスに表示するだけのものですが、
投稿したメッセージの「<」や「>」を無害化する処理を実装済みであるため
特に何も実行はされませんでした。

3.ゲストログイン機能のゲストユーザー情報を書き換えられる

今回作ったアプリには、「かんたんログイン」ボタンを押すと、予め用意されている
「ゲストユーザー」としてログインできるような機能があります。
(ユーザー名、メールアドレス等を入力しなくても簡単にログインできる)

イメージはこちらです↓

guestlogin_image.gif

この機能の使用上、ゲストユーザーのユーザー名メールアドレスを変更されると、ゲストログイン機能が動かなくなってしまいます。

そこで、ユーザープロフィール編集機能では、ゲストユーザーでログインしている時だけ
ユーザー名メールアドレスを変更できないように工夫しました。

スクリーンショット 2021-01-03 15.45.11.png

仕組みとしては、ゲストユーザーでログインしている時だけ、Viewの
< input >タグにreadonly属性が付与されたものが表示され、テキストボックスが灰色になって入力できなくなるようにしていました。

しかし、この対策だけでは結構マズくて、いとも簡単にゲストユーザーのユーザー名やメールアドレスを書き換えられてしまいました。
ある日ゲストログインしたら、ユーザー名が突然「にゃ〜ん」というものに変えられていて、めちゃめちゃ焦りました!!!^^;  笑

おそらくChrome等のデベロッパーツールを使ってHTMLを改ざんされてしまったのだと考えられます。
スクリーンショット 2021-01-03 15.56.19.png

デベロッパーツールで< input >タグのreadonly="readonly"を削除すると、ただのテキストボックスになり
ユーザー名やメールアドレスが普通に変更できるようになってしまいます↓
スクリーンショット 2021-01-03 15.59.09.png

デベロッパーツールって万能ですね・・
この時フロントエンド側だけでの対策がいかにマズイのかを学びました^^;
したがって、取り急ぎバックエンド側でも、ゲストユーザーのメールアドレス等を変更できないようにセキュリティ対策の実装をしたところ、
今回の問題は無事に解決しました。
その時の対策方法は、こちらの記事にまとめています。
 Laravel6系でゲストログイン機能(かんたんログイン)の実装

最後に

今回はざっと、公開したポートフォリオアプリへのイタズラをまとめてみましたが、
この経験を通して学びも多かったです。
セキュリティ面の実装の甘さに気付けたり、NGワード機能を追加してみようかな?と新たなアイディアが浮かんだり等々。。
今後はセキュリティ面を強化した実装等をより心がけていきたく思います!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む