[Drogon]Login画面の作り方
こんにちは、もしくはこんばんは皆様。みじんこきなこです。
さて、前回の実験でOpenSSLを使用して暗号化ハッシュを取得する方法を紹介しましたが、今回はそれを利用したログイン画面の作り方について解説していきます。
ちなみに今回のプロジェクトはちょっと規模が大きくなります。写経のように移すのは大変だと思いますので、適宜コピペして使ってみてくださいね。
それでは参りましょう。
データベースを用意する
今回はログイン情報の記録にデータベースを使用します。使用するデータベースのテーブルはこんな感じ。
ユーザIDと、パスワードから得られる暗号化ハッシュを記録するカラムを用意します。
プロジェクトを作る
プロジェクト
名称:login_sampleコントローラ
種類:HttpController
名称:loginController
config.jsonの編集
DB接続を行うために、config.jsonを編集します。編集の仕方は以前の記事をご参照頂ければいいかと思いますので、そちらをご覧ください。
main.ccの編集
DrogonフレームワークにはSessionというsessionIDなどの情報を付与したCookieで、ログイン情報などを記録する仕組みがあります。
Cookieそのものは自身でカスタムして作ることもできますが、sessionID等のよく使う仕組みを使用する場合は、drogonのSessionオブジェクトを使用してしまう方が簡単です。
Sessionの使用には、main.ccの中で drogon::app() の enableSession() メソッドを呼び出してやります。
enableSession()の引数は、sessionがタイムアウトするまでの時間と、same siteの設定が最低限出来ます。
もしスクリプトから利用できないようhttponlyなどを設定したい場合は、Cookieを独自実装する必要があります。
main.cc
#include <drogon/drogon.h>
#include <chrono>
using namespace std::chrono_literals;
int main() {
//Set HTTP listener address and port
drogon::app().addListener("0.0.0.0",8848);
//Load config file
drogon::app().loadConfigFile("../config.json");
//Run HTTP framework,the method will block in the internal event loop
drogon::app()
.enableSession(1h)
.run();
return 0;
}
忘れずにloadConfigFile()のコメントアウトも外しておきます。
CSPファイルを追加する
今回用意するCSPファイルは3種類です。全体の流れとしては、 ユーザを登録する ログインするという流れになりますので、ユーザ登録画面と、ログイン画面、そして通過したときに表示される画面の3画面を用意しています。
userAdd.csp
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="Content-Style-Type" content="text/css">
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta name="description" content="DrogonのCSS利用サンプル">
<meta name="keywords" content="Drogon,CSS,MVCフレームワーク,C++,高速">
<meta name="Mizinko-Kinako" content="MKnote">
<meta name="copyright" content="Mizinko-Kinako">
<meta charset="UTF-8">
<title>サンプルLogin</title>
</head>
<body>
<form action="/loginControll/submit" method="post">
<p>
<label>ログインID:</label>
<input type="text" name="loginID" placeholder="IDを入れてください" />
</p>
<p>
<label>パスワード:</label>
<input type="password" name="passWord" placeholder="パスワードを入れてください" />
</p>
<p>
<label>パスワード:</label>
<input type="password" name="validation" placeholder="同じパスワードを入れてください" />
</p>
<p>
<button type="submit">登録</button>
</p>
</form>
</body>
</html>
login.csp
<%c++ auto message = @@.get<std::string>("message"); %>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="Content-Style-Type" content="text/css">
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta name="description" content="DrogonのCSS利用サンプル">
<meta name="keywords" content="Drogon,CSS,MVCフレームワーク,C++,高速">
<meta name="Mizinko-Kinako" content="MKnote">
<meta name="copyright" content="Mizinko-Kinako">
<meta charset="UTF-8">
<title>サンプルLogin</title>
</head>
<body>
<form action="/loginControll/login" method="post">
<p>
<label>ログインID:</label>
<input type="text" name="loginID" placeholder="IDを入れてください" />
</p>
<p>
<label>パスワード:</label>
<input type="password" name="passWord" placeholder="パスワードを入れてください" />
</p>
<p>
<label>ログイン状態を記録する</label>
<input type="checkbox" name="preserve" />
</p>
<p>
<button type="submit">ログイン</button>
</p>
<p>
<label>{% message %}</label>
</p>
</form>
</body>
</html>
passed.csp
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="Content-Style-Type" content="text/css">
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta name="description" content="DrogonのCSS利用サンプル">
<meta name="keywords" content="Drogon,CSS,MVCフレームワーク,C++,高速">
<meta name="Mizinko-Kinako" content="MKnote">
<meta name="copyright" content="Mizinko-Kinako">
<meta charset="UTF-8">
<title>サンプルLogin</title>
</head>
<body>
ログインは、承認されました!
</body>
</html>
暗号化ハッシュを取ってくる関数を追加する
次にControllersディレクトリの下に、任意のデータの暗号化ハッシュを取得する関数を追加します。
今回は分かりやすさのためにString型でダイジェストを取ってくれる形で実装しています。
ソースコードの中身は、前回の記事の実験の内容に準じていますので、詳しく知りたい方はそちらの記事を適宜参照してください。
loginUtility.h
#pragma once
#include <iostream>
#include <memory>
#include <iomanip>
#include <openssl/sha.h>
template < class T >
std::string createDigestArray(
const T* data_,
const size_t data_size_,
std::array<u_char, SHA512_DIGEST_LENGTH>& hash_
)
{
std::stringstream ss_digest;
std::unique_ptr< SHA512_CTX > context
= std::make_unique<SHA512_CTX>();
SHA512_Init(context.get());
SHA512_Update(
context.get(),
static_cast<const void *>(data_),
data_size_
);
SHA512_Final(hash_.data(), context.get());
ss_digest << std::setfill('0') << std::hex;
for(auto it : hash_) {
ss_digest << std::setw(2) << static_cast<int>(it);
}
return ss_digest.str();
}
loginUtility.cc
//dummy
loginUtility.cc に関しては今回はダミーファイルとしています。
どうせストリング型でデータを与えるので、このファイルの中でテンプレートを部分特殊化してももちろんいいのですが、サンプルなので少しでもいじるファイルの数を減らす意図です。
Controllerを編集する
そして最後にControllersディレクトリの下に作った、loginControllクラスを編集していきます。
ポイントは後で解説することにして、まずはファイルの全体像。
loginControll.h
#pragma once
#include <drogon/HttpController.h>
using namespace drogon;
class loginControll : public drogon::HttpController<loginControll>
{
public:
METHOD_LIST_BEGIN
METHOD_ADD(loginControll::newuser, "/newuser", Get);
METHOD_ADD(loginControll::loginform, "/loginform", Get);
METHOD_ADD(loginControll::submit, "/submit", Post);
METHOD_ADD(loginControll::login, "/login", Post);
METHOD_LIST_END
void newuser(
const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback
) const;
void submit(
const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback
) const;
void loginform(
const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback
) const;
void login(
const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback
) const;
std::string getStoredDigest(
std::string userID
) const;
};
loginControll.cc
#include "loginControll.h"
#include "loginUtility.h"
void loginControll::newuser(
const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback
) const
{
auto response = drogon::HttpResponse::newHttpViewResponse("userAdd.csp");
callback(response);
}
void loginControll::submit(
const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback
) const
{
std::string id = req->getParameter("loginID");
std::string pass = req->getParameter("passWord");
std::string validation = req->getParameter("validation");
drogon::HttpResponsePtr response = drogon::HttpResponse::newHttpViewResponse("userAdd.csp");
if(pass == validation)
{
std::array<u_char, SHA512_DIGEST_LENGTH> passHash = {0};
// パスワードの暗号化ハッシュを作成します。
std::string digest =
createDigestArray<char>(
pass.c_str(),
static_cast<size_t>( pass.length() ),
passHash
);
drogon::orm::DbClientPtr dbp =
drogon::app().getDbClient();
// ファイルにせよ、DataBaseにせよ、保存するときは平文ではなく暗号化ハッシュを保存します。
// こうすることで、保存先が露呈して盗み見られた際にも、
// パスワードそのものが流出することを防ぐことができます。
auto result = dbp->execSqlSync(
"INSERT INTO passwords VALUES ($1,$2);",
id,
digest
);
if( result.affectedRows() != 0)
{
auto viewData = drogon::HttpViewData();
viewData.insert("message", std::string(""));
response =
drogon::HttpResponse::newHttpViewResponse("login.csp", viewData);
}
}
callback( response );
}
void loginControll::loginform(
const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback
) const
{
bool loginState = false;
auto viewData = drogon::HttpViewData();
viewData.insert("message", std::string(""));
drogon::HttpResponsePtr response =
drogon::HttpResponse::newHttpViewResponse("login.csp", viewData);
drogon::SessionPtr sessionHolder = req->getSession();
if(sessionHolder->find("loginState"))
{
loginState = sessionHolder->getOptional<bool>("loginState").value_or(false);
}
if (loginState)
{
response = HttpResponse::newHttpViewResponse("passed.csp");
}
callback(response);
}
std::string loginControll::getStoredDigest(
std::string userID
) const
{
std::string resultString;
drogon::orm::DbClientPtr dbp = drogon::app().getDbClient();
auto result = dbp->execSqlSync(
"SELECT digest FROM public.passwords WHERE \"userID\" = $1",
userID
);
if(result.size() > 0)
{
resultString = result[0]["digest"].as<std::string>();
}
return resultString;
}
void loginControll::login(
const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback
) const
{
auto viewData = drogon::HttpViewData();
viewData.insert("message", std::string("ログイン情報が不正です。"));
drogon::HttpResponsePtr response = HttpResponse::newHttpViewResponse("login.csp", viewData);
std::string preserve = req->getParameter("preserve");
std::string id = req->getParameter("loginID");
std::string pass = req->getParameter("passWord");
std::array<u_char, 64UL> inputHash;
std::array<u_char, 64UL> storedHash;
std::string passDigest =
createDigestArray<char>( pass.c_str(), static_cast<size_t>( pass.length() ), inputHash );
std::string storedDigest =
getStoredDigest(id);
if( passDigest == storedDigest )
{
if (preserve != "")
{
req->session()->insert("loginState", true);
}
else
{
req->session()->erase("loginState");
}
response = HttpResponse::newHttpViewResponse("passed.csp");
}
callback(response);
}
ポイント:GetとPostは別定義
HttpSimpleControllersではなく、HttpControllerを使用していますので、一つのメソッドには一つのリクエストしか紐づけられない形になります。
ですから今回は画面を表示するためだけのGetを処理するメソッドと、表示された画面でボタンが押されたときにフォームからの情報を受け取るためのPostを処理するためのメソッドを用意しています。
newuserメソッドはsubmitメソッドと、loginformメソッドはloginメソッドとそれぞれ対になります。
ポイント:パスワードの平文保存はNG
ここが重要なポイントとなりますが、サービスを利用するユーザのパスワードなどの情報は、原則的に平文で保存してはいけません。
例えば、
サーバを直接いじれる方が悪意を持っていたとき
不正アクセスでサーバの中が覗かれたとき
DBへの不正アクセスがあったとき
そんなときに平文でユーザIDとパスワードがセットで保存されていたりすると、もはやそのサービスにはどのユーザでも入り放題になってしまいます。
しかし今回のようにパスワードを保存する際暗号化して保存しておけば、仮に不正にその情報を見られたとしても元のパスワードは知りようがありません。
また、今回はサンプルなのでそこまでしていませんが、実際にはDBとのやり取りもSSHを通すべきですし、ViewからPostされてくるデータもHTTPS以外では受け取らない、平文の値を一次的にでも受ける変数は、必ず生存期間をスコープ内に区切り、メモリを破壊して読み取るような攻撃にも備える、といった対応を必ず取るべきです。
ポイント:sessionへのログイン状態の保持
DrogonのsessionはCookieを利用した仕組みなので、簡単なオプションであれば保持させることができます。
今回はdrogonのexampleを参考に、bool値でログイン状態を保持する形の実装としています。
書くところ
if (preserve != "")
{
req->session()->insert("loginState", true);
}
else
{
req->session()->erase("loginState");
}
読むところ
if(sessionHolder->find("loginState"))
{
loginState = sessionHolder->getOptional<bool>("loginState").value_or(false);
}
value_orの中でその値が存在するかどうかは見ていますが、基本的には今回のようにfind()メソッドを使用して、そのキーが存在するか確認を行ってから操作する方が安全です。
ビルドをしてみる
ビルド手順は従来と同じです。
$ cd build$ cmake .. && make$ ./login_sample
ログインしてみる
さてそれではログインしてみましょう。
例のごとくブラウザからアクセスします。
まずはユーザ登録画面
http://localhost:80/loginControll/newuser
ここでユーザ登録を行います。
2回入力したパスワードが正しく一致すれば、以下のようにデータベースにパスワードのダイジェストが登録されます。
今回入力したのはtestpassという文字列ですが、もはや全く元のパスワードが何なのかわかりません。
ログイン画面登録ボタンを押したら、ログイン画面に飛ばされるはずです。
ここでは敢えて間違えた情報を入力しましょう。
怒られましたね。
次にログイン状態を保存するチェックボックスをオフにしてログインしてみます。
ログインは、承認されました。この状態で、もう一度http://localhost:80/loginControll/loginformにアクセスしてみます。
ログイン画面が再び表示されました。
今度はログイン状態を保存してログインしてみましょう。
ログインは、承認されました。
さらにもう一度
http://localhost:80/loginControll/loginform
にアクセスしてみます。
ログインは、承認されてました。
このような形で、一通りのログインの動作が確認できるかと思います。
さいごに
今回はサンプルという割には、なかなか厳つい事例を解説しました。
ログイン動作のサンプルという意味では、暗号化ハッシュの利用などは必要ないようにも思われますが、実際にネットワークに公開する際にはこのようなセキュリティ保護は必須となります。
なので個人的にはサンプルを見て書いてみる段階から、このようなやり方に慣れておくのは大事なことだと考えています。
皆様の安全で楽しい開発に役立てて頂ければ幸いです。