bottle+MongoDBでPWAアプリに簡単な認証を実装する

アプリ開発

前回の記事で、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
view raw Dockerfile hosted with ❤ by GitHub

■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()
view raw bottlerun.py hosted with ❤ by GitHub

■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"
}
]
}
view raw manifest.json hosted with ❤ by GitHub

■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>
view raw index.html hosted with ❤ by GitHub

■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>
view raw login.html hosted with ❤ by GitHub

■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>
view raw newuser.html hosted with ❤ by GitHub

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で操作します。

一度ログインすれば、認証情報を記憶するため、キャッシュ削除やスマホ変更を行わない限り、アカウント情報を入力しなくてもよいです。

コメント

タイトルとURLをコピーしました