Python でのユーザーの認証

Python Bookshelf チュートリアルのこのパートでは、ユーザーのログインフローの作成方法と、プロフィール情報に基づいてユーザー固有の機能を提供する方法について説明します。

Google Identity Platform を使用すると、ユーザーの情報に簡単にアクセスできると同時に、ユーザーのログイン認証情報が Google によって安全に管理されます。 OAuth 2.0 を使用すると、アプリのすべてのユーザーにログインフローが簡単に提供され、アプリケーションから認証済みユーザーに関する基本的なプロフィール情報にアクセスできるようになります。

このページは複数ページからなるチュートリアルの一部です。最初からの説明や設定手順を確認するには、Python Bookshelf アプリに移動してください。

ウェブ アプリケーション クライアント ID の作成

ウェブアプリのクライアント ID を使用することで、アプリによるユーザーの承認と Google API へのアクセスが可能になります。

  1. Google Cloud Platform Console で、[認証情報] ページに移動します。

    [認証情報] ページに移動

  2. [OAuth 同意画面] をクリックします。

  3. プロダクト名に「Python Bookshelf App」と入力します。

  4. 承認済みドメインの場合、App Engine アプリケーション名を [YOUR_PROJECT_ID].appspot.com として追加します。

    [YOUR_PROJECT_ID] は実際の GCP プロジェクト ID に置き換えてください。

  5. その他の関連する任意フィールドを入力して [保存] をクリックします。

  6. [認証情報を作成] > [OAuth クライアント ID] をクリックします。

  7. [アプリケーションの種類] プルダウン リストで、[ウェブ アプリケーション] をクリックします。

  8. [名前] フィールドに「Python Bookshelf Client」と入力します。

  9. [承認済みのリダイレクト URI] に、以下の URL を 1 つずつ入力します。

    http://localhost:8080/oauth2callback
    http://[YOUR_PROJECT_ID].appspot.com/oauth2callback
    https://[YOUR_PROJECT_ID].appspot.com/oauth2callback

  10. [作成] をクリックします。

  11. クライアント IDクライアント シークレットをコピーし、後で使用するために保存します。

設定の構成

ここでは、4-auth ディレクトリにあるコードを使用します。ファイルを編集し、このディレクトリでコマンドを実行します。

  1. config.py を編集用に開きます。
  2. PROJECT_ID の値を、GCP Console に表示されるプロジェクト ID に設定します。
  3. DATA_BACKEND を、構造化データの使用チュートリアルで使用したものと同じ値に設定します。
  4. Cloud SQL または MongoDB を使用する場合は、Cloud SQL または Mongo セクションの値を、「構造化データの使用」ステップで使用したものと同じ値に設定します。
  5. CLOUD_STORAGE_BUCKET の値を、実際の Cloud Storage バケット名に設定します。

  6. OAuth2 configuration セクションの GOOGLE_OAUTH2_CLIENT_IDGOOGLE_OAUTH2_CLIENT_SECRET の値を、以前に作成したアプリケーション クライアント ID とシークレットに設定します。

  7. config.py を保存して閉じます。

Cloud SQL を使用する場合:

  1. app.yaml を編集用に開きます。
  2. cloud_sql_instances の値を、config.py CLOUDSQL_CONNECTION_NAME で使用したものと同じ値に設定します。 project:region:cloudsql-instance の形式で設定してください。この行全体のコメントを解除します。
  3. app.yaml を保存して閉じます。

依存関係のインストール

以下のコマンドを入力して仮想環境を作成し、依存関係をインストールします。

Linux / macOS

virtualenv -p python3 env
source env/bin/activate
pip install -r requirements.txt

Windows

virtualenv -p python3 env
env\scripts\activate
pip install -r requirements.txt

ローカルマシンでのアプリの実行

  1. ローカル ウェブサーバーを起動します。

    python main.py
    
  2. ウェブブラウザに次のアドレスを入力します。

    http://localhost:8080

これで、アプリのウェブページを閲覧できるようになります。Google アカウントでログインして書籍を追加できます。追加した書籍は、上部のナビゲーション バーにある [My Books] リンクから確認できます。

Ctrl+C キーを押してワーカーを終了し、次にローカル ウェブサーバーを終了します。

