Notes

はじめに

  • これまで、HTMLとCSSを使って簡単なWebページを構築する方法、GitとGitHubを使ってコードの変更を追跡し、他の人と共同作業する方法について説明してきました。また、Pythonプログラミング言語に慣れ、Djangoを使用してWebアプリケーションを作成し始め、Djangoモデルを使用してサイトに情報を保存する方法を学びました。その後、JavaScriptを導入してWebページをよりインタラクティブにする方法を学習し、アニメーションとReactを使用してユーザインターフェイスをさらに改善する方法について説明しました。次に、ソフトウェア開発におけるいくつかのベスト・プラクティスと、それらのベスト・プラクティスを実現するために一般的に使用されるいくつかのテクノロジーについて説明しました。
  • 今日の最後の講義では、Webアプリケーションの拡張とセキュリティの問題について説明します。

スケーラビリティ (拡張性)

これまでこのコースでは、ローカル・コンピュータでのみ実行されるアプリケーションを構築してきましたが、最終的には、インターネット上の誰もがアクセスできるようにサイトを立ち上げたいと考えています。そのためには、アプリケーションを実行するための専用ハードウェアであるサーバ上でサイトを実行します。サーバはオンプレミス (アプリケーションがホストされている物理サーバを所有し、保守している) でもクラウド (サーバはAmazonやGoogleなどの別の会社が所有しており、アプリケーションがホストされているサーバスペースを借りるためにお金を払っている) でも問題ありません。どちらのオプションにも利点と欠点があります。

  • カスタマイズ:独自のサーバをホストすることで、サーバの動作を正確に決定できるようになり、クラウドベースのホスティングよりも柔軟性が高まります。
  • 専門知識:クラウド上でアプリケーションをホストする方が、独自のサーバを管理するよりもはるかに簡単です。
  • コスト:サーバホスティングサイトは利益を上げる必要があるため、オンプレミスのサーバを維持するコストよりも高い料金を請求することになり、クラウドベースのサーバはより高価になります。ただし、物理サーバを購入して、そのセットアップの専門知識を持つ人を雇う必要があるため、オンプレミスのサーバを実行するための初期コストは高くなる可能性があります。
  • 拡張性:クラウド上でホストする場合は、一般的にスケーリングが容易です。たとえば、社内で500人の訪問者があるサイトをホストし、その後50万人の訪問者があるとすると、要求を処理するために、より多くの物理サーバを注文して設定する必要があり、その間に多くのユーザがサイトにアクセスできなくなります。ほとんどのクラウドホスティングサイトでは、サイトがどれだけのアクションを見ているかに応じて料金を支払うことで、柔軟にサーバスペースを借りることができます。

ユーザがこのサーバにHTTP要求を送信すると、サーバは応答を返します。しかし、実際には、以下に示すように、ほとんどのサーバは一度に複数の要求を受け取ります。

server many inputs

ここで拡張性の問題に直面します。1台のサーバが一度に処理できる要求の数は限られているため、1台のサーバが過負荷になった場合の対処方法を計画する必要があります。オンプレミスでホストするかクラウドでホストするかにかかわらず、サーバがクラッシュせずに処理できるリクエストの数を決定する必要があります。これは、Apache Benchを含むいくつかのベンチマークツールを使用して行うことができます。

スケーリング

サーバが処理できる要求の数に上限が設定されると、アプリケーションのスケーリングをどのように処理するかを考えることができます。スケーリングには、次の2つの方法があります。

  1. 垂直スケーリング:垂直スケーリングでは、サーバに負荷がかかったときに、より大きなサーバを購入または構築します。ただし、1台のサーバの処理能力には上限があるため、この方法には限界があります。
  2. 水平スケーリング:水平スケーリングでは、サーバに負荷がかかったときに、より多くのサーバを購入または構築し、複数のサーバ間で要求を分割します。

ロードバランシング

