LoginSignup
4
2

More than 3 years have passed since last update.

Swift Vaporを使って、Githubサインイン機能とCAPTCHA検証を備えたお問い合わせフォームを構築し、Ubuntuサーバ (+Nginx) にデプロイする

Last updated at Posted at 2020-12-02

私はSwiftの経験はありますが、これは私にとってSwiftでサーバーを実行する最初の数少ないプロジェクトの1つです。

​多くの個人開発者のウェブサイトを見てみたところ、すべてにコンタクトフォームがあることが分かりました。​私のサイトにはまだコンタクトフォームがなかったので、Swift Vaporを実装したのです。

この記事では、私が Swift Vapor を使って、アプリケーション(GithubサインインとCAPTCHA検証が必要なコンタクトフォーム)を学び始め開発した経緯について説明します。 この記事全体を通して、このアプリケーションを開発するために私がとった手順について説明します。また、Vaporの学習についても説明します。

この記事が、Swift Vaporについて学び始めたり、Swift Vaporを使ったプログラミングに関するヒントを学んだりするのに役立つことを願っています。

筆者がVaporで学習し、本稿で解説するのは以下の内容です。

  • MacOSでのVaporの環境設定
  • リクエストURLに従ったhttpトラフィックのルーティング
  • (重要)非同期レスポンスの実現(「EventLoopFuture」を使用)
  • (重要)Ubuntuシステムでのサーバの設定と展開
  • HTTPSのサポート

基本的な機能

このプロジェクトの設計目標は次のとおりです。

  • スパムを防ぐため、ユーザーはGithub.comアカウントを使用してサインインする必要があります。​サーバーは、Github APIを使用して、ユーザーのGithubプロファイルからメールアドレスを取得します。
  • ユーザーはサインインしたあと、このフォームにより、確認済みのメールアドレスの1つを連絡先情報として選択できるようになります。
  • ユーザーがフォームを送信すると、Mailgunを通じてメールが送信されます。​ユーザーはCAPTCHAの検証も完了します。

このアプリケーションの基本的な流れは以下のとおりです。

  • ユーザーが Web サイトにアクセスし、ホーム ページのボタンをクリックして Github.com のサインイン ページにリダイレクトします。

  • Github がユーザーを認証し、サインイン トークンと一緒にユーザーを当社 Web サイトにリダイレクトします。
  • 当社はサインイン トークンを OAuth 承認トークンと交換し、OAuth トークンを使用してメール アドレスのリストをフェッチします。正常にフェッチしたら、そのリストをセッション ストレージに保存し、ユーザーに連絡先フォームを表示します。

  • 連絡先フォームには Captcha 認証があります。Captcha API を呼び出してユーザーがロボットでないことを確認します。
  • 送信すると、サーバーがユーザー入力情報に関するメールを当社に送信します。

Github リポジトリ

完成したコードが Github にアップロードされました。Github リポジトリを表示するにはここをクリックしてください。

APICredentials.swiftを更新して、GithubアプリケーションクライアントID/シークレット(ユーザーの認証用)、MailgunAPI(メールの送信用)、hCaptchaアプリID/シークレット(ユーザーにCAPTCHAの検証を依頼するため)を提供する必要があります。

Vapor とは

Vapor は Swift 言語を使ってウェブサーバーを構築できるようにするフレームワークです。Macにサーバーを開発し、Ubuntuシステムにサーバーをデプロイすることができます。

Vapor は動的に生成されたHTMLコンテンツをSwiftコードでレンダリングできるようにするとともに、ご自分のAPIを(入力に基づいてさまざまな応答を提供することにより)ホストできるようにします。

概して言うと、すでにモバイル開発用のSwiftをお使いであれば、サーバーおよびアプリケーション開発に対するSwift + Vaporの実用性をご理解いただける可能性があります。

ステップ1. Vapor をインストールしてスタータープロジェクトを設定

Vapor を Mac にインストールするには、これらのコマンドをターミナルで実行することができます。

brew tap vapor/tap
brew install vapor/tap/vapor

