1. บทนำ
Codelab นี้สร้างขึ้นจาก Codelab ปุ่มลงชื่อเข้าใช้ด้วย Google สำหรับเว็บ ดังนั้นโปรดทำ Codelab ดังกล่าวก่อน
ในโค้ดแล็บนี้ คุณจะได้ใช้ไลบรารี JavaScript ของ Google Identity Services และข้อความแจ้ง One Tap เพื่อเพิ่มการลงชื่อเข้าใช้ของผู้ใช้ในหน้าเว็บแบบคงที่และแบบไดนามิกโดยใช้ HTML และ JavaScript API
นอกจากนี้ เราจะตั้งค่าปลายทางการเข้าสู่ระบบฝั่งเซิร์ฟเวอร์เพื่อยืนยันโทเค็นรหัส JWT ด้วย
สิ่งที่คุณจะได้เรียนรู้
- วิธีตั้งค่าปลายทางการเข้าสู่ระบบฝั่งเซิร์ฟเวอร์เพื่อยืนยันโทเค็นรหัส
- วิธีเพิ่มข้อความแจ้งให้ลงชื่อเข้าใช้ด้วย Google Tap ลงในหน้าเว็บ
- เป็นองค์ประกอบ HTML แบบคงที่ และ
- แบบไดนามิกโดยใช้ JavaScript
- ลักษณะการทำงานของข้อความแจ้ง One Tap
สิ่งที่คุณต้องมี
- มีความรู้พื้นฐานเกี่ยวกับ HTML, CSS, JavaScript และ Chrome DevTools (หรือเทียบเท่า)
- พื้นที่สำหรับแก้ไขและโฮสต์ไฟล์ HTML และ JavaScript
- รหัสไคลเอ็นต์ที่ได้รับในโค้ดแล็บก่อนหน้า
- สภาพแวดล้อมที่เรียกใช้แอป Python พื้นฐานได้
เริ่มเลย
2. ตั้งค่าปลายทางการเข้าสู่ระบบ
ก่อนอื่น เราจะสร้างสคริปต์ Python ที่ทำหน้าที่เป็นเว็บเซิร์ฟเวอร์พื้นฐานและตั้งค่าสภาพแวดล้อม Python ที่จำเป็นต่อการเรียกใช้
สคริปต์ที่ทำงานในเครื่องจะแสดงหน้า Landing Page และ HTML แบบคงที่ รวมถึงหน้า One Tap แบบไดนามิกต่อเบราว์เซอร์ โดยจะยอมรับคำขอ POST, ถอดรหัส JWT ที่อยู่ในพารามิเตอร์ข้อมูลเข้าสู่ระบบ และตรวจสอบว่าออกโดยแพลตฟอร์ม OAuth ของ Google Identity
หลังจากถอดรหัสและยืนยัน JWT แล้ว สคริปต์จะเปลี่ยนเส้นทางไปยังหน้า Landing Page 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) ของโทเค็นรหัส แอป Python ของคุณจึงต้องทราบว่ามีการใช้รหัสไคลเอ็นต์ใด โดยให้แทนที่ PUT_YOUR_WEB_CLIENT_ID_HERE
ด้วยรหัสไคลเอ็นต์ที่คุณใช้ใน Codelab ปุ่มลงชื่อเข้าใช้ด้วย Google ก่อนหน้า
สภาพแวดล้อม Python
มาตั้งค่าสภาพแวดล้อมเพื่อเรียกใช้สคริปต์เว็บเซิร์ฟเวอร์กัน
คุณจะต้องมี Python 3.8 ขึ้นไปพร้อมกับแพ็กเกจ 2-3 รายการเพื่อช่วยในการยืนยันและถอดรหัส JWT
$ 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 ในระบบคลาวด์
Codelab นี้ออกแบบมาให้ทำงานในเทอร์มินัลในเครื่องและ localhost แต่คุณอาจใช้ Codelab นี้ในแพลตฟอร์มอย่าง Replit หรือ Glitch ได้โดยทำการเปลี่ยนแปลงบางอย่าง แต่ละแพลตฟอร์มมีข้อกำหนดในการตั้งค่าและค่าเริ่มต้นของสภาพแวดล้อม Python ของตัวเอง ดังนั้นคุณอาจต้องเปลี่ยนบางอย่าง เช่น TARGET_HTML_PAGE_URL
และการตั้งค่า Python
เช่น ใน Glitch คุณยังคงเพิ่ม requirements.txt
ได้ แต่ก็ต้องสร้างไฟล์ชื่อ start.sh
เพื่อเริ่มเซิร์ฟเวอร์ Python โดยอัตโนมัติด้วย
python3 ./simple-server.py
นอกจากนี้ คุณยังต้องอัปเดต URL ที่ใช้โดยสคริปต์ Python และไฟล์ HTML เป็น URL ภายนอกของ Cloud IDE ด้วย ดังนั้นเราจึงมีลักษณะดังนี้ TARGET_HTML_PAGE_URL = f"https://your-project-name.glitch.me/"
และเนื่องจากไฟล์ HTML ใน Codelab นี้จะใช้ localhost เป็นค่าเริ่มต้น คุณจึงต้องอัปเดตไฟล์ด้วย URL ของ Cloud IDE ภายนอก: data-login_uri="https://your-project-name.glitch.me/user-login"
3. สร้างหน้า Landing Page
จากนั้นเราจะสร้างหน้า Landing Page ที่แสดงผลลัพธ์ของการลงชื่อเข้าใช้ด้วย One Tap หน้านี้จะแสดงโทเค็นรหัส JWT ที่ถอดรหัสแล้วหรือข้อผิดพลาด นอกจากนี้ คุณยังใช้แบบฟอร์มในหน้าเว็บเพื่อส่ง JWT ไปยังปลายทางการเข้าสู่ระบบในเซิร์ฟเวอร์ HTTP ของ Python ซึ่งจะมีการถอดรหัสและยืนยันได้ด้วย โดยใช้คุกกี้ CSRF แบบส่ง 2 ครั้งและพารามิเตอร์คำขอ POST เพื่อให้สามารถนำuser-login
ปลายทางของเซิร์ฟเวอร์เดียวกันกลับมาใช้ใหม่ได้เหมือนกับตัวอย่าง HTML และ JavaScript API ใน Codelabgsi/client
ในเทอร์มินัล ให้บันทึกข้อมูลนี้ในไฟล์ชื่อ 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 เราจะตรวจสอบว่าสภาพแวดล้อมของปลายทางเซิร์ฟเวอร์ได้รับการตั้งค่าและใช้งานได้
ไปที่หน้า Landing Page http://localhost:3000/ แล้วกดปุ่มยืนยัน JWT
คุณควรเห็นสิ่งนี้
การกดปุ่มจะส่ง POST พร้อมเนื้อหาของช่องป้อนข้อมูลไปยังสคริปต์ Python สคริปต์คาดหวังว่าจะมี JWT ที่เข้ารหัสอยู่ในช่องรายการ จึงพยายามถอดรหัสและยืนยันเพย์โหลด หลังจากนั้นระบบจะเปลี่ยนเส้นทางกลับไปยังหน้า Landing Page เพื่อแสดงผลลัพธ์
แต่เดี๋ยวก่อน ไม่มี JWT นี่นา... แล้วทำไมถึงไม่ล้มเหลวล่ะ ใช่ แต่ต้องอย่างสง่างาม
เนื่องจากช่องว่าง ระบบจึงแสดงข้อผิดพลาด ตอนนี้ลองป้อนข้อความ (ข้อความใดก็ได้ที่คุณต้องการ) ในช่องป้อนข้อมูล แล้วกดปุ่มอีกครั้ง แต่จะล้มเหลวเนื่องจากข้อผิดพลาดในการถอดรหัสอื่น
คุณสามารถวางโทเค็นรหัส JWT ที่เข้ารหัสซึ่ง Google ออกให้ในช่องรายการ และให้สคริปต์ Python ถอดรหัส ยืนยัน และแสดงโทเค็นดังกล่าว หรือจะใช้ https://jwt.io เพื่อตรวจสอบ JWT ที่เข้ารหัสก็ได้
4. หน้า HTML แบบคงที่
ตอนนี้เราจะตั้งค่า One Tap ให้ทำงานในหน้า HTML โดยไม่ต้องใช้ JavaScript ซึ่งอาจเป็นประโยชน์สำหรับเว็บไซต์แบบคงที่หรือสำหรับระบบแคชและ 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
ด้วยรหัสไคลเอ็นต์ที่คุณใช้ในโค้ดแล็บปุ่มลงชื่อเข้าใช้ด้วย Google ก่อนหน้า
แล้วการดำเนินการนี้มีผลอย่างไร
ระบบจะใช้องค์ประกอบ HTML ที่มี id
เป็น g_id_onload
และแอตทริบิวต์ข้อมูลเพื่อกำหนดค่าไลบรารีบริการระบุตัวตนของ Google (gsi/client
) และจะแสดงข้อความแจ้งให้ลงชื่อเข้าใช้ด้วยแตะครั้งเดียวเมื่อโหลดเอกสารในเบราว์เซอร์
แอตทริบิวต์ data-login_uri
คือ URI ที่จะรับคำขอ POST จากเบราว์เซอร์หลังจากที่ผู้ใช้ลงชื่อเข้าใช้ คำขอนี้มี JWT ที่เข้ารหัสซึ่งออกโดย Google
ดูรายการตัวเลือกการแตะครั้งเดียวทั้งหมดได้ในเครื่องมือสร้างโค้ด HTML และข้อมูลอ้างอิง HTML API
ลงชื่อเข้าใช้
คลิก http://localhost:3000/static-page.html
คุณควรเห็นข้อความแจ้งการแตะครั้งเดียวแสดงในเบราว์เซอร์
กดดำเนินการต่อในชื่อเพื่อลงชื่อเข้าใช้
หลังจากลงชื่อเข้าใช้แล้ว Google จะส่งคำขอ POST ไปยังปลายทางการเข้าสู่ระบบของเซิร์ฟเวอร์ Python คำขอมี JWT ที่เข้ารหัสซึ่ง Google ลงนาม
จากนั้นเซิร์ฟเวอร์จะใช้คีย์การลงนามสาธารณะของ Google รายการใดรายการหนึ่งเพื่อยืนยันว่า Google เป็นผู้สร้างและลงนามใน JWT จากนั้นจะถอดรหัสและยืนยันว่ากลุ่มเป้าหมายตรงกับ Client ID ของคุณ จากนั้นจะมีการตรวจสอบ CSRF เพื่อให้แน่ใจว่าค่าคุกกี้และค่าพารามิเตอร์คำขอในเนื้อหาของ POST เท่ากัน หากไม่เป็นเช่นนั้น แสดงว่ามีปัญหาแน่นอน
สุดท้าย หน้า Landing Page จะแสดง JWT ที่ยืนยันเรียบร้อยแล้วเป็นข้อมูลเข้าสู่ระบบของผู้ใช้โทเค็นรหัสที่จัดรูปแบบ JSON
ข้อผิดพลาดที่พบบ่อย
ขั้นตอนการลงชื่อเข้าใช้ล้มเหลวได้หลายวิธี สาเหตุที่พบบ่อยที่สุดมีดังนี้
data-client_id
ขาดหายไปหรือไม่ถูกต้อง ในกรณีนี้ คุณจะเห็นข้อผิดพลาดในคอนโซล DevTools และข้อความแจ้งการแตะหนึ่งครั้งจะไม่ทํางานdata-login_uri
ไม่พร้อมใช้งานเนื่องจากป้อน URI ไม่ถูกต้อง ไม่ได้เริ่มเว็บเซิร์ฟเวอร์ หรือกำลังฟังในพอร์ตที่ไม่ถูกต้อง หากเกิดกรณีนี้ขึ้น ข้อความแจ้งการแตะครั้งเดียวจะดูเหมือนใช้งานได้ แต่ข้อผิดพลาดจะปรากฏในแท็บเครือข่ายของ DevTools เมื่อมีการส่งคืนข้อมูลเข้าสู่ระบบ- ชื่อโฮสต์หรือพอร์ตที่เว็บเซิร์ฟเวอร์ใช้ไม่ได้อยู่ในต้นทาง JavaScript ที่ได้รับอนุญาตสำหรับรหัสไคลเอ็นต์ OAuth คุณจะเห็นข้อความในคอนโซลว่า "เมื่อดึงข้อมูลปลายทางการยืนยันรหัส ระบบได้รับรหัสการตอบกลับ HTTP 400" หากคุณเห็นข้อความนี้ในระหว่างการทำ Codelab นี้ ให้ตรวจสอบว่ามีทั้ง
http://localhost/
และhttp://localhost:3000
แสดงอยู่
5. หน้าเว็บแบบไดนามิก
ตอนนี้เราจะแสดง One Tap โดยใช้การเรียก JavaScript ในตัวอย่างนี้ เราจะแสดง 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
ด้วยรหัสไคลเอ็นต์ที่คุณใช้ใน Codelab ปุ่มลงชื่อเข้าใช้ด้วย Google ก่อนหน้า
โค้ดนี้เป็นการผสมผสานระหว่าง HTML และ JavaScript ซึ่งทำสิ่งต่างๆ ดังนี้
- กำหนดค่าไลบรารีบริการระบุตัวตนของ Google (
gsi/client
) โดยการเรียกใช้google.accounts.id.initialize()
- สร้างคุกกี้การปลอมแปลง Cross-site Request (CSRF)
- เพิ่มตัวแฮนเดิลการเรียกกลับเพื่อรับ JWT ที่เข้ารหัสจาก Google และส่งโดยใช้ POST แบบฟอร์มไปยังปลายทางสคริปต์ Python
/user-login
ของเรา และ - แสดงข้อความแจ้งการแตะครั้งเดียวโดยใช้
google.accounts.id.prompt()
ดูรายการการตั้งค่า One Tap ทั้งหมดได้ในเอกสารอ้างอิง JavaScript API
มาลงชื่อเข้าใช้กันเลย
เปิด http://localhost:3000/dynamic-page.html ในเบราว์เซอร์
ลักษณะการทำงานของข้อความแจ้ง One Tap จะเหมือนกับสถานการณ์ HTML แบบคงที่ ยกเว้นว่าหน้านี้จะกำหนดตัวแฮนเดิลการเรียกกลับของ JavaScript เพื่อสร้างคุกกี้ CSRF รับ JWT จาก Google และ POST ไปยังปลายทาง user-login
ของเซิร์ฟเวอร์ Python HTML API จะดำเนินการตามขั้นตอนเหล่านี้ให้คุณโดยอัตโนมัติ
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
ด้วย Client ID แล้วบันทึกไฟล์
เปิด http://localhost:3000/prompt-outcomes.html ในเบราว์เซอร์
การคลิกหน้าเว็บ
เริ่มต้นโดยคลิกที่ใดก็ได้นอกพรอมต์การแตะครั้งเดียว คุณควรเห็นข้อความ "ระบบยกเลิกคำขอแล้ว" ที่บันทึกไว้ทั้งในหน้าเว็บและคอนโซล
ลงชื่อเข้าใช้
จากนั้นก็ลงชื่อเข้าใช้ตามปกติ คุณจะเห็นการอัปเดตการบันทึกและการแจ้งเตือนที่ใช้เพื่อทริกเกอร์สิ่งต่างๆ เช่น การสร้างหรือรีเฟรชเซสชันของผู้ใช้
ปิดข้อความแจ้ง
ตอนนี้ให้โหลดหน้าเว็บอีกครั้ง แล้วหลังจากที่ระบบแสดง One Tap ให้กด "X" ในแถบชื่อของ One Tap ระบบควรบันทึกข้อความนี้ลงในคอนโซล
- "ผู้ใช้ปฏิเสธหรือปิดข้อความแจ้ง ระบบทริกเกอร์การหยุดพักแบบเลขชี้กำลังของ API"
ในระหว่างการทดสอบ คุณจะทริกเกอร์การลดอุณหภูมิ ในระหว่างระยะเวลาพัก ระบบจะไม่แสดงข้อความแจ้งแบบแตะครั้งเดียว ในระหว่างการทดสอบ คุณอาจต้องการรีเซ็ตแทนที่จะรอให้รีเซ็ตโดยอัตโนมัติ เว้นแต่คุณจะอยากไปดื่มกาแฟหรือกลับบ้านไปนอนจริงๆ วิธีรีเซ็ตระยะเวลาคูลดาวน์
- คลิกไอคอน "ข้อมูลเว็บไซต์" ทางด้านซ้ายของแถบที่อยู่ของเบราว์เซอร์
- กดปุ่ม "รีเซ็ตสิทธิ์" และ
- โหลดหน้าเว็บซ้ำ
หลังจากรีเซ็ตระยะเวลาพักและโหลดหน้าเว็บซ้ำแล้ว ระบบจะแสดงข้อความแจ้งแบบแตะครั้งเดียว
7. บทสรุป
ดังนั้นใน Codelab นี้ คุณได้เรียนรู้สิ่งต่างๆ เช่น วิธีแสดงการแตะครั้งเดียวโดยใช้เพียง HTML แบบคงที่หรือแบบไดนามิกด้วย JavaScript
คุณตั้งค่าเว็บเซิร์ฟเวอร์ Python พื้นฐานมากสำหรับการทดสอบในเครื่อง และได้เรียนรู้ขั้นตอนที่จำเป็นในการถอดรหัสและตรวจสอบโทเค็นรหัส
คุณได้ทดลองวิธีที่พบบ่อยที่สุดที่ผู้ใช้โต้ตอบและปิดข้อความแจ้งแบบแตะครั้งเดียว รวมถึงมีหน้าเว็บที่ใช้ในการแก้ไขข้อบกพร่องของลักษณะการทำงานของข้อความแจ้ง
ยินดีด้วย
หากต้องการคะแนนพิเศษ ให้ลองย้อนกลับไปและใช้การแตะครั้งเดียวในเบราว์เซอร์ต่างๆ ที่รองรับ
ลิงก์ต่อไปนี้อาจช่วยคุณในขั้นตอนถัดไป
- HTML API ของบริการระบุตัวตนของ Google
- JavaScript API ของบริการระบุตัวตนของ Google
- วิธีตั้งค่าการลงชื่อเข้าใช้ด้วย Google สำหรับเว็บ
- ยืนยันโทเค็นรหัส Google
- ดูข้อมูลเพิ่มเติมเกี่ยวกับโปรเจ็กต์ Google Cloud
- วิธีการตรวจสอบสิทธิ์ของ Google Identity