水平スケーリングを使用すると、どのサーバをどのリクエストに割り当てるかをどのように決定するかという追加の問題に直面します。この質問に答えるために、ロードバランサを使用します。ロードバランサは、着信要求をインターセプトし、その要求をサーバの1つに割り当てます。どのサーバがどのリクエストを受信するかを決定する方法はいくつかありますが、以下にいくつかの方法を示します。

  • ランダム:この単純な方法では、ロードバランサはリクエストを割り当てるサーバをランダムに決定します。
  • ラウンドロビン:この方法では、ロードバランサが着信リクエストを受信するサーバを代替します。3つのサーバがある場合、最初のリクエストはサーバAに送信され、2番目のリクエストはサーバBに送信され、3番目のリクエストはサーバCに送信され、4番目のリクエストはサーバAに送信されます。
  • 最少接続:この方法では、ロードバランサは現在最も少ない要求を処理しているサーバを探し、そのサーバに着信要求を割り当てます。これにより、特定の1つのサーバが過負荷になっていないことを確認できますが、各サーバが現在処理しているリクエストの数をロードバランサが計算するには、ランダム・サーバを選択するよりも時間がかかります。

他のすべての方法よりも厳密に優れたロードバランシングの方法はなく、実際には多くの異なる方法が使用されています。水平方向に拡張する場合に発生する可能性がある問題の1つは、あるサーバにはセッションが保存されているが、別のサーバには保存されていない可能性があることです。また、ロードバランサが要求を新しいサーバに送信するだけで、ユーザが情報を再入力する必要がないようにする必要があります。スケーラビリティの多くの問題と同様に、セッションの問題を解決するには複数のアプローチがあります。

  • スティッキセッション:ユーザがサイトにアクセスすると、ロードバランサは最初に送信されたサーバを記憶し、必ず同じサーバに送信します。この方法の大きな懸念の1つは、多数のユーザが1つのサーバに固執し、そのサーバがクラッシュする可能性があることです。
  • データベースセッション:すべてのセッションは、すべてのサーバがアクセスできるデータベースに格納されます。これにより、ユーザがどのサーバに割り当てられていても、ユーザの情報を利用できるようになります。ここでの欠点は、データベースの読み書きに時間と処理能力がかかることです。
  • クライアントサイドセッション:サーバに情報を保存するのではなく、ユーザのWebブラウザにクッキーとしてローカルに保存することができます。この方法の欠点には、ユーザが別のユーザとしてログインすることを可能にする偽のクッキーを作成するというセキュリティ上の懸念と、リクエスト毎にクッキー情報をやりとりするという計算上の懸念があります。

ロードバランシングと同様に、セッションの問題に対する最善の解決策はありません。選択する方法は、多くの場合、特定の状況によって異なります。

自動スケーリング

特定の時間帯に多くのWebサイトが頻繁にアクセスされるという問題もあります。例えば、私たちが「お正月ですか?」アプリを早い時期からローンチすることを決めた場合、どの時期よりも多くのトラフィックを12月下旬から1月上旬に起こると予想するでしょう。このサイトが冬の間アクティブであり続けるのに十分な数のサーバを購入すると、それらのサーバは1年の残りの期間アイドル状態になり、スペースとエネルギーを浪費します。このシナリオは、要求の数に応じてサイトで使用されるサーバの数が増減するクラウドコンピューティングで一般的になった自動スケーリングの概念をもたらしました。しかし、新しいサーバが必要かどうかを判断し、そのサーバを起動するには時間がかかるため、自動スケールは完璧なソリューションではありません。もう1つの潜在的な問題は、実行しているサーバが多いほど、障害が発生する可能性が高くなることです。

サーバ障害

ただし、複数のサーバがあると、単一障害点 (Single Point of Failure) と呼ばれる状態を回避するのに役立ちます。単一障害点とは、障害が発生した後にサイト全体がクラッシュするハードウェアのことです。水平方向に拡張する場合、ロードバランサは、各サーバに定期的にハートビート要求を送信することによって、どのサーバがクラッシュしたかを検出し、クラッシュしたサーバへの新しい要求の割り当てを停止できます。この時点では、単一障害点をサーバからロードバランサに移動しただけのようですが、元のロードバランサがクラッシュした場合にバックアップ・ロードバランサを使用できるようにすることで、この問題を解決できます。