Vapor アプリケーションは次をコールして作成することができます :

vapor new Hello

このサンプルプロジェクトでは、Fluentは必須ではありませんが、Leafは必須です。

  • Fluentはデータ構造の作成に役立ちます。
  • また、Leafは、HTMLテンプレートファイルを設計し、Vaporを使ってこれらのテンプレートにコンテンツをロードするのに役立つツールです。

ガイドに従ってプロジェクトを作成したら、作成したディレクトリに移って、vapor xcodeを入力してXcodeでプロジェクトを開きます。

基本ファイル構造

本稿では以下を利用します。

  • .leafファイル(HTMLテンプレート)がすべて入ったViewsフォルダ
  • サーバの全ルーティング情報(訪問先のURL経路ごとのサーバのレスポンスを定義)が入ったroutes.swiftファイル
  • サーバをコンフィギュレーションするconfigure.swiftファイル

ステップ 2.認証を処理するため、Githubアプリケーションを作成

詳細に関してはこちらをクリックしてください

サーバーがユーザーのGithubプロフィールからメールアドレスのリストをフェッチするコンタクトフォームを作成する必要があります。それを行うには、Github.comの認証情報 (Oauth) でユーザーを認証する必要があります。

認証を処理するには、Githubアプリケーションを作成し、そのGithubアプリケーションのクライアントキーとシークレットキーを取得する必要があります。

Settings > Developer Settings、そしてGitHub Appsを選択すると、新しいアプリケーションを作成できます。

User authorization callback URLという名の構成フィールドにはhttps://[Your domain]/github_callbackと入力します。Githubはユーザーの認証を完了すると、ログインコードとともにhttps://[Your domain]/github_callbackにリダイレクトします。

記事の次のパートで、github_callback をデザインします。

また、GitHubアプリでユーザーのメールアドレスを取得できるようにしておきましょう。アプリのページに移動し、Permissions & eventsをクリックし、User permissionsセクションに移動し、Email addressesRead-Onlyに設定します。

image

ステップ 3. トップページの定義

routes.swiftファイルで、トップページを定義します。トップページは、index.leafファイルのHTMLコードを利用します。

app.get { request in
    return request.view.render("index")
}

また、ウェブサイトのトップページの外観を設定するために、index.leafをカスタマイズすることもできます。

詳細に関してはこちらをクリックしてください
<!doctype html>
<html lang="ja">

<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"
        integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
    <title>MszMagic連絡フォーム</title>
</head>

<style>
    .content {
        max-width: 500px;
        margin: auto;
    }
</style>

<body>

    <div class="content">
        <br>
        <h1>​お問い合わせフォーム</h1>
        <br>
        スパム予防のため、問い合わせフォームへとお進みいただくにはGithub.comアカウントでログインしてください。お客様の保存済みEメールアドレスへアクセスする許可のみ必要になります。当社がその他の情報を利用することはありません。
        <br><br>
        <button type="button" onclick="location.href='/start';" class="btn btn-outline-success">Githubでログイン</button>
    </div>

</body>

</html>

トップページにはボタンがあり、そのボタンをクリックすることで URL /start に遷移します。

ステップ 4. Githubログインページへのユーザーのリダイレクト (/start)

これで、ユーザーが閲覧するとGithubの認証ページにユーザーがリダイレクトされるよう/startのルーティングをコンフィギュレーションできます。リダイレクトは自分のroutes.swiftファイルで定義できます。

app.get("start") { request in
    return request.redirect(to: "https://github.com/login/oauth/authorize?client_id=\(APICredentials.github_client_id)")
}

ヒント:Response オブジェクトの構成

Response オブジェクトは複数の方法で組み立てることができます。

  • ユーザーが新しいウェブページにリダイレクトされるように .redirect を使います。

  • プレーンテキスト、ヘッダ、HTTPレスポンスステータス、HTTPのバージョンでResponseオブジェクトを生成することもできます。