App Engine フレキシブル環境へのアプリのデプロイ

  1. サンプルアプリをデプロイします。

    gcloud app deploy
    
  2. ウェブブラウザで、次のアドレスを入力します。[YOUR_PROJECT_ID] は実際のプロジェクト ID で置き換えます。

    https://[YOUR_PROJECT_ID].appspot.com
    

アプリを更新する場合は、最初にデプロイしたときと同じコマンドを使って、更新バージョンをデプロイできます。新たにデプロイすると、アプリの新しいバージョンが作成され、それがデフォルトのバージョンに設定されます。古いバージョンはそのまま残り、関連付けられた VM インスタンスも同様に残ります。すべてのアプリ バージョンと VM インスタンスが課金対象のリソースとなるのでご注意ください。

アプリのデフォルト以外のバージョンを削除することで、コストを削減できます。

アプリのバージョンを削除するには:

  1. GCP Console で、App Engine の [バージョン] ページに移動します。

    [バージョン] ページに移動

  2. デフォルト以外で削除するアプリのバージョンのチェックボックスを選択します。
  3. アプリのバージョンを削除するには、[削除]()をクリックします。

課金対象のリソースをクリーンアップする方法の詳細については、このチュートリアルの最後のステップにあるクリーンアップ セクションをご覧ください。

アプリケーションの構造

次の図は、アプリケーションを構成するコンポーネントと、それらの接続関係を示しています。

Auth サンプルの構造

アプリケーションは、ユーザーを Google の承認サービスにリダイレクトします。承認されたユーザーは、アプリケーションに再度リダイレクトされます。アプリケーションは、ユーザーのプロフィール情報をセッションに格納します。

コードを理解する

このセクションでは、アプリケーションのコードとその動作を順を追って説明します。

セッションについて

アプリケーションがユーザーを認証するには、現在のユーザーに関する情報をセッションに格納する方法を確保する必要があります。 暗号化された Cookie に基づくセッションを含む Flask は、この機能を備えています。

Flask でセッションを使用するには、アプリケーションの秘密鍵を設定する必要があります。

SECRET_KEY = 'secret'

Flask では、urandom を使用してランダムなキーを生成することが推奨されます。

python
>>> import os
>>> os.urandom(24)
'\xfd{H\xe5<\x95\xf9\xe3\x96.5\xd1\x01O<!\xd5\xa2\xa0\x9fR"\xa1\xa8'

最後の出力をコピーし、config.py に秘密鍵として貼り付けます。

本番環境では、集中管理データベースを使用してセッションを安全に格納することもできます。Google Cloud Platform では、Redis や Memcache など、多くのデータベースを簡単にデプロイできます。これらのサービスの設定については、このチュートリアルでは扱いません。

ユーザーの認証

ユーザーの認証には次の 2 つの基本ステップが必要です。これらを総称して「ウェブサービス フロー」と呼びます。

  • ユーザーを Google の承認サービスにリダイレクトする。
  • ユーザーがアプリケーションに再度リダイレクトされたときのレスポンスを処理する。

oauth2clientflask_util 拡張機能を使用すると、このフローを自分で実装せずに済むため、OAuth2 をアプリケーションに簡単に統合できます。

  1. UserOAuth2 インスタンスを作成します。

    from oauth2client.contrib.flask_util import UserOAuth2
    
    oauth2 = UserOAuth2()
  2. Flask アプリケーションでこのインスタンスを初期化します。email および profile スコープを使用して、ユーザーのメールアドレスと基本的な Google プロフィール情報にアクセスできます。

    # Initalize the OAuth2 helper.
    oauth2.init_app(
        app,
        scopes=['email', 'profile'],
        authorize_callback=_request_user_info)
  3. flask_util にはユーザーがログアウトする機能がデフォルトで用意されていないため、この機能を作成します。

    # Add a logout handler.
    @app.route('/logout')
    def logout():
        # Delete the user's profile and the credentials stored by oauth2.
        del session['profile']
        session.modified = True
        oauth2.storage.delete()
        return redirect(request.referrer or '/')
  4. 認証情報を取得したら、ユーザーに関する情報をフェッチできます。認証情報にはユーザーの ID とメールアドレスを提供する id_token が含まれていますが、ユーザーの名前や写真などの基本的なプロフィール情報を取得する方が便利です。Google OAuth2 API のメソッド userinfo を使用すると、認証済みのユーザーに関する情報を取得できます。UserOAuth2 には、ユーザーの認証情報を取得した後で関数を実行するために使用できる authorize_callback 引数があります。

    def _request_user_info(credentials):
        """
        Makes an HTTP request to the Google OAuth2 API to retrieve the user's basic
        profile information, including full name and photo, and stores it in the
        Flask session.
        """
        http = httplib2.Http()
        credentials.authorize(http)
        resp, content = http.request(
            'https://www.googleapis.com/oauth2/v3/userinfo')
    
        if resp.status != 200:
            current_app.logger.error(
                "Error while obtaining user profile: \n%s: %s", resp, content)
            return None
        session['profile'] = json.loads(content.decode('utf-8'))
    
  5. セッションにより提供されたプロフィール情報をテンプレートで使用して、ユーザーに自分がログインしているかログアウトしているかを示します。

    <p class="navbar-text navbar-right">
    {% if session.profile %}
      <a href="/logout">
        {% if session.profile.picture %}
          <img class="img-circle" src="{{session.profile.picture}}" width="24">
        {% endif %}
        {{session.profile.name}}
      </a>
    {% else %}
      <a href="/oauth2authorize">Login</a>
    {% endif %}
    </p>

