Python으로 사용자 인증

Python Bookshelf 가이드 중 이 부분에서는 사용자를 위한 로그인 과정을 만드는 방법과 프로필 정보를 사용해서 사용자에게 맞춤설정된 기능을 제공하는 방법을 보여줍니다.

Google Identity Platform을 사용하면 개발자는 사용자에 대한 정보에 쉽게 액세스하면서 Google에서 로그인 사용자 인증 정보를 안전하게 관리하도록 할 수 있습니다. OAuth 2.0을 사용하면 앱의 모든 사용자에게 로그인 과정을 쉽게 제공하고 인증된 사용자의 기본 프로필 정보에 대한 액세스 권한을 애플리케이션에 제공할 수 있습니다.

이 페이지는 여러 페이지로 구성된 가이드의 일부입니다. 처음부터 시작하여 설정 안내를 보려면 Python Bookshelf 앱으로 이동하세요.

웹 애플리케이션 클라이언트 ID 만들기

웹 앱 클라이언트 ID를 사용하면 앱에서 사용자를 승인하고 사용자를 대신하여 Google API에 액세스할 수 있습니다.

  1. Google Cloud Platform Console에서 사용자 인증 정보로 이동합니다.

    사용자 인증 정보

  2. OAuth 동의 화면을 클릭합니다. 제품 이름으로 Python Bookshelf App을 입력합니다.

  3. 승인된 도메인의 경우 App Engine 앱 이름을 [YOUR_PROJECT_ID].appspot.com으로 추가합니다. [YOUR_PROJECT_ID]를 GCP 프로젝트 ID로 바꿉니다.

  4. 다른 관련 선택 필드를 작성합니다. 저장을 클릭합니다.

  5. 사용자 인증 정보 만들기 > OAuth 클라이언트 ID를 클릭합니다.

  6. 애플리케이션 유형 드롭다운 목록에서 웹 애플리케이션을 클릭합니다.

  7. 이름 필드에 Python Bookshelf Client를 입력합니다.

  8. 승인된 리디렉션 URI 필드에 다음 URL을 한 번에 하나씩 입력합니다.

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

  9. 만들기를 클릭합니다.

  10. 클라이언트 ID클라이언트 비밀번호를 복사하고 나중에 사용할 수 있도록 저장합니다.

설정 구성

이 섹션에서는 4-auth 디렉토리의 코드를 사용합니다. 이 디렉토리에서 파일을 수정하고 명령어를 실행하세요.

  1. 수정하기 위해 config.py를 엽니다.
  2. PROJECT_ID 값을 GCP 콘솔에 표시되는 프로젝트 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 계정으로 로그인하고 도서를 추가하고 상단 탐색 메뉴의 내 도서 링크를 사용하여 추가한 도서를 볼 수 있습니다.

작업자를 종료한 다음 로컬 웹 서버를 종료하려면 Control+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. 페이지 상단의 삭제 버튼을 클릭하여 앱 버전을 삭제합니다.

청구 가능한 리소스 삭제에 대한 자세한 내용은 이 가이드의 마지막 단계에서 삭제를 참조하세요.

애플리케이션 구조

이 다이어그램은 앱의 구성요소 및 서로 연결된 방식을 보여줍니다.

인증 샘플 구조

애플리케이션은 사용자를 Google 인증 서비스로 리디렉션합니다. 이 서비스는 인증 시 사용자를 다시 리디렉션합니다. 애플리케이션은 사용자의 프로필 정보를 세션에 저장합니다.

코드 이해하기

이 섹션에서는 애플리케이션 코드에 대해 단계별로 알아보고 이 코드의 작동 방식을 설명합니다.

세션 정보

애플리케이션에서 사용자를 인증하려면 현재 사용자에 대한 정보를 세션에 저장하는 방법이 필요합니다. 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를 비롯한 많은 데이터베이스를 손쉽게 배포할 수 있습니다. 이러한 서비스를 설정하는 방법은 본 가이드에서 다루지 않습니다.

사용자 인증

사용자 인증에는 웹 서비스 흐름이라고 하는 두 가지 기본 단계가 있습니다.

  • 사용자를 Google 인증 서비스로 리디렉션합니다.
  • Google이 사용자를 애플리케이션으로 다시 리디렉션하면 응답을 처리합니다.

oauth2client에서 flask_util 확장 프로그램을 통해 흐름을 직접 구현하지 않고도 OAuth2를 애플리케이션에 손쉽게 통합할 수 있습니다.

  1. UserOAuth2 인스턴스를 만듭니다.

    from oauth2client.contrib.flask_util import UserOAuth2
    
    oauth2 = UserOAuth2()
  2. Flask 애플리케이션을 사용하여 인스턴스를 초기화합니다. emailprofile 범위를 통해 사용자의 이메일 주소와 기본 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+ API 메소드 people.get('me')가 인증된 사용자의 해당 정보를 제공할 수 있습니다. UserOAuth2에는 사용자에 대한 사용자 인증 정보를 얻은 후 함수를 실행하는 데 사용할 수 있는 authorize_callback 인수가 있습니다.

    def _request_user_info(credentials):
        """
        Makes an HTTP request to the Google+ 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/plus/v1/people/me')
    
        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.image %}
          <img class="img-circle" src="{{session.profile.image.url}}" width="24">
        {% endif %}
        {{session.profile.displayName}}
      </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']['displayName']
            data['createdById'] = session['profile']['id']

        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']['id'],
        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)
이 페이지가 도움이 되었나요? 평가를 부탁드립니다.

다음에 대한 의견 보내기...