質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.01%

会員制サイト:メールアドレスでログイン

解決済

回答 3

投稿

  • 評価
  • クリップ 3
  • VIEW 5,472

SoraSue

score 28

前提・実現したいこと

Qiita:PHPとMySQLで新規登録とログインを実装する(PDO使用)を参考に、PHPで会員制サイトを作っています。
このサイトで作れるものでは、ID(新規登録時に自動的に発行される番号)を使ってログインするのですが、これをメアドを使ってログインできるようにしたいです。

発生している問題・エラーメッセージ

メールアドレスを入力する用のフォームとデータベース(phpMyAdmin)を作りました。新規登録でメールアドレスを登録することには成功しました。しかし、正しいメールアドレスとパスワードを入力してログインしようとしても、うまくいきません。

該当のソースコード

<?php
require 'password.php';   // password_verfy()はphp 5.5.0以降の関数のため、バージョンが古くて使えない場合に使用
// セッション開始
session_start();

$db['host'] = "localhost";  // DBサーバのURL
$db['user'] = "hogeUser";  // ユーザー名
$db['pass'] = "hogehoge";  // ユーザー名のパスワード
$db['dbname'] = "loginManagement";  // データベース名

// エラーメッセージの初期化
$errorMessage = "";

// ログインボタンが押された場合
if (isset($_POST["login"])) {
    // 1. ユーザIDの入力チェック
    if (empty($_POST["userid"])) {  // emptyは値が空のとき
        $errorMessage = 'ユーザーIDが未入力です。';
    } else if (empty($_POST["password"])) {
        $errorMessage = 'パスワードが未入力です。';
    }

    if (!empty($_POST["userid"]) && !empty($_POST["password"])) {
        // 入力したユーザIDを格納
        $userid = $_POST["userid"];

        // 2. ユーザIDとパスワードが入力されていたら認証する
        $dsn = sprintf('mysql: host=%s; dbname=%s; charset=utf8', $db['host'], $db['dbname']);

        // 3. エラー処理
        try {
            $pdo = new PDO($dsn, $db['user'], $db['pass'], array(PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION));

            $stmt = $pdo->prepare('SELECT * FROM userData WHERE id = ?');
            $stmt->execute(array($userid));

            $password = $_POST["password"];

            if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
                if (password_verify($password, $row['password'])) {
                    session_regenerate_id(true);

                    // 入力したIDのユーザー名を取得
                    $sql = "SELECT * FROM userData WHERE id = $userid";  //入力した$useridのユーザー名を取得
                    $stmt = $pdo->query($sql);
                    foreach ($stmt as $row) {
                        $row['name'];  // ユーザー名
                    }
                    $_SESSION["USERID"] = $row['name'];
                    header("Location: Main.php");  // メイン画面へ遷移
                    exit();  // 処理終了
                } else {
                    // 認証失敗
                    $errorMessage = 'ユーザーIDあるいはパスワードに誤りがあります。';
                }
            } else {
                // 4. 認証成功なら、セッションIDを新規に発行する
                // 該当データなし
                $errorMessage = 'ユーザーIDあるいはパスワードに誤りがあります。';
            }
        } catch (PDOException $e) {
            $errorMessage = 'データベースエラー';
            //$errorMessage = $sql;
            // $e->getMessage() でエラー内容を参照可能(デバック時のみ表示)
            // echo $e->getMessage();
        }
    }
}
?>

<!doctype html>
<html>
    <head>
            <meta charset="UTF-8">
            <title>ログイン</title>
    </head>
    <body>
        <h1>ログイン画面</h1>
        <!-- $_SERVER['PHP_SELF']はXSSの危険性があるので、actionは空にしておく -->
        <!-- <form id="loginForm" name="loginForm" action="<?php print($_SERVER['PHP_SELF']) ?>" method="POST"> -->
        <form id="loginForm" name="loginForm" action="" method="POST">
            <fieldset>
                <legend>ログインフォーム</legend>
                <div><font color="#ff0000"><?php echo $errorMessage ?></font></div>
    <!--今回、入力フォームはメールアドレスではなくIDにしました。-->
                <label for="userid">ユーザーID</label><input type="text" id="userid" name="userid" placeholder="ユーザーIDを入力" value="<?php if (!empty($_POST["userid"])) {echo htmlspecialchars($_POST["userid"], ENT_QUOTES);} ?>">
                <br>
                <label for="password">パスワード</label><input type="password" id="password" name="password" value="" placeholder="パスワードを入力">
                <br>
                <input type="submit" id="login" name="login" value="ログイン">
            </fieldset>
        </form>
        <br>
        <form action="SignUp.php">
            <fieldset>          
                <legend>新規登録フォーム</legend>
                <input type="submit" value="新規登録">
            </fieldset>
        </form>
    </body>