static func generateResponse(text: String, status: HTTPStatus, version: HTTPVersion) -> Response {
    return .init(status: status,
                 version: version,
                 headers: ["Content-Type": "text/html; charset=utf-8"],
                 body: .init(string: text))
}

ワンポイント・アドバイス:Webサイトのホームページの作成手順3と同様に、HTMLテンプレートをレンダリングすることもできます。

ステップ 5. セッションストレージを有効化する

ユーザー情報の一部を保存する必要があることから、Vaporアプリを設定し、セッションストレージを有効化する必要があります。 configure.swiftファイルに移動し、以下の行を追加してください

app.middleware.use(app.sessions.middleware)

更新済みのコード:

public func configure(_ app: Application) throws {
    app.views.use(.leaf)
    app.middleware.use(app.sessions.middleware)
    app.sessions.configuration.cookieName = "session_id"
    try routes(app)
}

これで、セッションデータがサーバーのメモリに保存されることになります。

routes.swift ファイルでは、以下のコードを利用して、ユーザーのセッションに値を保存することができます

request.session.data["github_email_addresses"] = userEmails.joined(separator: ",")

また、以下のコードを利用すると、セッションから値を読み取ることができます。

let emailAddressesString = request.session.data["github_email_addresses"]

ステップ 6. ユーザーのメールアドレスを非同期に取得し、ユーザーのメールアドレスをVaporのセッションストレージに保存します。

Githubがユーザーを認証すると、Githubは、GitHubアプリの設定ページで指定したURLにリダイレクトします。ルーティングがそのURLに移動したときに何をするかを定義する必要があります。

GithubがログインコールバックのURLを呼び出すと、URLにはcodeパラメータも用意されています。このcodeパラメータの値を取得し、それを使ってアカウント認証トークンと交換する必要があります。

以下は、HTTP URLクエリパラメータを取得するコードです。

let authCode = try? request.query.get(String.self, at: "code")

トークンの交換はネットワークアクティビティであり、終了までにかなり時間がかかる場合があります。従って、応答する前にEventLoopFutureを使い、認証トークンがダウンロードされるまで待つようVaporサーバーに求めます。

非同期的にコードが実行されていることを確認するために私が行った手順は次の通りです。

  1. 返すデータ型EventLoopFuture<Response>を作成します
  2. let promise = request.eventLoop.makePromise(of: Response.self) を使って Promise オブジェクトを定義します
  3. 関数の最後で return promise.futureResult を実行します。このコードを呼び出すと、関数は結果の準備が整っている状態になるまで待機します。
  4. データを取得できなかった時は、エラーメッセージとともに promise.fail を呼び出すことができます。
  5. データの準備が整ったら、取得している結果データと一緒に promise.succeed を呼び出します。

ログインコードから認証トークンを取得し、その認証コードからユーザーの電子メールアドレスを取得する完成したコードは次の通りです。

app.get("github_callback") { request -> EventLoopFuture<Response> in // 1
    let promise = request.eventLoop.makePromise(of: Response.self) // 2
    if let authCode = try? request.query.get(String.self, at: "code") {
        RequestController.shared.fetchToken(loginCode: authCode) { result in
            if let fetchedToken = result.code {
                RequestController.shared.fetchEmailAddress(authToken: fetchedToken) { userEmails in
                    if userEmails.count > 0 {
                        request.session.data["github_email_addresses"] = userEmails.joined(separator: ",")
                        promise.succeed(request.redirect(to: "/form")) // 5
                    } else {
                        promise.succeed(.failedObtainEmailFromGithub(version: request.version)) // 5
                    }
                }
            } else {
                let errorMessage = result.error ?? "不明なエラー"
                promise.succeed(.generateResponse(text: errorMessage, status: .unauthorized, version: request.version)) // 5
            }
        }
    } else {
        promise.succeed(.failedObtainGithubAuthCode(version: request.version)) // 5
    }
    return promise.futureResult // 3
}

RequestController.shared.fetchTokenfetchEmailAddressのコードはここには含まれていません。これらの2つの機能はNSURLSessionを使用してGitHub APIから情報をフェッチし、完了コールバックを通して結果を返します。この記事のGithubレポジトリでこれら2つの機能のコードを見つけることができます。