データベースのスケーリング

リクエストを処理するサーバを拡張するだけでなく、データベースを拡張する方法も検討したいと考えています。このコースでは、サーバ上のファイル内にデータを格納するSQLiteを使用しますが、格納するデータの数が増えるにつれて、データを複数の異なるファイルに格納する方が理にかなっている場合があり、場合によっては別のサーバに格納する方が合理的な場合もあります。この場合、データベースサーバが受信するすべての要求を処理できなくなったときにどうするべきかという問題が発生します。拡張性に関する他の問題と同様に、この問題を緩和するために使用できる方法がいくつかあります。

  • 垂直パーティショニング:これは、最初にSQLを説明したときに使用した方法に似ています。この方法では、1つのテーブルに冗長な情報を持たせるのではなく、データを複数の異なるテーブルに分割します ( flights テーブルを flights テーブルと airports テーブルに分割したレッスン4を振り返ってください) 。
  • 水平パーティショニング:この方法では、同じフォーマットで情報が異なる複数の表を格納します。たとえば、flights テーブルを domestic_flights テーブルと international_flights テーブルに分割できます。こうすることで、JFKからLHRへのフライトを検索する際に、国内線でいっぱいのテーブルを探す時間を無駄にする必要がなくなります。この方法の欠点の1つは、複数のテーブルを分割して結合するとコストがかかることです。

データベースの複製

データベースを拡張した後も、単一障害点が残っているようです。データベースサーバがクラッシュすると、すべてのデータが失われる可能性があります。単一点障害を避けるためにサーバを追加したように、データベースのコピーを追加して、1つのデータベースの障害によってアプリケーションが停止しないようにすることができます。また、以前と同じように、データベース複製には異なる方法があります。最も一般的な方法は次の2つです。

  • 単一プライマリ・レプリケーション:この方法では、複数のデータベースが存在しますが、そのうちの1つのみがプライマリ・データベースとみなされます。つまり、1つのデータベースに対して読取りおよび書込みを実行できますが、他のデータベースに対しては読取りのみを実行できます。プライマリ・データベースが更新されると、他のデータベースもプライマリ・データベースと一致するように更新されます。このメソッドの欠点の1つは、データベースへの書き込み時に単一障害点がまだ含まれていることです。
single primary visual
  • マルチプライマリ・レプリケーション:この方法では、すべてのデータベースの読み取りと書き込みが可能です。これにより、単一点障害の問題は解決されますが、トレードオフが発生します。各データベースが他のすべてのデータベースに対する変更を認識する必要があるため、すべてのデータベースを最新の状態に保つことがはるかに困難になっています。このシステムはまた、いくつかの衝突の可能性を設定します。
    • 更新の競合:複数のデータベースがある場合、あるユーザがあるデータベースのローを編集しようとしたときに、別のユーザが別のデータベースの同じローを編集しようとすると、データベースの同期時に問題が発生します。
    • 一意性の競合:SQLデータベースの各行は一意の識別子を持たなければならず、2つの異なるデータベースの2つの異なるエントリに同じIDを割り当てるという問題が発生する可能性があります。
    • 削除の競合:あるユーザが行を削除し、別のユーザがその行を更新しようとしている可能性があります。
multi primary visual

キャッシング

大規模なデータベースを扱う場合、データベースとのやり取りはすべてコストがかかることを認識することが重要です。したがって、データベースサーバへの呼び出し回数を最小限に抑えたいと考えています。たとえば、New York Times のWebサイトを見てみましょう。New York Timesには、すべての記事がクエリーされるデータベースと、誰かがホーム・ページをロードするたびにレンダリングされるテンプレートがあるかもしれませんが、ホームページに表示される記事は秒単位でほとんど変化しないため、これはリソースの無駄です。この問題に対処する1つの方法は、キャッシングを使用することです。これは、近い将来再び必要になると予想される場合に、一部の情報をアクセスしやすい場所に格納するという考えです。