</html>
<label for="mailaddress">メールアドレス</label><input type="text" id="mailaddress" name="mailaddress" placeholder="メールアドレスを入力" value="<?php if (!empty($_POST["mailaddress"])) {echo htmlspecialchars($_POST["mailaddress"], ENT_QUOTES);} ?>">


というのを<legend>ログインフォーム</legend>の中に追加してみました。
(ただし、今は上記のように変更しています。)そして、上の<?php?>の中の"userid"を"mailaddress"に変えました。
しかし、「ユーザーIDあるいはパスワードに誤りがあります。」と出て、ログインがうまくいきませんでした。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • Kosuke_Shibuya

    2016/12/19 22:44

    参考にしているソースコードにはセキュリティ上の脆弱性が含まれています。より良質なコードを参考にされる方がよろしいかと思います。

    キャンセル

  • SoraSue

    2016/12/19 23:03

    セキュリティ上の脆弱性とは、具体的に言うと$_POSTに直接アクセスしていることでしょうか?

    キャンセル

  • Kosuke_Shibuya

    2016/12/19 23:10 編集

    SQLインジェクションですね。SQLに変数をそのまま書いてしまっています。
    あとは、CSRF脆弱性も

    キャンセル

回答 3

checkベストアンサー

+8

セキュリティとかはできるだけ質問者さんのコードのままにしておきたいのでおいておいて、このようにすれば動くのではないでしょうか。

<?php
session_start();

$db['host'] = "localhost";  // DBサーバのURL
$db['user'] = "root";  // ユーザー名
$db['pass'] = "pass";  // ユーザー名のパスワード
$db['dbname'] = "loginManagement";  // データベース名
$errorMessage = "";

// ログインボタンが押された場合
if (isset($_POST["login"])) {
    // 1. ユーザIDの入力チェック
    if (empty($_POST["mailaddress"])) {  // emptyは値が空のとき
        $errorMessage = 'ユーザーIDが未入力です。';
    } else if (empty($_POST["password"])) {
        $errorMessage = 'パスワードが未入力です。';
    }

    if (!empty($_POST["mailaddress"]) && !empty($_POST["password"])) {
        // 入力したユーザIDを格納
        $mailaddress = $_POST["mailaddress"];

        // 2. ユーザIDとパスワードが入力されていたら認証する
        $dsn = sprintf('mysql: host=%s; dbname=%s; charset=utf8', $db['host'], $db['dbname']);

        // 3. エラー処理
        try {
            $pdo = new PDO($dsn, $db['user'], $db['pass'], array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));

            $stmt = $pdo->prepare('SELECT * FROM userData WHERE mailaddress = :mailaddress');
            $stmt->execute(array(":mailaddress" => $mailaddress));

            $password = $_POST["password"];

            if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
                if (password_verify($password, $row['password'])) {
                    session_regenerate_id(true);

                    $_SESSION["USERID"] = $row['mailaddress'];
                    header("Location: Main.php");  // メイン画面へ遷移
                    exit();  // 処理終了
                } else {
                    // 認証失敗
                    $errorMessage = 'ユーザーIDあるいはパスワードに誤りがあります。';
                }
            } else {
                // 4. 認証成功なら、セッションIDを新規に発行する
                // 該当データなし
                $errorMessage = 'ユーザーIDあるいはパスワードに誤りがあります。';
            }
        } catch (PDOException $e) {
            $errorMessage = 'データベースエラー';
            //$errorMessage = $sql;
            // $e->getMessage() でエラー内容を参照可能(デバック時のみ表示)
            // echo $e->getMessage();
        }
    }
}
?>