コードを見ると分かるように、メールアドレスがフェッチされると、コードはメールアドレスが少なくとも1つ存在するかどうかを確認します。確認をパスすると、ユーザーのメールアドレスをセッションストレージに保存してから、ユーザーを/form URLにリダイレクトします。

LinuxでのNSURLSessionの使用について

NSURLSessionFoundationフレームワークにあります。しかし、Linuxでは、このフレームワークはFoundationNetworkingと呼ばれています。​そのため、次のようにしてフレームワークをインポートする必要があります。

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

ステップ 7. 連絡先フォームを設計する(ダイナミックHTML)

ここでは、Leafを利用して、フォームを設計します。

まず、form.leafファイルを作成し、そこにHTMLコードを書き込みます。詳細については、リポジトリを参照してください。HTMLコードには#(FormOptions)のプレースホルダーが含まれているので、すべてのユーザーのGithubEメールアドレスをピッカーに表示することができます。

次は、ルーティング・ルールの作成についてです。

app.get("form") { request -> EventLoopFuture<View> in
    if let emailAddressesString = request.session.data["github_email_addresses"] {
        var formOptionString = ""
        let emailAddresses = emailAddressesString.split(separator: ",")
        for emailAddress in emailAddresses {
            formOptionString.append("<option>\(emailAddress)</option>")
        }
        return request.view.render("form", ["FormOptions": formOptionString])
    } else {
        return request.view.render("message", ["title": "無許可", "content": "セッションには有効なメールアドレスのリストが含まれていません。"])
    }
}

form.leaf の設計においては、フォームデータは/submitのようなエンドポイントに送信しなければなりません。ここをクリックして、私が form.leaf ファイル用に書いたコードをご覧ください。

ヒント:ウェブページに表示されるコンテンツを動的に更新

これで、Vaporコードでウェブページのコンテンツを更新できるようになっています。

