با درخواست Google One Tap for Web وارد سیستم شوید

1. مقدمه

این کد لبه بر روی دکمه ورود با Google برای Web codelab ساخته شده است، بنابراین ابتدا آن را کامل کنید.

در این لبه کد، از کتابخانه جاوا اسکریپت خدمات هویت گوگل و درخواست های One Tap برای افزودن ورود کاربر به صفحات وب استاتیک و پویا با استفاده از HTML و APIهای جاوا اسکریپت استفاده خواهید کرد.

یک اعلان با یک ضربه

ما همچنین یک نقطه پایانی ورود به سیستم سمت سرور را برای تأیید توکن های JWT ID تنظیم خواهیم کرد.

چیزی که یاد خواهید گرفت

  • نحوه تنظیم یک نقطه پایانی ورود سمت سرور برای تأیید توکن های ID
  • نحوه اضافه کردن درخواست Google One Tap به یک صفحه وب
    • به عنوان یک عنصر HTML ایستا و
    • به صورت پویا با استفاده از جاوا اسکریپت
  • اعلان One Tap چگونه رفتار می کند

آنچه شما نیاز دارید

  1. دانش اولیه HTML، CSS، JavaScript و Chrome DevTools (یا معادل آن).
  2. مکانی برای ویرایش و میزبانی فایل های HTML و جاوا اسکریپت.
  3. شناسه مشتری به دست آمده در آزمایشگاه کد قبلی.
  4. محیطی که قادر به اجرای یک برنامه پایه پایتون است.

بگذار برویم

2. یک نقطه پایانی برای ورود به سیستم تنظیم کنید

ابتدا یک اسکریپت پایتون ایجاد می کنیم که به عنوان یک وب سرور اصلی عمل می کند و محیط پایتون لازم برای اجرای آن را تنظیم می کنیم.

این اسکریپت که به صورت محلی اجرا می شود، صفحه فرود و صفحات HTML ایستا و پویا One Tap را به مرورگر ارائه می دهد. درخواست‌های POST را می‌پذیرد، JWT موجود در پارامتر اعتبارنامه را رمزگشایی می‌کند و تأیید می‌کند که توسط پلتفرم OAuth Google Identity صادر شده است.

پس از رمزگشایی و تأیید یک JWT، اسکریپت برای نمایش نتایج به صفحه فرود index.html هدایت می شود.

این را در فایلی به نام simple-server.py کپی کنید:

"""Very basic web server to handle GET and POST requests."""
from http.server import SimpleHTTPRequestHandler
import json
import socketserver
from typing import Dict, Optional, Tuple
import urllib.parse
from urllib.parse import parse_qs

from google.auth.transport import requests as google_auth_requests
from google.oauth2 import id_token


""" NOTE: You'll need to change this """
CLIENT_ID = (
    "PUT_YOUR_WEB_CLIENT_ID_HERE"
)

""" these may change for a Cloud IDE, but good as-is for local termainals """
SERVER_ADDRESS = "0.0.0.0"
PORT = 3000
TARGET_HTML_PAGE_URL = f"http://localhost:{PORT}/"
""" and this is the end of constants you might need to change """

HTTP_STATUS_OK = 200
HTTP_STATUS_BAD_REQUEST = 400
HTTP_STATUS_UNAUTHORIZED = 401
HTTP_STATUS_INTERNAL_SERVER_ERROR = 500
HTTP_STATUS_FOUND = 303  # For redirection after decode and verify
OIDC_SERVER = "accounts.google.com"