キャッシュを実装する1つの方法は、ユーザのWebブラウザにデータを格納することです。これにより、ユーザが特定のページをロードしたときに、サーバに要求を送信する必要がなくなります。これを行う1つの方法は、HTTP応答のヘッダーに次の行を含めることです。

Cache-Control: max-age=86400

これはブラウザに、ページを訪れたときに、最後の86400ミリ秒以内にそのページを訪れている限り、サーバに要求を行う必要がないことを伝えます。この方法は、特にCSSファイルなど、短期間で変更される可能性が低いファイルで、Webブラウザでよく使用されます。このプロセスをさらに制御するために、ドキュメントの特定のバージョンを表す一意の文字シーケンスであるHTTP応答ヘッダーに ETag を追加することもできます。これは、将来の要求でこのタグを含め、サーバ上の最新の文書のタグと比較し、両者が異なる場合にのみ文書全体を返すことができるため便利です。

前述のクライアント側のキャッシュに加えて、サーバ側にキャッシュを含めると便利な場合があります。このキャッシュを使用すると、バックエンドの設定は、すべてのサーバがキャッシュにアクセスできる次のようになります。

server caching

Djangoは独自のキャッシュフレームワークを提供しているため、プロジェクトにキャッシュを組み込むことができます。このフレームワークは、キャッシュを実装するいくつかの方法を提供します。

  • ビューごとのキャッシュ:これにより、特定のビューがロードされると、次に指定された時間、関数を使用せずに同じビューをレンダリングできるようになります。
  • Template-Fragmentキャッシュ:テンプレートの特定の部分をキャッシュするため、再レンダリングする必要はありません。たとえば、ほとんど変更されないナビゲーションバーがある場合は、リロードしないことで時間を節約できます。
  • Low-LevelキャッシュAPI:これにより、より柔軟なキャッシングが可能になり、基本的には必要な情報をすべて保存できます。

ここでは、Djangoにキャッシュを実装する方法の詳細は説明しませんが、興味があればドキュメントを参照してください。

セキュリティ

次に、Webアプリケーションのセキュリティを確保する方法について説明します。これには、このコースで説明したほぼすべてのトピックに及ぶさまざまな方法が含まれます。

GitとGitHub

GitとGitHubの最大の強みの1つは、インターネット上で誰でも見ることができ、貢献できるオープンソースソフトウェアであることです。オープンソースソフトウェアは誰でも簡単に共有し、変更することができます。この方法の欠点の1つは、パスワードやAPIキーなどのプライベートなクレデンシャルを含むファイルをコミットすると、そのクレデンシャルが公開される可能性があることです。

HTML

HTMLを使用すると発生する脆弱性は数多くありますが、一般的な脆弱性の1つにフィッシング攻撃があります。フィッシング攻撃は、あるページに移動しようと思っているユーザが実際に別のページに移動したときに発生します。これらは、ウェブサイトをデザインするときに必ずしも説明できることではありませんが、自分自身でウェブを操作するときには、必ず覚えておくべきことです。たとえば、悪意のあるユーザは次のHTMLを書き出します。

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Link</title>
    </head>
    <body>
        <a href="https://cs50.harvard.edu/">https://www.google.com/</a>
    </body>
</html>

これは以下のように動作します。

phishing attack

HTMLが実際にはリクエストの一部としてユーザに送信されるという事実は、サイトを作成するためのレイアウトやスタイルに誰もがアクセスできるため、より多くの脆弱性をもたらします。たとえば、ハッカーがbankofamerica.comにアクセスし、HTMLをすべてコピーして自分のサイトに貼り付けると、Bank of Americaのサイトにそっくりになります。その後、ハッカーはページ上のログインフォームをリダイレクトして、すべてのユーザ名とパスワードを送信できます (また、ここに本物のBank of Americaのリンクがあるので、クリックする前にURLをチェックしてみましょう)。

HTTPS

このコースですでに説明したように、オンラインで行われるほとんどの対話はHTTPプロトコルに従っていますが、現在では、HTTPの暗号化バージョンであるHTTPSを使用するトランザクションが増えています。これらのプロトコルを使用している間に、次の図に示すように、一連のサーバを介してコンピュータ間で情報が転送されます。