.leafファイルには、プレースホルダー(例:#(title))を記入してください

message.leafファイルの例がこちらです。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">

  <title>#(title)</title>
</head>

<body>
  <h1>#(title)</h1>
  #(content)
  <a href="/">TOP</a>
</body>
</html>

そして、routes.swiftファイルの中に、プレースホルダー用の値を定義できます。

return request.view.render("message", ["title": "無許可", "content": "セッションには有効なメールアドレスのリストが含まれていません。"])

ステップ 8. Mailgunを統合してメール送信

これで、Mailgunのようなサービスを使って、メールが送信できるようになります。

MailgunのAPIにメッセージを送信するためのコードは、こちらをクリックして閲覧してください。

ルーティング用にはEventLoopFutureも用います。メール送信APIからの応答を待つためです。本記事のこれまでのセクションで説明したのとよく似ています。そのコードは、こちらをクリックして閲覧してください。

ステップ 9. アプリケーションをHerokuまたはUbuntuに展開します

Ubuntu 20.04 を実行した状態で小規模な AWS EC2 インスタンスを開始することを選択しました

Swiftをインストールします

最新のSwiftバージョンのダウンロード用のリンクはこちらから取得できます : https://swift.org/download/#releases

sudo apt-get install clang

wget https://swift.org/builds/swift-5.3.1-release/ubuntu2004/swift-5.3.1-RELEASE/swift-5.3.1-RELEASE-ubuntu20.04.tar.gz

tar xzf swift-5.3.1-RELEASE-ubuntu20.04.tar.gz

rm swift-5.3.1-RELEASE-ubuntu20.04.tar.gz

sudo mv swift-5.3.1-RELEASE-ubuntu20.04 /usr/share/swift

echo "export PATH=/usr/share/swift/usr/bin:$PATH" >> ~/.bashrc

source  ~/.bashrc

それからコマンドを実行することでテストをインストールできます。

swift --version

Vapor toolbox をインストールします


apt-get install -y zlib1g-dev

git clone https://github.com/vapor/toolbox vapor-toolbox

cd vapor-toolbox

cd Tests

touch LinuxMain.swift

cd ../

swift build -c release --disable-sandbox

mv .build/release/vapor /usr/local/bin

サーバー ファイルを複製します。

Mac 上のファイルをリモート Git サーバーにプッシュし、これらのコードを Linux サーバーのディレクトリにプルできます。この例では、コードを /home/ubuntu/feedbackform に複製できます。

以下のコマンドを実行して、レポジトリを複製し、サーバーを構築します。

cd /home/ubuntu
git clone ...
cd feedbackform
vapor build

また、supervisorアプリケーションがサーバーを素早く構築して起動できるように、run.shファイルを作成することもできます。

nano run.sh

#!/bin/bash
swift build --configuration release
.build/release/Run serve --env production --port 8080 --hostname 0.0.0.0

加えて、Testsディレクトリーに空のLinuxMain.swiftファイルを作成する必要もあります。

cd Tests
touch LinuxMain.swift

Vaporサーバーの起動

./run.shを実行することで、Vaporサーバーを起動できます。

また、supervisorを使ってサーバーが自動的に実行されるように設定することもできます。

apt install supervisor

nano /etc/supervisor/conf.d/vapor_server.conf

vapor_server.confファイルに以下の内容を入力してください

[program:app_collection]
command=/home/ubuntu/feedbackform/run.sh
directory=/home/ubuntu/feedbackform
autorestart=true
user=ubuntu
supervisorctl reread
supervisorctl update

これで、ポート8080上でVaporサーバーが起動しているはずです。次に、そのポートをポート80またはポート443にプロキシ接続するために、プロキシを使う必要があります。

また、supervisorを使うことで、Linuxシステムが起動する度にVaporサーバーを立ち上げさせることができます。

Nginxプロキシサーバーの設定

まず、Nginxをインストールする必要があります

sudo apt update
sudo apt install nginx

その後、Nginxのデフォルト設定ファイルを編集できます。

cd /etc/nginx/sites-enabled/
nano default

defaultファイルの内容がこちらです。

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    location / {
        proxy_pass http://localhost:8080;
    }
}

これで、Nginxサーバーを再読み込みする準備が整いました。

sudo service nginx restart

ステップ 9. HTTPSの設定

これで、LetsEncryptを使ってサーバーのHTTPSを設定する準備が整いました。Nginxサーバーをプロキシとして設定したので、DNSを用いてドメインの所有権を検証して、手動でNginxの設定ファイルを編集する方が楽かもしれません。

sudo apt-get update
sudo apt-get install python3-certbot-nginx
sudo certbot -d [Your domain] --manual --preferred-challenges dns certonly

その後、/etc/nginx/sites-enabled/default ファイルを編集することができます :

ssl_certificate /etc/letsencrypt/live/[Domain name]/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/[Domain name]/privkey.pem;

更新されたNginx構成はこのように表示されます

server {
    listen 443 ssl;

    ssl_certificate /etc/letsencrypt/live/[Domain name]/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/[Domain name]/privkey.pem;
    ssl_protocols TLSv1.2;

    location / {
        proxy_pass http://localhost:8080;
    }
}

これで、Nginxサーバーを再読み込みする準備が整いました。

sudo service nginx restart

この記事の中で、お気づきになった改善点がございましたら、訂正していただけませんでしょうか。この記事は、私のSwift Vaporに関する学習の進捗状況を記録することだけを目的としたものです。

Qiitaには、Swift Vaporについて説明するすばらしい記事がたくさんあります。​これらの記事は、より高度なトピックを理解するのに非常に役立ちますので必ず読んでください。

​この記事で説明した内容の一部は、他の記事ですでに取り上げられている場合もあります。​しかし、 Swift Vaporアプリケーションの開発とデプロイの方法を完全に理解できるために、これらのコンテンツを含めることにしました。


:relaxed: Twitter @MszPro

:sunny: 私の公開されているQiita記事のリストをカテゴリー別にご覧いただけます。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2