class OIDCJWTReceiver(SimpleHTTPRequestHandler):
  """Request handler to securely process a Google ID token response."""

  def _validate_csrf(self, request_parameters: Dict) -> Tuple[bool, str]:
    """Validates the g_csrf_token to protect against CSRF attacks."""
    csrf_token_body = request_parameters.get("g_csrf_token")
    if not csrf_token_body:
      return False, "g_csrf_token not found in POST body."

    csrf_token_cookie = None
    cookie_header = self.headers.get("Cookie")
    if cookie_header:
      cookie_pairs = (c.split("=", 1) for c in cookie_header.split(";"))
      cookies = {k.strip(): v.strip() for k, v in cookie_pairs}
      csrf_token_cookie = cookies.get("g_csrf_token")
    if not csrf_token_cookie:
      return False, "g_csrf_token not found in cookie."

    if csrf_token_body != csrf_token_cookie:
      return False, "CSRF token mismatch."

    return True, "CSRF token validated successfully."

  def _parse_and_validate_credential(
      self, request_parameters: Dict
  ) -> Optional[Tuple[Optional[Dict], str]]:
    """Parse POST data, extract, decode and validate user credential."""
    credential = request_parameters.get("credential")
    if not credential:
      return None, "Credential not provided"

    try:
      id_info = id_token.verify_oauth2_token(
          credential, google_auth_requests.Request(), CLIENT_ID
      )
      return id_info, ""
    except ValueError as e:
      return None, f"Error during JWT decode: {e}"
    except Exception as e:
      return None, f"Unexpected error during credential validation: {e}"

  def _redirect_to_html(self, response_data: Dict) -> None:
    """Redirect to the target HTML page with data in the URL fragment."""
    try:
      json_data = json.dumps(response_data)
      encoded_data = urllib.parse.quote(json_data)
      redirect_url = f"http://localhost:{PORT}/#data={encoded_data}"
      self.send_response(HTTP_STATUS_FOUND)
      self.send_header("Location", redirect_url)
      self.send_header("Connection", "close")
      self.end_headers()
    except Exception as e:
      print(f"An error occurred during redirection: {e}")
      self.send_response(HTTP_STATUS_INTERNAL_SERVER_ERROR)
      self.send_header("Content-type", "text/plain")
      self.send_header("Connection", "close")
      self.end_headers()
      self.wfile.write(f"A redirect error occurred: {e}".encode("utf-8"))

  def _send_bad_request(self, message: str) -> None:
    """Sends a 400 Bad Request response."""
    self.send_response(HTTP_STATUS_BAD_REQUEST)
    self.send_header("Content-type", "text/plain")
    self.send_header("Connection", "close")
    self.end_headers()
    self.wfile.write(message.encode("utf-8"))

  def do_POST(self):
    """Handle POST requests for the /user-login path."""
    if self.path != "/user-login":
      self.send_error(404, "File not found")
      return

    try:
      content_length = int(self.headers.get("Content-Length", 0))
      post_data_bytes = self.rfile.read(content_length)
      post_data_str = post_data_bytes.decode("utf-8")
      request_parameters = {
          key: val[0]
          for key, val in parse_qs(post_data_str).items()
          if len(val) == 1
      }

      csrf_valid, csrf_message = self._validate_csrf(request_parameters)
      if not csrf_valid:
        print(f"CSRF verify failure: {csrf_message}")
        self._send_bad_request(f"CSRF verify failure: {csrf_message}")
        return

      decoded_id_token, error_message = self._parse_and_validate_credential(
          request_parameters
      )

      response_data = {}
      if decoded_id_token:
        response_data["status"] = "success"
        response_data["message"] = decoded_id_token
      elif error_message:
        response_data["status"] = "error"
        response_data["message"] = error_message
      else:
        response_data["status"] = "error"
        response_data["message"] = "Unknown error during JWT validation"

      self._redirect_to_html(response_data)

    except Exception as e:
      self._redirect_to_html(
          {"status": "error", "error_message": f"Internal server error: {e}"}
      )


with socketserver.TCPServer(("", PORT), OIDCJWTReceiver) as httpd:
  print(
      f"Serving HTTP on {SERVER_ADDRESS} port"
      f" {PORT} (http://{SERVER_ADDRESS}:{PORT}/)"
  )
  httpd.serve_forever()

از آنجایی که می‌خواهیم تأیید کنیم JWT با استفاده از فیلد مخاطب (aud) کد ID برای مشتری شما صادر شده است، برنامه پایتون شما باید بداند که از کدام شناسه مشتری استفاده می‌شود. برای انجام این کار، PUT_YOUR_WEB_CLIENT_ID_HERE با شناسه مشتری که در لبه کد قبلی دکمه Sign In With Google استفاده کردید، جایگزین کنید.

محیط پایتون

اجازه می دهد تا محیط را برای اجرای اسکریپت وب سرور تنظیم کنیم.