transferring

多くの場合、これらの転送のすべてが安全であることを保証する方法はありません。そのため、転送されるすべての情報が暗号化されることが重要です。つまり、メッセージの送信者と受信者が理解できるようにメッセージの文字が変更されますが、他の人は理解できません。

秘密鍵暗号

これに対する1つのアプローチは、秘密鍵暗号として知られています。このアプローチでは、送信者と受信者の両方が、自分だけが知っている秘密鍵にアクセスできます。次に、送信者は秘密キーを使用してメッセージを暗号化し、受信者はその秘密キーを使用してメッセージを復号化します。この方法は非常に安全ですが、実用性に関しては大きな問題があります。これが機能するためには、送信者と受信者の両方が秘密鍵にアクセスできる必要があります。つまり、安全に鍵を交換するためには、両者が直接会う必要があります。私たちが日常的にやりとりするウェブサイトの数を見れば、対面でのミーティングは選択肢にないことは明らかです。

公開鍵暗号

インターネットを現在のように機能させる暗号技術の驚くべき進歩は、公開鍵暗号として知られています。この方法では、2つの鍵があります。1つは公開鍵で共有でき、もう1つは秘密にしておく必要があります。これらの鍵が確立されると (コース全体を構成するキーのペアを作成するには、いくつかの異なる数学的方法があるため、ここでは説明しません)、送信者は受信者の公開キーを検索し、それを使用してメッセージを暗号化し、受信者は秘密キーを使用してメッセージを復号化できます。HTTPではなくHTTPSを使用する場合、要求が公開キー暗号化を使用して保護されていることがわかります。

データーベース

要求と応答に加えて、データベースの安全性も確認する必要があります。一般的に保存する必要があるのは、次の表に示すユーザ名とパスワードを含むユーザ情報です。

bad table

ただし、権限のないユーザがデータベースにアクセスした場合に備えて、パスワードをプレーンテキストで保存する必要はありません。代わりに、次の表に示すように、ハッシュ関数を使用して各パスワードのハッシュを作成します。ハッシュ関数は、テキストを取り込み、一見ランダムな文字列を出力する関数です。

good table

ハッシュ関数は一方向であることに注意してください。つまり、パスワードをハッシュに変換することはできますが、ハッシュをパスワードに戻すことはできません。つまり、この方法でユーザ情報を保存する企業は、ユーザのパスワードを実際には知らないということです。つまり、ユーザがサインインしようとするたびに、入力されたパスワードがハッシュ化され、既存のハッシュと比較されます。ありがたいことに、このプロセスはすでにDjangoによって処理されています。ユーザがパスワードを忘れてしまった場合、会社は古いパスワードを教えてくれず、新しいパスワードを作らなければなりません。

開発者としてどの程度の情報をリークするかを決定しなければならない場合もあります。たとえば、多くのサイトには、次のようなパスワードを忘れた場合のページがあります。

forgotten password?

開発者は、送信後に成功またはエラーメッセージを含めることができます。

success message
error message

しかし、電子メールを入力することで、誰がそのサイトに登録している電子メールを持っているかを知ることができることに注目してほしいのです。ユーザがサイトを利用するかどうかが重要でない場合 (たとえばフェイスブック) はこれで十分かもしれないが、ユーザが特定のサイトのメンバーであるという事実がユーザを危険にさらす可能性がある場合 (たとえば虐待被害者のためのオンライン支援グループ) は、非常に危険です。

データが漏洩するもう1つの方法は、応答が返ってくるまでにかかる時間です。電子メールアドレスが正しくてパスワードが間違っている人よりも、無効な電子メールを受け取った人を拒否する方が時間はかからないでしょう。

このコースですでに説明したように、コード内で単純なSQLクエリーを使用する場合は常に、SQLインジェクション攻撃に注意する必要があります。

API