カスタマイズ

これでセッションを介してログイン ユーザーの情報を利用できるようになったため、どのユーザーのどの書籍をデータベースに追加したのかを追跡できます。

@crud.route('/add', methods=['GET', 'POST'])
def add():
    if request.method == 'POST':
        data = request.form.to_dict(flat=True)

        # If an image was uploaded, update the data to point to the new image.
        image_url = upload_image_file(request.files.get('image'))

        if image_url:
            data['imageUrl'] = image_url

        # If the user is logged in, associate their profile with the new book.
        if 'profile' in session:
            data['createdBy'] = session['profile']['name']
            data['createdById'] = session['profile']['email']

        book = get_model().create(data)

        return redirect(url_for('.view', id=book['id']))

    return render_template("form.html", action="Add", book={})

データベース内にこの情報が格納されているため、この情報を使用して、データベースに追加したすべての書籍をユーザーに示すことができる新しいビューを作成できます。

@crud.route("/mine")
@oauth2.required
def list_mine():
    token = request.args.get('page_token', None)
    if token:
        token = token.encode('utf-8')

    books, next_page_token = get_model().list_by_user(
        user_id=session['profile']['email'],
        cursor=token)

    return render_template(
        "list.html",
        books=books,
        next_page_token=next_page_token)

このビューは oauth2.required でデコレートされています。つまり、有効な認証情報を持つユーザーのみがこのビューにアクセスできます。認証情報を持っていないユーザーは、OAuth2 ウェブフローにリダイレクトされます。

このコードでは、list_by_user と呼ばれる新しいモデルメソッドを使用しています。実装は選択したバックエンドのデータベースに応じて異なります。

Datastore

def list_by_user(user_id, limit=10, cursor=None):
    ds = get_client()
    query = ds.query(
        kind='Book',
        filters=[
            ('createdById', '=', user_id)
        ]
    )

    query_iterator = query.fetch(limit=limit, start_cursor=cursor)
    page = next(query_iterator.pages)

    entities = builtin_list(map(from_datastore, page))
    next_cursor = (
        query_iterator.next_page_token.decode('utf-8')
        if query_iterator.next_page_token else None)

    return entities, next_cursor

Cloud SQL

def list_by_user(user_id, limit=10, cursor=None):
    cursor = int(cursor) if cursor else 0
    query = (Book.query
             .filter_by(createdById=user_id)
             .order_by(Book.title)
             .limit(limit)
             .offset(cursor))
    books = builtin_list(map(from_sql, query.all()))
    next_page = cursor + limit if len(books) == limit else None
    return (books, next_page)

MongoDB

def list_by_user(user_id, limit=10, cursor=None):
    cursor = int(cursor) if cursor else 0

    results = mongo.db.books\
        .find({'createdById': user_id}, skip=cursor, limit=10)\
        .sort('title')
    books = builtin_list(map(from_mongo, results))

    next_page = cursor + limit if len(books) == limit else None
    return (books, next_page)
このページは役立ちましたか?評価をお願いいたします。

フィードバックを送信...