前回の記事で、Docker Desktop for Windows上にMongoDBを構築しました。
これにより、他のDockerコンテナからMongoDBを利用できるようになりました。
今回は、自作PWAアプリをインターネット上に公開して自分だけが利用したい場合に利用できる簡単な認証を実装します。
用途は実装者の想像力次第ですが、特にIoT的な何かを作りたいときにおすすめです。
僕の場合は、エアコンをリモート制御したり、ライフログ管理アプリや、家族向けの音声再生メッセンジャーを作成した際に利用してます。
Dockerコンテナーの準備
それでは、今回もCoderを利用して構築していきます。
まず、Coderを起動し、ログインします。
前回作成した、MongoDBフォルダと同一階層に「PWALogin」フォルダを作成し、
下記構成で、フォルダとファイルを作成します。
PWALogin
| docker-compose.yml
| Dockerfile
|
—src
| bottlerun.py
|
+—static
| —json
| manifest.json
|
—views
index.html
login.html
newuser.html
vscode上では、下記のようになります。

プログラム作成
それぞれファイルに以下内容を書き込みます。
■docker-compose.yml
Dockerfileを読み込みコンテナを立ち上げるための定義です。
Webのポートは8133としてますが、ご自身の環境に合わせて、好きなポートに変更してください。
version: '3' | |
services: | |
maccounter: | |
restart: always | |
build: . | |
container_name: 'maccounter' | |
working_dir: '/root/src/' | |
command: python3 bottlerun.py | |
tty: true | |
ports: | |
- '8133:8133' | |
volumes: | |
- /E/coder/projects/MacCounter/src:/root/src | |
networks: | |
- mongodb_default | |
networks: | |
mongodb_default: | |
external: true |
■Dockerfile
コンテナの基本イメージとして「python3」を利用し、必要となるパッケージをインストールしています。
FROM python:3 | |
USER root | |
RUN apt-get update | |
RUN apt-get -y install locales && \ | |
localedef -f UTF-8 -i ja_JP ja_JP.UTF-8 | |
ENV LANG ja_JP.UTF-8 | |
ENV LANGUAGE ja_JP:ja | |
ENV LC_ALL ja_JP.UTF-8 | |
ENV TZ JST-9 | |
ENV TERM xterm | |
RUN apt-get install -y vim less | |
RUN pip install --upgrade pip | |
RUN pip install --upgrade setuptools | |
RUN pip3 install bottle | |
RUN pip3 install pymongo |
■bottlerun.py
PythonでWebサーバの処理を記載しています。フレームワークでBottleを利用しています。
Webのポートは8133としてますが、ご自身の環境に合わせて、好きなポートに変更してください。
# -*- coding: utf-8 -*- | |
from wsgiref import simple_server | |
from socketserver import ThreadingMixIn | |
from wsgiref.simple_server import WSGIServer | |
from bottle import HTTPResponse, route, run, template, request, response, view, default_app, static_file, redirect | |
from pymongo import MongoClient | |
class ThreadedWSGIServer(ThreadingMixIn, WSGIServer): | |
"""マルチスレッド化した WSGIServer""" | |
pass | |
# ログイン | |
@route('/login') | |
@view('login') | |
def login(): | |
com = request.query.com | |
if com == "": | |
pass | |
elif com == "login": | |
user = request.query.user | |
pass_hash = request.query.pass_hash | |
client = MongoClient('mongo', 27017, username='root', password='example') | |
col = client['auth']['users'] | |
auths = list(col.find({},{'user': 1, 'pass_hash': 1, '_id': 0})) | |
for auth in auths: | |
if auth["user"] == user and auth["pass_hash"] == pass_hash: | |
response.set_cookie("user_id", user, secret="secret_key") | |
return [b"OK"] | |
return [b"NG"] | |
# メイン | |
@route('/') | |
@view('index') | |
def index(): | |
# ログイン確認 | |
user_id = check_cookie(request) | |
# ニックネーム | |
nickname = getNickName(user_id) | |
print("ニックネーム: " + nickname) | |
# リクエスト処理 | |
com = request.query.com | |
if com == "": | |
pass | |
# 新規ユーザ登録用 | |
@route('/newuser') | |
@view('newuser') | |
def index(): | |
# リクエスト処理 | |
com = request.query.com | |
if com == "": | |
pass | |
# スタティックファイル | |
@route('/static/json/<filename:path>') | |
def json2(filename): | |
return static_file(filename, root="static/json") | |
# クッキーチェック | |
def check_cookie(request): | |
user_id = request.get_cookie("user_id", secret="secret_key") | |
print("login user: " + str(user_id)) | |
if not user_id: | |
redirect('/login') | |
return str(user_id) | |
# ユーザID⇒ニックネーム | |
def getNickName(user_id): | |
client = MongoClient('mongo', 27017, username='root', password='example') | |
col = client['auth']['users'] | |
auths = list(col.find({},{'user': 1, 'nickname': 1, '_id': 0})) | |
for auth in auths: | |
if auth["user"] == user_id: | |
return auth["nickname"] | |
if __name__ == '__main__': | |
server = simple_server.make_server('', 8133, default_app(), server_class=ThreadedWSGIServer) | |
server.serve_forever() |
■manifest.json
PWAの定義ファイルです。アイコンや画面を縦にするか横にするか等の設定を記載します。
manifest.jsonのアイコン画像は空にしておりますが、設定する場合はそれぞれのサイズのpng画像を用意し、パスを指定します。
base64での指定も可能です。
{ | |
"name" : "PWAログイン", | |
"short_name" : "PWAログイン", | |
"description" : "PWAログイン", | |
"start_url" : "/", | |
"display" : "standalone", | |
"orientation" : "portrait", | |
"background_color" : "#ffffff", | |
"theme_color" : "#ffffff", | |
"icons": [ | |
{ | |
"src" : "", | |
"sizes" : "192x192", | |
"type" : "image/png" | |
}, | |
{ | |
"src" : "", | |
"sizes" : "256x256", | |
"type" : "image/png" | |
}, | |
{ | |
"src" : "", | |
"sizes" : "512x512", | |
"type" : "image/png" | |
} | |
] | |
} |
■index.html
認証後のメイン画面です。認証の説明のためサンプルのhtmlを用意しました。
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="utf-8" /> | |
<meta name="viewport" | |
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=10.0, user-scalable=yes"> | |
<title>ログイン成功</title> | |
<link rel="manifest" href="static/json/manifest.json"> | |
</head> | |
<body> | |
ログイン成功 | |
</body> | |
</html> |
■login.html
ログイン画面のHTMLです。
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="utf-8" /> | |
<meta name="viewport" | |
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=10.0, user-scalable=yes"> | |
<title>ログイン</title> | |
<style type="text/css"> | |
<!-- | |
.cp_iptxt { | |
position: relative; | |
width: 80%; | |
margin: 5px 3%; | |
} | |
.cp_iptxt input { | |
font: 15px/24px sans-serif; | |
box-sizing: border-box; | |
width: 100%; | |
margin: 8px 0; | |
padding: 0.3em; | |
transition: 0.3s; | |
border: 1px solid #bbbbbb; | |
border-radius: 4px; | |
outline: none; | |
} | |
.btn { | |
position: relative; | |
width: 100%; | |
text-align: center; | |
} | |
.btn-square-little-rich { | |
position: relative; | |
display: inline-block; | |
padding: 0.25em 0.5em; | |
text-decoration: none; | |
color: #FFF; | |
background: #03A9F4; | |
/*色*/ | |
border: solid 1px #0f9ada; | |
/*線色*/ | |
border-radius: 4px; | |
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2); | |
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); | |
} | |
.btn-square-little-rich:active { | |
/*押したとき*/ | |
border: solid 1px #03A9F4; | |
box-shadow: none; | |
text-shadow: none; | |
} | |
--> | |
</style> | |
</head> | |
<body> | |
<div class="cp_iptxt"> | |
<input type="text" id="user" placeholder="ユーザ名"> | |
</div> | |
<div class="cp_iptxt"> | |
<input type="password" id="pass" placeholder="パスワード"> | |
</div> | |
<div class="btn"> | |
<a href="#" id="login" class="btn-square-little-rich">ログイン</a> | |
</div> | |
<div id="layer" style="display:none;"></div> | |
<script src="https://code.jquery.com/jquery-3.3.1.min.js" | |
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script> | |
<script src="https://cdn.jsdelivr.net/npm/jssha@3.2.0/dist/sha.min.js"></script> | |
<script> | |
// LocalStorageの取り出し | |
var strauth = localStorage.getItem('auth'); | |
var auth = JSON.parse(strauth ? strauth : "[]"); | |
// 初期設定 | |
$(function () { | |
if (auth.length != 0) { | |
// 自動ログイン | |
waitLayerOn(); | |
$.get("/login?com=login&user=" + auth[0]["user"] + "&pass_hash=" + auth[0]["pass_hash"], function (data) { | |
if (data == "NG") { | |
window.location.href = "/login"; | |
} | |
else if (data == "OK") { | |
window.location.href = "/"; | |
} | |
}); | |
} | |
}); | |
function waitLayerOn() { | |
$('#layer').css({ | |
zIndex: 1000, | |
position: 'fixed', | |
top: 0, | |
left: 0, | |
display: 'block', | |
width: '100%', | |
height: '100%', | |
opacity: 0.6, | |
backgroundColor: '#ffffff' | |
}); | |
} | |
// ログインボタンクリック | |
$(document).on("click", "#login", function () { | |
// 入力値の取得 | |
var user = $("#user").val(); | |
var pass = $("#pass").val(); | |
// 入力チェック | |
if (user == "") { | |
alert("ユーザ名を入力してください。"); | |
return; | |
} | |
else if (pass == "") { | |
alert("パスワードを入力してください。"); | |
return; | |
} | |
// パスワードのハッシュ値を計算 | |
var shaObj = new jsSHA("SHA-512", "TEXT"); | |
shaObj.setHMACKey("messen", "TEXT"); | |
shaObj.update(pass); | |
var pass_hash = shaObj.getHMAC("HEX"); | |
console.log("pass_hash = " + pass_hash); | |
// ログイン | |
waitLayerOn(); | |
$.get("/login?com=login&user=" + user + "&pass_hash=" + pass_hash, function (data) { | |
if (data == "NG") { | |
window.location.href = "/login"; | |
} | |
else if (data == "OK") { | |
a = {}; | |
a["user"] = user; | |
a["pass_hash"] = pass_hash; | |
auth.push(a); | |
strauth = JSON.stringify(auth); | |
localStorage.setItem('auth', strauth); | |
window.location.href = "/"; | |
} | |
}); | |
}); | |
</script> | |
</body> | |
</html> |
■newuser.html
ユーザ作成画面のHTMLです。
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="utf-8" /> | |
<meta name="viewport" | |
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=10.0, user-scalable=yes"> | |
<title>ログインユーザ作成用</title> | |
<style type="text/css"> | |
<!-- | |
.cp_iptxt { | |
position: relative; | |
width: 80%; | |
margin: 5px 3%; | |
} | |
.cp_iptxt input { | |
font: 15px/24px sans-serif; | |
box-sizing: border-box; | |
width: 100%; | |
margin: 8px 0; | |
padding: 0.3em; | |
transition: 0.3s; | |
border: 1px solid #bbbbbb; | |
border-radius: 4px; | |
outline: none; | |
} | |
.btn { | |
position: relative; | |
width: 100%; | |
text-align: center; | |
} | |
.btn-square-little-rich { | |
position: relative; | |
display: inline-block; | |
padding: 0.25em 0.5em; | |
text-decoration: none; | |
color: #FFF; | |
background: #03A9F4; | |
/*色*/ | |
border: solid 1px #0f9ada; | |
/*線色*/ | |
border-radius: 4px; | |
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2); | |
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); | |
} | |
.btn-square-little-rich:active { | |
/*押したとき*/ | |
border: solid 1px #03A9F4; | |
box-shadow: none; | |
text-shadow: none; | |
} | |
.authinfo { | |
margin: 40px; | |
} | |
--> | |
</style> | |
</head> | |
<body> | |
<div class="cp_iptxt"> | |
<input type="text" id="user" placeholder="ユーザ名"> | |
</div> | |
<div class="cp_iptxt"> | |
<input type="password" id="pass" placeholder="パスワード"> | |
</div> | |
<div class="cp_iptxt"> | |
<input type="text" id="nickname" placeholder="ニックネーム"> | |
</div> | |
<div class="btn"> | |
<a href="#" id="make" class="btn-square-little-rich">作成</a> | |
</div> | |
<div id="authinfo" class="authinfo"></div> | |
<script src="https://code.jquery.com/jquery-3.3.1.min.js" | |
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script> | |
<script src="https://cdn.jsdelivr.net/npm/jssha@3.2.0/dist/sha.min.js"></script> | |
<script> | |
// 作成ボタンクリック | |
$(document).on("click", "#make", function () { | |
// 入力値の取得 | |
var user = $("#user").val(); | |
var pass = $("#pass").val(); | |
var nickname = $("#nickname").val(); | |
// 入力チェック | |
if (user == "") { | |
alert("ユーザ名を入力してください。"); | |
return; | |
} | |
else if (pass == "") { | |
alert("パスワードを入力してください。"); | |
return; | |
} | |
else if (nickname == "") { | |
alert("ニックネームを入力してください。"); | |
return; | |
} | |
// パスワードのハッシュ値を計算 | |
var shaObj = new jsSHA("SHA-512", "TEXT"); | |
shaObj.setHMACKey("messen", "TEXT"); | |
shaObj.update(pass); | |
var pass_hash = shaObj.getHMAC("HEX"); | |
console.log("pass_hash = " + pass_hash); | |
// 作成情報の表示 | |
auth_str = ',"user": "' + user + '","pass_hash": "' + pass_hash + '","nickname": "' + nickname + '"' | |
$("#authinfo").text(auth_str); | |
}); | |
</script> | |
</body> | |
</html> |
Dockerコンテナーの起動
Coderのターミナルで、下記コマンドを実行し、コンテナを起動すればサーバ構築完了です。
cd MongoDB
sudo docker-compose build
sudo docker-compose up -d
新規ユーザ作成
次に、新規ユーザの作り方を説明します。
新規ユーザ作成もWeb画面でできれば良いですが、今回は個人利用の想定のため、そのような機能は用意しておりません。
少し手間がかかりますが、一度作成してしまえば後はユーザを利用するだけで良いので、良しとします。
下記URLに接続します。
http://[サーバIP]:8133/newuser
すると、下記画面が表示されるので、新規ユーザのユーザ名、パスワード、ニックネームを入力し、作成ボタンをクリックします。

作成すると、下記のような画面で認証設定文字列が表示されます。

表示された文字列はコピーしておきます。
この認証設定文字列をMongoDBに登録します。
下記URLに接続します。前回構築したMongoDBのMongo Expressです。
http://[サーバIP]:8081/
Mongo Expressの画面が表示されたら「+Create Database」ボタンをクリックし、「auth」という名前のデータベースを作成します。
作成した「auth」をクリックします。

さらに、「+Create collection」ボタンをクリックし、「users」という名前のコレクションを作成します。
作成した「users」をクリックします。

次に「New Document」をクリックし、下記画面が表示されることを確認します。
「ObjectID()」の右側にカーソルを合わせて、先ほどコピーした認証設定文字列をペースト、「Save」ボタンをクリックします。


駆け足になりましたが、上記操作により、新規ユーザでのログインが可能になりました。
PWAインストール、ログイン実行
あとは、スマートフォンのブラウザで「http://[サーバIP]:8133/」に接続し、一度ログインし、ホームに追加でインストールすれば完了です。
iPhoneならSafari、AndroidならChromeで操作します。
一度ログインすれば、認証情報を記憶するため、キャッシュ削除やスマホ変更を行わない限り、アカウント情報を入力しなくてもよいです。
コメント