多くの場合、JavaScriptとAPIを組み合わせて1ページのアプリケーションを作成します。独自のAPIを構築する場合、APIを安全に保つために使用できるメソッドがいくつかあります。

  • API Keys:キーを提供したAPIクライアントからの要求のみを処理します。
  • レート制限:任意のユーザが指定された時間枠内に実行できる要求の数を制限します。これにより、悪意のあるユーザがAPIを大量にコールしてクラッシュさせるDenial of Service (DOS) 攻撃から保護できます。
  • ルート認証:ルート認証を使用して、特定のユーザだけが特定のデータを表示できるようにするために、すべてのユーザにすべてのデータへのアクセスを許可したくない場合が多くあります。

環境変数

平文でパスワードを保存したくないのと同じように、ソースコードにAPIキーを含めることは避けたいと考えるでしょう。これを回避する一般的な方法の1つは、環境変数、つまりオペレーティングシステムまたはサーバの環境に格納されている変数を使用することです。ソースコードにテキストの文字列を含めるのではなく、環境変数への参照を含めることができます。

JavaScript

悪意のあるユーザがJavaScriptを使用しようとする攻撃には、いくつかの種類があります。例えば、ユーザが独自のJavaScriptコードを作成し、それをWebサイト上で実行するクロスサイトスクリプティング (Cross-Site Scripting) があります。たとえば、1つのURLを持つDjangoアプリケーションがあるとします。

urlpatterns = [
    path("<path:path>", views.index, name="index")
]

以下のように単一のビューを持つとします:

def index(request, path):
    return HttpResponse(f"Requested Path: {path}")

このWebサイトは、基本的に、ユーザがナビゲートしたURLをユーザに通知します。

good path

しかし、ユーザはURLに入力することで簡単にJavascriptをページに挿入することができます。

bad path

この alert の例は無害ですが、DOMを操作したり、fetch を使用して要求を送信したりするJavaScriptを含めることは、それほど難しいことではありません。

クロスサイト要求偽造

CSRF攻撃を防ぐためにDjangoを使用する方法についてはすでに説明しましたが、この保護なしで何が起こるかを見てみましょう。例えば、ある銀行が、自分の口座から送金してくれるURLを持っているとします。この転送を行うリンクを簡単に作成できます。

<a href="http://yourbank.com/transfer?to=brian&amt=2800">
    Click Here!
</a> 

この攻撃は、リンクよりもさらに巧妙な場合があります。URLが画像に挿入されている場合、ブラウザが画像をロードしようとするとURLにアクセスされます。

 <img src="http://yourbank.com/transfer?to=brian&amt=2800"> 

このため、何らかの状態変更を受け入れることができるアプリケーションを作成する場合は、常にPOSTリクエストを使用して実行する必要があります。銀行がPOSTリクエストを要求している場合でも、隠しフォームフィールドによってユーザが誤ってリクエストを送信してしまう可能性があります。次のフォームでは、ユーザがクリックするのを待つこともありません。自動的にサブミットします。

 <body onload="document.forms[0].submit()">
    <form action="https://yourbank.com/transfer"
    method="post">
        <input type="hidden" name="to" value="brian">
        <input type="hidden" name="amt" value="2800">
        <input type="submit" value="Click Here!">
    </form>
</body>

上記は、Cross-Site Request Forgeryの例です。Webページを読み込むときにCSRFトークンを作成し、有効なトークンを持つフォームだけを受け入れることで、このような攻撃を防ぐことができます。

次はなんですか?

このクラスでは、DjangoやReactなど、多くのWebフレームワークについて説明しましたが、他にも試してみたいフレームワークがあります。

将来的には、Webにサイトを展開できるようにすることもできます。これには、次のようなさまざまなサービスがあります。

このコースを始めて以来、私たちは長い道のりを歩んできて、多くの素材をカバーしてきましたが、ウェブプログラミングの世界で学ぶべきことはまだたくさんあります。ときには圧倒されることもあるかもしれませんが、より多くを学ぶための最良の方法の1つは、プロジェクトに飛び込んで、それをどこまで実行できるかを見ることです。この時点で、あなたはウェブデザインのコンセプトにおいて強固な基盤を持っており、アイデアをあなた自身のウェブサイトに変えるために必要なものを持っていると信じています。