LoginSignup
2
1

More than 1 year has passed since last update.

[HUGO+GAE+FastAPI] HUGOで作成したサイトをBasic認証によるアクセス制限付きで公開

Last updated at Posted at 2021-05-14

Hugoで作成したサイトをアクセス制限付きで公開したい

Hugoはとても簡単にサイトが作れて、かっこいいテーマもたくさんあって楽しいですね。
あるコミュニティ向けにちょっとした情報サイトを作ろうと思い、そうすると閲覧制限を付けたいので、どうすると良いのかなと考えました。

Hugoで作成したサイトを Google App Engine で公開します。
GETリクエストをFastAPIで処理して、特定のパスに対してはBasic認証を通さないとコンテンツを戻さないようにすることで、HUGOのコンテンツを、制限なし、制限付きに分けて公開するできるようにしました。


構成
.
├── app.py
├── app.yaml
├── mysite
│   ├── archetypes
│   ├── assets
│   ├── config.toml
│   ├── content
│   ├── data
│   ├── layouts
│   ├── public
│   ├── resources
│   ├── static
│   └── themes
└── requirements.txt

最終的なファイル構成はこのようになります。 mysite以下にHugoのサイトを置きます。

Hugoでサイトを作る

Hugoでサイトを作ります。例として、以下のようにしています。

.
├── content
│   ├── _index.md
│   ├── docs
│   │   ├── caution.md
│   │   ├── doc.md
│   │   └── secret
│   │       └── secret.md

docs/doc.md は通常のページ。制限無しに公開するページです。
docs/secret 以下にあるページは制限付きのページにします。
docs/caution.md は制限付きページの認証に失敗した際に表示するページです。

config.toml
baseURL = "https://xxxxxxxxx.df.r.appspot.com/"
languageCode = "ja"
title = "おもしろサイト"
...

config.toml の baseURL を GAEのURLに変えておきます。

GAE向けのファイルを用意する

app.yaml
runtime: python39
entrypoint: uvicorn app:app --port $PORT
  • app.yaml

シンプル。


requirements.txt
fastapi
uvicorn
aiofiles
  • requirements.txt

aiofiles はFastAPIが FileResponse を返すのに必要みたいです。
webフレームワークはFastAPIでなくとも良いですが、FastAPIは起動が早いので気に入っています。


app.py
import secrets
from pathlib import Path
from fastapi import FastAPI, Depends, status, HTTPException
from fastapi.responses import FileResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()
security = HTTPBasic()

@app.get("/docs/secret/{file_path:path}")
async def secret_path(
    file_path: str, credentials: HTTPBasicCredentials = Depends(security)
):

    correct_username = secrets.compare_digest(credentials.username, "user")
    correct_password = secrets.compare_digest(credentials.password, "pass")
    if not (correct_username and correct_password):
        response = FileResponse("mysite/public/docs/caution/index.html")
        response.status_code = status.HTTP_401_UNAUTHORIZED
        return response

    target = Path("mysite/public/docs/secret/{}".format(file_path))
    return read_file(target)


@app.get("/{file_path:path}")
async def public_path(file_path: str):
    target = Path("mysite/public/{}".format(file_path))
    return read_file(target)


def read_file(target: Path):
    if not target.exists():
        response = FileResponse("mysite/public/404.html")
        response.status_code = status.HTTP_404_NOT_FOUND
    elif target.is_dir():
        response = FileResponse(target.joinpath("index.html"))
    elif target.is_file():
        response = FileResponse(target)
    else:
        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)

    return response

説明

  • app.py
@app.get("/docs/secret/{file_path:path}")
async def secret_path(
    file_path: str, credentials: HTTPBasicCredentials = Depends(security)
):
    correct_username = secrets.compare_digest(credentials.username, "user")
    correct_password = secrets.compare_digest(credentials.password, "pass")
    if not (correct_username and correct_password):
        response = FileResponse("mysite/public/docs/caution/index.html")
        response.status_code = status.HTTP_401_UNAUTHORIZED
        return response

    target = Path("mysite/public/docs/secret/{}".format(file_path))
    return read_file(target)

↑ まず制限付きページへのGETリクエストを受けるデコレータを、定義します。

Basic認証のログインダイアログを表示して、IDとPASSを受け取ります。
入力されたIDとPASSを検証して、一致していなければ、認証失敗を示すページをFileResponseで返します。
ステータスコードは認証失敗を示す401にしておきます。そうしておかないと、再度制限付きページにアクセスしたときにログインダイアログが表示されなくなります(一度入力したものが自動で使われてしまう?)。

ID/PASSが一致していれば、mysite/public以下のファイルパスへの置き換えをしてFileResponseを返します。

@app.get("/{file_path:path}")
async def public_path(file_path: str):
    target = Path("mysite/public/{}".format(file_path))
    return read_file(target)

↑ 制限のないページへのGETリクエストを受けるデコレータも定義します。

def read_file(target: Path):
    if not target.exists():
        response = FileResponse("mysite/public/404.html")
        response.status_code = status.HTTP_404_NOT_FOUND
    elif target.is_dir():
        response = FileResponse(target.joinpath("index.html"))
    elif target.is_file():
        response = FileResponse(target)
    else:
        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)

    return response

mysite/public以下のファイルパスをチェックして、もし存在していなければ404.htmlを返します。(テーマによってはパスが違うのかもしれません)
ディレクトリであれば、その直下のindex.htmlを戻します。ファイルであれば、そのまま返します。
さいごよくわからないものはとりあえず500 internal server error にしておきました。

こんなかんじ

hoge.gif

2
1
3

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
2
1