<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>ログイン</title>
</head>
<body>
<h1>ログイン画面</h1>
<!-- $_SERVER['PHP_SELF']はXSSの危険性があるので、actionは空にしておく -->
<!-- <form id="loginForm" name="loginForm" action="<?php print($_SERVER['PHP_SELF']) ?>" method="POST"> -->
<form id="loginForm" name="loginForm" action="" method="POST">
    <fieldset>
        <legend>ログインフォーム</legend>
        <div><font color="#ff0000"><?php echo $errorMessage ?></font></div>
        <label for="mailaddress">ユーザーID</label><input type="text" id="mailaddress" name="mailaddress"
                                                      placeholder="ユーザーIDを入力"
                                                      value="<?php if (!empty($_POST["userid"])) {
                                                          echo htmlspecialchars($_POST["userid"], ENT_QUOTES);
                                                      } ?>">
        <br>
        <label for="password">パスワード</label><input type="password" id="password" name="password" value=""
                                                  placeholder="パスワードを入力">
        <br>
        <input type="submit" id="login" name="login" value="ログイン">
    </fieldset>
</form>
<br>
<form action="SignUp.php">
    <fieldset>
        <legend>新規登録フォーム</legend>
        <input type="submit" value="新規登録">
    </fieldset>
</form>
</body>
</html>


ちなみに、メールアドレスでの登録ということは、新規登録のときにfilter_var関数などでユーザーが入力した文字列がメールアドレスの形式なのかチェックする必要があると思います。質問者さんはこちらもリンクしているページの新規登録を参考にしていると思うので、メールアドレスのチェックを加えたコードを以下に書いておきます。

<?php
session_start();

$db['host'] = "localhost";  // DBサーバのURL
$db['user'] = "root";  // ユーザー名
$db['pass'] = "pass";  // ユーザー名のパスワード
$db['dbname'] = "loginManagement";  // データベース名

// エラーメッセージ、登録完了メッセージの初期化
$errorMessage = "";
$SignUpMessage = "";

// ログインボタンが押された場合
if (isset($_POST["signUp"])) {
    // 1. ユーザIDの入力チェック
    if (empty($_POST["mailaddress"])) {  // 値が空のとき
        $errorMessage = 'mailaddressが未入力です。';
    } else if (empty($_POST["password"])) {
        $errorMessage = 'パスワードが未入力です。';
    } else if (empty($_POST["password2"])) {
        $errorMessage = 'パスワードが未入力です。';
    }

    if (!empty($_POST["mailaddress"]) && !empty($_POST["password"]) && !empty($_POST["password2"]) && $_POST["password"] == $_POST["password2"] && filter_var($_POST['mailaddress'], FILTER_VALIDATE_EMAIL)) {
        // 入力したユーザIDとパスワードを格納
        $mailaddress = $_POST["mailaddress"];
        $password = $_POST["password"];

        // 2. ユーザIDとパスワードが入力されていたら認証する
        $dsn = sprintf('mysql: host=%s; dbname=%s; charset=utf8', $db['host'], $db['dbname']);

        // 3. エラー処理
        try {
            $pdo = new PDO($dsn, $db['user'], $db['pass'], array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));

            $stmt = $pdo->prepare("INSERT INTO userData(mailaddress, password) VALUES (?, ?)");

            $stmt->execute(array($mailaddress, password_hash($password, PASSWORD_DEFAULT)));  // パスワードのハッシュ化を行う(今回は文字列のみなのでbindValue(変数の内容が変わらない)を使用せず、直接excuteに渡しても問題ない)
            $userid = $pdo->lastinsertid();  // 登録した(DB側でauto_incrementした)IDを$useridに入れる

            $SignUpMessage = '登録が完了しました。あなたの登録ユーザIDは ' . $userid . ' です。パスワードは ' . $password . ' です。';  // ログイン時に使用するIDとパスワード
        } catch (PDOException $e) {
            $errorMessage = 'データベースエラー';
            // $e->getMessage() でエラー内容を参照可能(デバック時のみ表示)
            // echo $e->getMessage();
        }
    } elseif ($_POST["password"] != $_POST["password2"]) {
        $errorMessage = 'パスワードに誤りがあります。';
    } elseif (filter_var($_POST['mailaddress'], FILTER_VALIDATE_EMAIL) == false) {
        $errorMessage = 'メールアドレスの書式が間違っています。';
    }
}
?>