برای کمک به تأیید JWT و رمزگشایی به پایتون 3.8 یا جدیدتر به همراه چند بسته نیاز دارید.

$ python3 --version

اگر نسخه python3 شما کمتر از 3.8 است، ممکن است لازم باشد PATH پوسته خود را تغییر دهید تا نسخه مورد انتظار پیدا شود یا یک نسخه جدیدتر از Python را روی سیستم خود نصب کنید.

در مرحله بعد، فایلی به نام requirements.txt بسازید که بسته های مورد نیاز ما برای رمزگشایی و تأیید JWT را فهرست می کند:

google-auth

برای ایجاد یک محیط مجازی و نصب بسته ها فقط برای این برنامه، این دستورات را در همان دایرکتوری simple-server.py و requirements.txt اجرا کنید:

$ python3 -m venv env
$ source env/bin/activate
(env) $ pip install -r requirements.txt

اکنون سرور را راه اندازی کنید و اگر همه چیز به درستی کار کند، این را خواهید دید:

(env) $ python3 ./simple-server.py
Serving HTTP on 0.0.0.0 port 3000 (http://0.0.0.0:3000/) ...

IDE های مبتنی بر ابر

این کد لبه برای اجرا در یک ترمینال محلی و میزبان محلی طراحی شده است، اما با برخی تغییرات ممکن است در پلتفرم هایی مانند Replit یا Glitch استفاده شود. هر پلتفرم نیازمندی‌های راه‌اندازی و پیش‌فرض‌های محیط پایتون خود را دارد، بنابراین احتمالاً باید مواردی مانند TARGET_HTML_PAGE_URL و تنظیمات پایتون را تغییر دهید.

به عنوان مثال، در Glitch شما همچنان یک requirements.txt اضافه می‌کنید، اما یک فایل به نام start.sh ایجاد می‌کنید تا به‌طور خودکار سرور پایتون راه‌اندازی شود:

python3 ./simple-server.py

URL های استفاده شده توسط اسکریپت پایتون و فایل های HTML نیز باید به URL خارجی Cloud IDE شما به روز شوند. بنابراین ما چیزی شبیه به این خواهیم داشت: TARGET_HTML_PAGE_URL = f"https://your-project-name.glitch.me/" و از آنجایی که فایل های HTML در سراسر این کد لبه به طور پیش فرض برای استفاده از localhost نیز نیاز دارید، باید آنها را با URL خارجی Cloud IDE به روز کنید: data-login_uri="https://your-project-name.glitch.me/user-login" .

3. یک صفحه فرود ایجاد کنید

در مرحله بعد، یک صفحه فرود ایجاد می کنیم که نتایج ورود به سیستم با One Tap را نمایش می دهد. صفحه رمز رمزگشایی شده JWT ID یا یک خطا را نشان می دهد. یک فرم در صفحه همچنین می تواند برای ارسال یک JWT به نقطه پایانی ورود به سیستم در سرور HTTP پایتون استفاده شود، جایی که رمزگشایی و تأیید می شود. از یک کوکی ارسال دوبار CSRF و پارامتر درخواست POST استفاده می کند تا بتواند از همان نقطه پایانی سرور user-login مانند نمونه های gsi/client HTML و JavaScript API در Codelab استفاده مجدد کند.

در ترمینال خود، این را در فایلی به نام index.html ذخیره کنید:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>JWT Verification</title>
    <style>
      body { font-family: sans-serif; margin: 0; }
      .top-nav {
        text-align: center; padding: 15px 0; background-color: #f8f9fa;
        border-bottom: 1px solid #dee2e6; width: 100%;
      }
      .top-nav a {
        margin: 0 15px; text-decoration: none; font-weight: bold;
        color: #007bff; font-size: 1.1em;
      }
      .top-nav a:hover { text-decoration: underline; }
      .page-container { padding: 20px; }
      pre {
        background-color: #f9f9f9; padding: 10px; overflow-x: auto;
        white-space: pre-wrap; word-break: break-all;
      }
      .error { color: red; }
      .success { color: green; }
      #jwt-form { margin-bottom: 20px; flex: 1; }
      fieldset {
        border: 1px solid #ddd; padding: 10px; margin: 0;
        min-width: 0; flex: 1; }
      legend { font-weight: bold; margin-bottom: 5px; }
      textarea { width: 100%; box-sizing: border-box; vertical-align: top; }
      button[type="submit"] { padding: 8px 15px; margin-top: 10px; }
      @media (min-width: 1024px) {
        .main-content { display: flex; gap: 20px; }
        .main-content > #jwt-form,
        .main-content > .result-container {
          flex-basis: 50%; /* Each item takes up 50% of the width */
          margin-bottom: 0; /* Remove bottom margin when side-by-side */
          display: flex; /* Make the result container a flex container */
          flex-direction: column; /* Stack children vertically */
          flex: 1; /* Allows the result container to grow and shrink */
        }
      }
    </style>
  </head>
  <body>
    <nav class="top-nav">
      <a href="static-page.html">One Tap Static Page</a>
      <a href="dynamic-page.html">One Tap Dynamic Page</a>
      <a href="prompt-outcomes.html">Prompt behaviors</a>
    </nav>
    <div class="page-container">
      <h1>JWT Verification</h1>

      <div class="main-content">
        <form id="jwt-form" action="/user-login" method="post">
          <fieldset>
            <legend>Encoded JWT ID Token</legend>
            <textarea id="credential" name="credential" rows="5"
                      cols="50"></textarea>
            <button type="submit">Verify JWT</button>
          </fieldset>
        </form>

        <section class="result-container">
          <fieldset>
            <legend>Decode and verify result</legend>
            <p id="status"></p>
            <pre id="result"></pre>
          </fieldset>
        </section>
      </div>
    </div>
    <script>
      const statusElement = document.getElementById("status");
      const resultElement = document.getElementById("result");

      const handleResponse = (responseData) => {
        const { status, message } = responseData;
        const result = message
          ? status === "success"
            ? JSON.stringify(message, null, 2)
            : message
          : "";

        statusElement.textContent = status;
        resultElement.textContent = result;
        statusElement.className = "";
        if (status === "success") {
          statusElement.classList.add("success");
        } else if (status === "error") {
          statusElement.classList.add("error");
        }
      };

      const getEncodedDataFromHash = (hash) => {
        const urlParams = new URLSearchParams(hash.substring(1));
        return urlParams.get("data");
      };

      const processHashData = (hash) => {
        const encodedData = getEncodedDataFromHash(hash);
        if (encodedData) {
          try {
            const jsonData = JSON.parse(decodeURIComponent(encodedData));
            handleResponse(jsonData);
            history.pushState(
              "",
              document.title,
              window.location.pathname + window.location.search,
            );
          } catch (error) {
            handleResponse({
              status: "error",
              message: "Error parsing data from URL: " + error,
            });
          }
        }
      };

      window.addEventListener("load",
                              () => processHashData(window.location.hash));
      window.addEventListener("hashchange",
                              () => processHashData(window.location.hash));
    </script>

    <script>
      document.addEventListener("DOMContentLoaded", () => {

        const generateRandomString = (length) => {
        const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
                      "abcdefghijklmnopqrstuvwxyz" +
                      "0123456789";
          let result = "";
          for (let i = 0; i < length; i++) {
            result += chars.charAt(Math.floor(Math.random() * chars.length));
          }
          return result;
        };

        const csrfToken = generateRandomString(12);
        document.cookie = `g_csrf_token=${csrfToken};path=/;SameSite=Lax`;
        const form = document.getElementById("jwt-form");
        const hiddenInput = document.createElement("input");
        hiddenInput.setAttribute("type", "hidden");
        hiddenInput.setAttribute("name", "g_csrf_token");
        hiddenInput.setAttribute("value", csrfToken);
        form.appendChild(hiddenInput);
      });
    </script>
  </body>
</html>

وب سرور و رمزگشایی JWT را آزمایش کنید

قبل از اینکه سعی کنیم و با One Tap کار کنیم، مطمئن خواهیم شد که محیط نقطه پایانی سرور راه‌اندازی و کار می‌کند.

به صفحه فرود http://localhost:3000/ بروید و دکمه Verify JWT را فشار دهید.

شما باید این را ببینید

JWT تأیید بدون محتوا

با فشار دادن دکمه، یک POST با محتوای فیلد ورودی به اسکریپت پایتون ارسال می شود. این اسکریپت انتظار دارد که یک JWT کدگذاری شده در قسمت ورودی وجود داشته باشد، بنابراین سعی می کند بارگذاری را رمزگشایی و تأیید کند. سپس برای نمایش نتایج به صفحه فرود هدایت می شود.

اما صبر کنید، JWT وجود نداشت... آیا این شکست نمی خورد؟ بله، اما با ظرافت!

از آنجایی که فیلد خالی بود یک خطا نمایش داده می شود. حالا سعی کنید متنی (هر متنی که می خواهید) را در قسمت ورودی وارد کنید و دوباره دکمه را فشار دهید. با یک خطای رمزگشایی متفاوت خراب می شود.

می‌توانید یک رمز JWT ID صادر شده توسط Google را در قسمت ورودی بچسبانید و اسکریپت پایتون را رمزگشایی کنید، تأیید کنید و آن را برای شما نمایش دهید... یا می‌توانید از https://jwt.io برای بررسی هر JWT کدگذاری شده استفاده کنید.

4. صفحه HTML ایستا

خوب، اکنون One Tap را برای کار بر روی صفحات HTML بدون استفاده از جاوا اسکریپت تنظیم می کنیم. این می تواند برای سایت های استاتیک یا برای سیستم های کش و CDN مفید باشد.

با افزودن این نمونه کد به فایلی به نام static-page.html شروع کنید:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://accounts.google.com/gsi/client" async></script>
    <link rel="icon" href="data:," />
  </head>
  <body>
    <h1>Google One Tap static HTML page</h1>
    <div
      id="g_id_onload"
      data-client_id="PUT_YOUR_WEB_CLIENT_ID_HERE"
      data-ux_mode="redirect"
      data-login_uri="http://localhost:3000/user-login"
    ></div>
  </body>
</html>

در مرحله بعد، static-page.html را باز کنید و PUT_YOUR_WEB_CLIENT_ID_HERE با شناسه مشتری که در لبه کد قبلی دکمه Sign In With Google استفاده کرده بودید، جایگزین کنید.

پس این چه کاری انجام می دهد؟

هر عنصر HTML با id g_id_onload و ویژگی‌های داده آن برای پیکربندی کتابخانه خدمات هویت Google ( gsi/client ) استفاده می‌شود. همچنین هنگامی که سند در مرورگر بارگذاری می شود، اعلان One Tap را نمایش می دهد.

ویژگی data-login_uri URI است که پس از ورود کاربر به سیستم، یک درخواست POST از مرورگر دریافت می‌کند. این درخواست حاوی JWT کدگذاری‌شده صادر شده توسط Google است.

برای لیست کامل گزینه های One Tap ، مولد کد HTML و مرجع HTML API را بررسی کنید.

وارد شوید

روی http://localhost:3000/static-page.html کلیک کنید.

باید اعلان One Tap را در مرورگر خود مشاهده کنید.

رابط کاربری One Tap

برای ورود به سیستم، Continue را فشار دهید.

پس از ورود به سیستم، گوگل یک درخواست POST به نقطه پایانی ورود به سیستم سرور پایتون شما ارسال می کند. این درخواست حاوی یک JWT کدگذاری شده است که توسط Google امضا شده است.

از آنجا، سرور از یکی از کلیدهای امضای عمومی Google برای تأیید اینکه Google JWT را ایجاد و امضا کرده است استفاده می کند. سپس رمزگشایی می کند و تأیید می کند که مخاطب با شناسه مشتری شما مطابقت دارد. در مرحله بعد، یک بررسی CSRF انجام می شود تا مطمئن شود که مقدار کوکی و مقدار پارامتر درخواست در بدنه POST برابر هستند. اگر آنها نباشند، نشانه ای از مشکل است.

در نهایت، صفحه فرود JWT تایید شده با موفقیت را به عنوان یک شناسه کاربری رمز ID با فرمت JSON نمایش می دهد.

JWT تایید شده

خطاهای رایج

راه های مختلفی برای شکست جریان ورود به سیستم وجود دارد. برخی از رایج ترین دلایل عبارتند از:

  • data-client_id وجود ندارد یا نادرست است، در این صورت یک خطا در کنسول DevTools خواهید دید و درخواست One Tap کار نخواهد کرد.
  • data-login_uri در دسترس نیست زیرا یک URI نادرست وارد شده است، وب سرور راه اندازی نشده است یا در پورت اشتباهی در حال گوش دادن است. اگر این اتفاق بیفتد، به نظر می‌رسد که درخواست One Tap کار می‌کند، اما هنگام بازگرداندن اعتبار، خطایی در تب شبکه DevTools قابل مشاهده است.
  • نام میزبان یا پورتی که سرور وب شما از آن استفاده می‌کند در منابع جاوا اسکریپت مجاز برای شناسه مشتری OAuth شما فهرست نشده است. یک پیام کنسول را خواهید دید: " هنگام واکشی نقطه پایانی ادعای شناسه، یک کد پاسخ HTTP 400 دریافت شد. " اگر این مورد را در طول این Codelab مشاهده کردید، بررسی کنید که هر دو http://localhost/ و http://localhost:3000 فهرست شده باشند.

5. صفحه پویا

اکنون One Tap را با استفاده از یک تماس جاوا اسکریپت نمایش خواهیم داد. در این مثال، ما همیشه وقتی صفحه بارگیری می‌شود، One Tap را نمایش می‌دهیم، اما شما می‌توانید انتخاب کنید که درخواست فقط در صورت نیاز نمایش داده شود. به عنوان مثال، ممکن است بررسی کنید که آیا جلسه کاربر بیش از 28 روز گذشته است و دوباره درخواست ورود به سیستم را نمایش دهید.

این نمونه کد را به فایلی با نام dynamic-page.html اضافه کنید.

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://accounts.google.com/gsi/client" async></script>
    <link rel="icon" href="data:," />
  </head>
  <body>
    <h1>Google One Tap dynamic page</h1>
    <script>
      const generateRandomString = (length) => {
        const array = new Uint8Array(length / 2);
        window.crypto.getRandomValues(array);
        return Array.from(array, (byte) =>
          byte.toString(16).padStart(2, "0")
        ).join("");
      };

      const setCookie = (name, value) => {
        document.cookie = '${name}=${value};path=/;SameSite=Lax';
      };

      const getCookie = (name) => {
        const nameEQ = name + "=";
        const ca = document.cookie.split(";");
        for (let i = 0; i < ca.length; i++) {
          let c = ca[i];
          while (c.charAt(0) == " ") c = c.substring(1, c.length);
          if (c.indexOf(nameEQ) == 0)
            return c.substring(nameEQ.length, c.length);
        }
        return null;
      };

      function handleResponse(rsp) {
        console.log("ID Token received from Google: ", rsp.credential);
        console.log("Submitting token to server via dynamic form POST.");

        const form = document.createElement("form");
        form.method = "POST";
        form.action = "http://" + window.location.host + "/user-login";

        // Add the credential and CSRF cookie value asa hidden fields
        const hiddenField = document.createElement("input");
        hiddenField.type = "hidden";
        hiddenField.name = "credential";
        hiddenField.value = rsp.credential;
        form.appendChild(hiddenField);

        const csrfToken = getCookie("g_csrf_token");
        if (csrfToken) {
          console.log("Found g_csrf_token cookie, adding to form.");
          const csrfField = document.createElement("input");
          csrfField.type = "hidden";
          csrfField.name = "g_csrf_token";
          csrfField.value = csrfToken;
          form.appendChild(csrfField);
        } else {
          console.warn(
            "Warning: g_csrf_token cookie not found. POSTing without it."
          );
        }

        document.body.appendChild(form);
        form.submit();
      }

      window.onload = function () {
        const csrfToken = generateRandomString(12);
        setCookie("g_csrf_token", csrfToken);
        console.log("CSRF token cookie set on page load:", csrfToken);

        google.accounts.id.initialize({
          client_id: "PUT_YOUR_WEB_CLIENT_ID_HERE",
          ux_mode: "popup",
          callback: handleResponse,
        });

        google.accounts.id.prompt(); // Display the One Tap prompt
      };
    </script>
  </body>
</html>

dynamic-page.html را باز کنید و PUT_YOUR_WEB_CLIENT_ID_HERE با شناسه مشتری که در لبه کد قبلی دکمه Sign In With Google استفاده کردید جایگزین کنید.

این کد ترکیبی از HTML و جاوا اسکریپت است و چندین کار را انجام می دهد:

  • کتابخانه خدمات هویت گوگل ( gsi/client ) را با فراخوانی google.accounts.id.initialize() پیکربندی می کند،
  • یک کوکی جعل درخواست بین سایتی ( CSRF ) ایجاد می کند،
  • یک کنترل کننده پاسخ به تماس را اضافه می کند تا JWT رمزگذاری شده را از Google دریافت کند و آن را با استفاده از فرم POST به اسکریپت پایتون ما /user-login ارسال کند، و
  • درخواست One Tap را با استفاده از google.accounts.id.prompt() نمایش می دهد.

فهرست کامل تنظیمات One Tap را می‌توانید در مرجع JavaScript API پیدا کنید.

اجازه می دهد وارد سیستم شوید!

http://localhost:3000/dynamic-page.html را در مرورگر خود باز کنید.

رفتار اعلان One Tap همانند سناریوی HTML ایستا است، با این تفاوت که این صفحه یک کنترل کننده پاسخ به تماس جاوا اسکریپت را برای ایجاد یک کوکی CSRF، دریافت JWT از Google و ارسال آن به نقطه پایانی user-login سرور پایتون تعریف می کند. HTML API این مراحل را به صورت خودکار برای شما انجام می دهد.

برگه Chrome Network

6. رفتارهای سریع

بنابراین اجازه دهید برخی از چیزها را با یک ضربه امتحان کنیم، زیرا برخلاف دکمه، اعلان همیشه نمایش داده نمی شود. می تواند توسط مرورگر و کاربر رد، بسته یا غیرفعال شود.

ابتدا این را در فایلی با نام prompt-outcomes.html ذخیره کنید:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Google One Tap Prompt behaviors</title>
    <style>
      body { font-family: sans-serif; padding: 20px; }
      #log {
        border: 1px solid #ccc; background-color: #f0f0f0;
        padding: 15px; margin-top: 20px;
        white-space: pre-wrap; font-family: monospace;
      }
      .success { color: green; }
      .error { color: red; }
      .info { color: blue; }
      .warning { color: orange; }
    </style>
  </head>
  <body>
    <h1>One Tap behaviors</h1>
    <p>Open the developer console to see detailed logs.</p>
    <div id="log">Awaiting events...</div>

    <script src="https://accounts.google.com/gsi/client" async defer></script>

    <script>
      // logging utility to display event and notification info
      const logElement = document.getElementById("log");
      function log(message, type = "info") {
        const timestamp = new Date().toLocaleTimeString();
        logElement.innerHTML +=
          `\n<span class="${type}">[${timestamp}] ${message}</span>`;
        console.log(`[${type.toUpperCase()}] ${message}`);
      }

      function decodeJwt(jwt) {
        try {
          const parts = jwt.split(".");
          if (parts.length !== 3) {
            throw new Error("Invalid JWT structure");
          }
          const base64Url = parts[1];
          const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
          const jsonPayload = decodeURIComponent(
            atob(base64)
              .split("")
              .map(function (c) {
                return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
              })
              .join(""),
          );
          return JSON.parse(jsonPayload);
        } catch (e) {
          log(`Error decoding JWT: ${e.message}`, "error");
          return null;
        }
      }

      /* Handles the credential response after a user signs in. */
      function handleCredentialResponse(credentialResponse) {
        log("Credential Response received.", "success");

        const credential = credentialResponse.credential;
        log(`Credential JWT: ${credential.substring(0, 30)}...`);

        // For demonstration, we decode the JWT on the client side.
        // REMEMBER: Always verify the token on your backend server!
        const payload = decodeJwt(credential);
        if (payload) {
          log(`Welcome, ${payload.name}! (Email: ${payload.email})`);
          log("Decoded JWT Payload: " + JSON.stringify(payload, null, 2));
        }
      }

      /* Handles notifications about the One-Tap prompt's UI status. */
      function handlePromptMomentNotification(notification) {
        log(`Prompt Moment Notification received.`, "info");

        if (notification.isNotDisplayed()) {
          const reason = notification.getNotDisplayedReason();
          log(`Prompt not displayed. Reason: <strong>${reason}</strong>`,
              "error");
        }
        if (notification.isSkippedMoment()) {
          const reason = notification.getSkippedReason();
          log(`Prompt was skipped. Reason: <strong>${reason}</strong>`,
              "warning");
          if (reason === "auto_cancel") {
            log("may have called prompt() multiple times in a row.");
          } else if (reason === "user_cancel") {
            log("The user manually closed the prompt.");
          }
        }
        if (notification.isDismissedMoment()) {
          const reason = notification.getDismissedReason();
          log(`Prompt dismissed. Reason: <strong>${reason}</strong>`, "info");
          if (reason === "credential_returned") {
            log("Expected, credential sent to the JS handler.");
          } else if (reason === "cancel_called") {
            log("programmatic call to google.accounts.id.cancel().");
          }
        }
      }

      window.onload = function () {
        try {
          google.accounts.id.initialize({
            client_id: "PUT_YOUR_WEB_CLIENT_ID_HERE",
            callback: handleCredentialResponse,
            ux_mode: "popup",
          });

          google.accounts.id.prompt(handlePromptMomentNotification);
          log("One Tap initialized. Waiting for prompt...");
        } catch (e) {
          log(`Initialization Error: ${e.message}`, "error");
        }
      };
    </script>
  </body>
</html>

سپس prompt-outcomes.html را باز کنید، PUT_YOUR_WEB_CLIENT_ID_HERE با شناسه مشتری خود جایگزین کنید و سپس فایل را ذخیره کنید.

در مرورگر خود، http://localhost:3000/prompt-outcomes.html را باز کنید

کلیک های صفحه

با کلیک کردن در هر نقطه خارج از اعلان One Tap شروع کنید. باید ببینید " درخواست لغو شد. " به صفحه و کنسول وارد شده است.

ورود به سیستم

بعد، فقط به طور معمول وارد سیستم شوید. گزارش‌ها و به‌روزرسانی‌های اعلان‌ها را می‌بینید که می‌توانند برای راه‌اندازی چیزی مانند ایجاد یا به‌روزرسانی یک جلسه کاربر استفاده شوند.

دستور را ببندید

اکنون صفحه را بارگیری مجدد کنید و پس از نمایش One Tap، روی "X" در نوار عنوان آن فشار دهید. این پیام باید در کنسول ثبت شود:

  • " کاربر درخواست را رد یا رد کرد. سرد شدن نمایی API فعال شد. "

در طول آزمایش، خنک شدن را فعال خواهید کرد. در طول دوره خنک شدن، اعلان One Tap نمایش داده نمی شود. در طول آزمایش، احتمالاً می خواهید به جای اینکه منتظر بمانید تا به طور خودکار تنظیم شود، آن را تنظیم مجدد کنید... مگر اینکه واقعاً بخواهید یک قهوه بخورید یا به خانه بروید و کمی بخوابید. برای تنظیم مجدد خنک کننده:

  • روی نماد "اطلاعات سایت" در سمت چپ نوار آدرس مرورگر کلیک کنید،
  • دکمه "Reset Permissions" را فشار دهید و
  • صفحه را بارگیری مجدد کنید

پس از تنظیم مجدد خنک کننده و بارگیری مجدد صفحه، اعلان One Tap نمایش داده می شود.

7. نتیجه گیری

بنابراین در این کد لبه چند چیز یاد گرفتید، مانند نحوه نمایش One Tap فقط با استفاده از HTML ایستا یا به صورت پویا با جاوا اسکریپت.

شما یک وب سرور بسیار ابتدایی پایتون را برای آزمایش محلی راه اندازی کردید و مراحل لازم برای رمزگشایی و اعتبار سنجی نشانه های شناسه را آموختید.

شما با متداول‌ترین روش‌هایی که کاربران با درخواست One Tap تعامل می‌کنند و آنها را رد می‌کنند بازی کرده‌اید و یک صفحه وب دارید که می‌تواند برای اشکال‌زدایی رفتار درخواست استفاده شود.

تبریک می گویم!

برای دریافت اعتبار بیشتر، سعی کنید به مرورگرهای مختلفی که از One Tap پشتیبانی می‌کند ، برگردید و از آن استفاده کنید.

این پیوندها ممکن است در مراحل بعدی به شما کمک کند:

سوالات متداول