<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>新規登録</title>
</head>
<body>
<h1>新規登録画面</h1>
<!-- $_SERVER['PHP_SELF']はXSSの危険性があるので、actionは空にしておく -->
<!-- <form id="loginForm" name="loginForm" action="<?php print($_SERVER['PHP_SELF']) ?>" method="POST"> -->
<form id="loginForm" name="loginForm" action="" method="POST">
    <fieldset>
        <legend>新規登録フォーム</legend>
        <div><font color="#ff0000"><?php echo $errorMessage ?></font></div>
        <div><font color="#0000ff"><?php echo $SignUpMessage ?></font></div>
        <label for="mailaddress">ユーザー名</label><input type="text" id="mailaddress" name="mailaddress"
                                                     placeholder="ユーザー名を入力"
                                                     value="<?php if (!empty($_POST["mailaddress"])) {
                                                         echo htmlspecialchars($_POST["mailaddress"], ENT_QUOTES);
                                                     } ?>">
        <br>
        <label for="password">パスワード</label><input type="password" id="password" name="password" value=""
                                                  placeholder="パスワードを入力">
        <br>
        <label for="password2">パスワード(確認用)</label><input type="password" id="password2" name="password2" value=""
                                                        placeholder="再度パスワードを入力">
        <br>
        <input type="submit" id="signUp" name="signUp" value="新規登録">
    </fieldset>
</form>
<br>
<form action="Login.php">
    <input type="submit" value="戻る">
</form>
</body>
</html>

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/12/23 19:09

    期待通りのことができました!ありがとうございます!!

    キャンセル

+6

HTML近辺に記載した"userid"を変更するだけでは修正が足りていません。

HTML近辺のphpタグ内にある、
$_POST["userid"]の箇所は"mailaddress"に変更したとのことですが、
PHPのコードを眺めてみると同じような記載が存在していますよね?

それも修正範囲となりますので、
同じような要領で直す必要があります。

今回のような場合だと、
テキストエディタのgrep検索とかが駆使できるようになると、
修正漏れを抑えやすくなります。

また、HTMLのinputタグと、
PHPのコード中に登場する$_POSTや、$_GETには強い関連性があるので、
ぜひ自身で調べてみて理解するようにしていって下さいね。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/12/19 23:02

     一度、"userid"と書いてあるところを全て"mailaddress"に変えてみました。すると、「データーベースエラー」と表示されました。そこで、 $stmt = $pdo->prepare('SELECT * FROM userData WHERE id = ?');の"id"の部分だけそのままにしておくと、今度は「ユーザーIDあるいはパスワードに誤りがあります。」と表示されました。この部分でd引っかかっています。
     inputタグや$_POST、$_GETなどについては、理解できているかわかりませんが調べてはみました。「inputタグはユーザーに入力してもらう部分を作るタグで、typeを使い入力形式を決められる。」「$_POST、$_GETはユーザーが入力した情報を取得するもの。$_GETはその情報がURLに表示されるので、ログイン機能などに利用するのは危険」この程度は理解していますが、正しいでしょうか。

    キャンセル

  • 2016/12/19 23:19

    > SoraSueさん
    ざっくりとしたレベルではその認識で問題はなさそうです。

    ただそのように理解しているのでしたら、
    $_POSTで指定している項目名と拾いたい入力項目名が異なっているとまずいことは分かりますよね?

    後データベースエラーの件ですが、
    「id = ?」のidをmailaddressに変えたという認識で相違ないでしょうか?

    その場合だと今度はPHPとHTMLを離れ今度はデータベースのお話となります。
    以下を確認して下さい。

    ①userDataテーブルに検索条件に指定した列名が定義されているか(誤字脱字はないか)
    ②SELECT文をコピーし、?の箇所に任意の値を指定し、phpMyAdminなどから正常に実行できるか
    ③上記2つとも当てはまらない場合、$eの内容を画面に吐いてみてエラーメッセージ確認(var_dump($e)とか$e->getMessage()でエラー内容を取得・表示してみる)

    キャンセル

  • 2016/12/19 23:26

    上の③の補足ですが、
    エラーメッセージの確認が取ることができれば、
    それをヒントに原因の切り分けが可能です。

    なので、③の手順からいきなりやってもいいかもしれませんね。

    キャンセル

  • 2016/12/20 22:14

    わかりました。①②③を試してみます。考え方の部分まで指導して頂きありがとうございました!

    キャンセル

+4

元のソースコードをできるだけ生かした上で、修正してみました。

<?php
// デバッグのため
ini_set('display_errors', true);
error_reporting(E_ALL);

/**
 * エスケープ
 * @param type $string
 * @return type
 */
function h($string)
{
    return htmlspecialchars($string, ENT_QUOTES, 'utf-8');
}

/**
 * トークンを生成する
 */
function generate_token()
{
    $token = hash('sha256', uniqid());
    $_SESSION['csrf_token'] = $token;
}

/**
 * トークンチェック
 * @param type $token
 * @return boolean
 * @throws Exception
 */
function check_token($token)
{
    if ($token === $_SESSION['csrf_token']) {
        unset($_SESSION['csrf_token']);
        return true;
    }

    throw new Exception('CSRFチェックエラー');
}

$errorMessage = null;

try {

    // セッション開始
    session_start();

    $db['host'] = "localhost";  // DBサーバのURL
    $db['user'] = "hogeUser";  // ユーザー名
    $db['pass'] = "hogehoge";  // ユーザー名のパスワード
    $db['dbname'] = "loginManagement";  // データベース名
    // ログインボタンが押された場合
    if (filter_input(INPUT_SERVER, 'REQUEST_METHOD') === 'POST') {

        // CSRFチェック
        $token = filter_input(INPUT_POST, 'csrf_token');
        check_token($token);

        $userid = filter_input(INPUT_POST, 'userid');
        $password = filter_input(INPUT_POST, 'password');

        // 例外を使うことで、エラー時は throw で処理を中断し、catch節に飛ぶので、
        // 以降の条件分岐など、ネストが深くならない
        if (empty($userid)) {
            throw new Exception('ユーザーIDが未入力です。');
        } else if (empty($password)) {
            $errorMessage = '';
            throw new Exception('パスワードが未入力です。');
        }

        $dsn = sprintf('mysql: host=%s; dbname=%s; charset=utf8', $db['host'], $db['dbname']);
        $pdo = new PDO($dsn, $db['user'], $db['pass'], array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));

        $stmt = $pdo->prepare('SELECT * FROM userData WHERE id = ?');
        $stmt->execute(array($userid));
        $row = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($row && password_verify($password, $row['password'])) {
            session_regenerate_id(true);
            $_SESSION["USERID"] = $row['name'];
            header("Location: Main.php");
            exit();  // 処理終了
        } else {
            throw new Exception('ユーザーIDあるいはパスワードに誤りがあります。');
        }
    }
} catch (PDOException $e) {
    $errorMessage = 'データベースエラー';
} catch (Exception $e) {
    $errorMessage = $e->getMessage();
}
?>
<!doctype html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>ログイン</title>
        <style type="text/css">
            .error_message {
                color:#ff0000;
            }
        </style>
    </head>
    <body>
        <h1>ログイン画面</h1>

        <form action="" method="POST">
            <fieldset>
                <legend>ログインフォーム</legend>

                <!-- <font> は非推奨 -->
                <div>
                    <span class="error_message"><?= h($errorMessage) ?></span>
                </div>

                <label for="userid">ユーザーID</label>
                <input type="text" id="userid" name="userid" placeholder="ユーザーIDを入力" value="<?= h($userid); ?>">
                <br>

                <label for="password">パスワード</label>
                <input type="password" id="password" name="password" value="" placeholder="パスワードを入力">
                <br>

                <input type="submit" value="ログイン">

                <input type="hidden" name="csrf_token" value="<?= h(generate_token()); ?>" />
            </fieldset>
        </form>
        <br>
        <form action="SignUp.php">
            <fieldset>
                <legend>新規登録フォーム</legend>
                <input type="submit" value="新規登録">
            </fieldset>
        </form>
    </body>
</html>

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/12/20 22:01

    わざわざ修正までして頂きありがとうございました!!
    早速コピーしてローカル開発環境で動かしてみたのですが、幾つか疑問点があります。
    一つ目に、ログインのページを開くと、ユーザ−IDの入力フォームに<br /><b>Notice</b>: Undefined variable: userid in <b>phpのファイル名</b> on line <b>117</b><br />と表示されます。
    二つ目に、正しいidとパスワードを入力しても必ずCSRFチェックエラーと表示されます。

    キャンセル

  • 2016/12/20 22:30

    対象のファイルで目についた部分を修正したまで。動作確認まではやってません。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.01%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

同じタグがついた質問を見る