1. Giới thiệu
Lớp học lập trình này dựa trên lớp học lập trình Nút Đăng nhập bằng Google cho web, vì vậy, hãy nhớ hoàn thành lớp học lập trình đó trước.
Trong lớp học lập trình này, bạn sẽ dùng thư viện JavaScript Dịch vụ nhận dạng của Google và lời nhắc Một lần chạm để thêm tính năng đăng nhập của người dùng vào các trang web tĩnh và động bằng cách sử dụng API HTML và JavaScript.
Chúng ta cũng sẽ thiết lập một điểm cuối đăng nhập phía máy chủ để xác minh mã thông báo JWT ID.
Kiến thức bạn sẽ học được
- Cách thiết lập điểm cuối đăng nhập phía máy chủ để xác minh mã nhận dạng
- Cách thêm lời nhắc Một lần chạm của Google vào trang web
- dưới dạng một phần tử HTML tĩnh và
- một cách linh hoạt bằng JavaScript.
- Cách hoạt động của lời nhắc Một lần chạm
Bạn cần có
- Kiến thức cơ bản về HTML, CSS, JavaScript và Chrome DevTools (hoặc công cụ tương đương).
- Một nơi để chỉnh sửa và lưu trữ các tệp HTML và JavaScript.
- Mã nhận dạng khách hàng thu được trong lớp học lập trình trước.
- Một môi trường có khả năng chạy một ứng dụng Python cơ bản.
Bắt đầu nào!
2. Thiết lập điểm cuối đăng nhập
Trước tiên, chúng ta sẽ tạo một tập lệnh Python đóng vai trò là máy chủ web cơ bản và thiết lập môi trường Python cần thiết để chạy tập lệnh đó.
Khi chạy cục bộ, tập lệnh sẽ phân phát trang đích, HTML tĩnh và các trang Đăng nhập một lần động cho trình duyệt. Nền tảng này chấp nhận các yêu cầu POST, giải mã JWT có trong tham số thông tin xác thực và xác thực rằng JWT đó do nền tảng OAuth của Google Identity phát hành.
Sau khi giải mã và xác minh JWT, tập lệnh sẽ chuyển hướng đến trang đích index.html
để hiển thị kết quả.
Sao chép nội dung này vào một tệp có tên 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()
Vì chúng ta sẽ xác minh JWT được phát hành cho ứng dụng khách của bạn bằng cách sử dụng trường đối tượng (aud) của mã thông báo nhận dạng, nên ứng dụng Python của bạn cần biết Mã ứng dụng khách nào đang được sử dụng. Để làm việc này, hãy thay thế PUT_YOUR_WEB_CLIENT_ID_HERE
bằng Client ID (Mã ứng dụng khách) mà bạn đã dùng trong lớp học lập trình trước đó về nút Đăng nhập bằng Google.
Môi trường Python
Hãy thiết lập môi trường để chạy tập lệnh máy chủ web.
Bạn sẽ cần Python 3.8 trở lên cùng với một số gói để hỗ trợ xác minh và giải mã JWT.
$ python3 --version
Nếu phiên bản python3 của bạn thấp hơn 3.8, bạn có thể cần thay đổi PATH của shell để tìm thấy phiên bản dự kiến hoặc cài đặt phiên bản Python mới hơn trên hệ thống của mình.
Tiếp theo, hãy tạo một tệp có tên requirements.txt
liệt kê các gói chúng ta cần để giải mã và xác minh JWT:
google-auth
Chạy các lệnh này trong cùng thư mục với simple-server.py
và requirements.txt
để tạo một môi trường ảo và chỉ cài đặt các gói cho ứng dụng này:
$ python3 -m venv env
$ source env/bin/activate
(env) $ pip install -r requirements.txt
Bây giờ, hãy khởi động máy chủ và nếu mọi thứ hoạt động bình thường, bạn sẽ thấy như sau:
(env) $ python3 ./simple-server.py
Serving HTTP on 0.0.0.0 port 3000 (http://0.0.0.0:3000/) ...
IDE trên đám mây
Lớp học lập trình này được thiết kế để chạy trong một thiết bị đầu cuối cục bộ và localhost, nhưng bạn có thể sử dụng lớp học lập trình này trên các nền tảng như Replit hoặc Glitch nếu có một số thay đổi. Mỗi nền tảng đều có các yêu cầu thiết lập riêng và chế độ mặc định của môi trường Python, vì vậy, bạn có thể cần thay đổi một số thứ như TARGET_HTML_PAGE_URL
và chế độ thiết lập Python.
Ví dụ: trên Glitch, bạn vẫn sẽ thêm requirements.txt
nhưng cũng tạo một tệp có tên start.sh
để tự động khởi động máy chủ Python:
python3 ./simple-server.py
Bạn cũng cần cập nhật URL mà tập lệnh Python và tệp HTML sử dụng thành URL bên ngoài của Cloud IDE. Vì vậy, chúng ta sẽ có một URL như sau: TARGET_HTML_PAGE_URL = f"https://your-project-name.glitch.me/"
và vì các tệp HTML trong lớp học lập trình này cũng mặc định sử dụng localhost, nên bạn sẽ cần cập nhật các tệp đó bằng URL Cloud IDE bên ngoài: data-login_uri="https://your-project-name.glitch.me/user-login"
.
3. Tạo trang đích
Tiếp theo, chúng ta sẽ tạo một trang đích hiển thị kết quả của việc đăng nhập bằng tính năng Đăng nhập bằng một lần nhấn. Trang này hiển thị mã thông báo nhận dạng JWT đã giải mã hoặc một lỗi. Bạn cũng có thể dùng một biểu mẫu trên trang để gửi JWT đến điểm cuối đăng nhập trên máy chủ HTTP Python của chúng tôi, nơi JWT được giải mã và xác minh. Ứng dụng này sử dụng cookie gửi hai lần CSRF và Tham số yêu cầu POST để có thể dùng lại cùng một điểm cuối máy chủ user-login
như các ví dụ về API HTML và JavaScript gsi/client
trong lớp học lập trình.
Trong thiết bị đầu cuối, hãy lưu nội dung này vào một tệp có tên là 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>
Kiểm thử máy chủ web và quá trình giải mã JWT
Trước khi thử sử dụng tính năng Đăng nhập bằng một lần chạm, chúng ta sẽ đảm bảo rằng môi trường điểm cuối của máy chủ đã được thiết lập và đang hoạt động.
Duyệt đến trang đích http://localhost:3000/ rồi nhấn vào nút Xác minh JWT.
Bạn sẽ thấy như sau
Khi nhấn nút này, một yêu cầu POST sẽ được gửi cùng với nội dung của entryfield đến tập lệnh Python. Tập lệnh này dự kiến sẽ có một JWT được mã hoá trong entryfield, vì vậy, tập lệnh sẽ cố gắng giải mã và xác minh tải trọng. Sau đó, trang này sẽ chuyển hướng trở lại trang đích để hiển thị kết quả.
Nhưng đợi đã, không có JWT nào cả... chẳng phải điều này sẽ thất bại sao? Có, nhưng phải thật khéo léo!
Vì trường này trống nên một lỗi sẽ xuất hiện. Bây giờ, hãy thử nhập một số văn bản (bất kỳ văn bản nào bạn muốn) vào trường nhập và nhấn lại vào nút. Thao tác này không thành công do gặp một lỗi giải mã khác.
Bạn có thể dán mã thông báo JWT do Google phát hành đã mã hoá vào trường nhập và để tập lệnh Python giải mã, xác minh và hiển thị mã thông báo đó cho bạn... hoặc bạn có thể sử dụng https://jwt.io để kiểm tra mọi JWT đã mã hoá.
4. Trang HTML tĩnh
OK, bây giờ chúng ta sẽ thiết lập tính năng Đăng nhập một lần để hoạt động trên các trang HTML mà không cần dùng JavaScript. Điều này có thể hữu ích cho các trang web tĩnh hoặc cho hệ thống lưu vào bộ nhớ đệm và CDN.
Bắt đầu bằng cách thêm đoạn mã mẫu này vào một tệp có tên 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>
Tiếp theo, hãy mở static-page.html
rồi thay thế PUT_YOUR_WEB_CLIENT_ID_HERE
bằng Mã ứng dụng khách mà bạn đã dùng trong lớp học lập trình trước về nút Đăng nhập bằng Google.
Vậy tính năng này có chức năng gì?
Mọi phần tử HTML có id
là g_id_onload
và thuộc tính dữ liệu của phần tử đó được dùng để định cấu hình thư viện Dịch vụ nhận dạng của Google (gsi/client
). Thư viện này cũng hiển thị lời nhắc Đăng nhập bằng một lần chạm khi tài liệu được tải trong trình duyệt.
Thuộc tính data-login_uri
là URI sẽ nhận được yêu cầu POST từ trình duyệt sau khi người dùng đăng nhập. Yêu cầu này chứa JWT được mã hoá do Google phát hành.
Hãy xem trình tạo mã HTML và tài liệu tham khảo về HTML API để biết danh sách đầy đủ các lựa chọn Đăng nhập bằng một lần nhấn.
Đăng nhập
Nhấp vào http://localhost:3000/static-page.html.
Bạn sẽ thấy lời nhắc Đăng nhập bằng một lần nhấn xuất hiện trong trình duyệt.
Nhấn vào Tiếp tục với tư cách để đăng nhập.
Sau khi đăng nhập, Google sẽ gửi một yêu cầu POST đến điểm cuối đăng nhập của máy chủ Python. Yêu cầu này chứa một JWT được mã hoá và do Google ký.
Từ đó, máy chủ sẽ sử dụng một trong các khoá ký công khai của Google để xác minh rằng Google đã tạo và ký JWT. Sau đó, hệ thống sẽ giải mã và xác minh xem đối tượng có khớp với Mã ứng dụng khách của bạn hay không. Tiếp theo, hệ thống sẽ kiểm tra CSRF để đảm bảo rằng giá trị Cookie và giá trị tham số yêu cầu trong phần nội dung POST bằng nhau. Nếu không, đó chắc chắn là dấu hiệu của vấn đề.
Cuối cùng, trang đích sẽ hiển thị JWT đã được xác minh thành công dưới dạng thông tin đăng nhập người dùng mã thông báo nhận dạng có định dạng JSON.
Lỗi phổ biến
Có một số trường hợp quy trình đăng nhập không thành công. Sau đây là một số lý do phổ biến nhất:
data-client_id
bị thiếu hoặc không chính xác. Trong trường hợp này, bạn sẽ thấy lỗi trong bảng điều khiển Công cụ cho nhà phát triển và lời nhắc Đăng nhập bằng một lần nhấn sẽ không hoạt động.data-login_uri
không hoạt động vì bạn đã nhập URI không chính xác, chưa khởi động máy chủ web hoặc máy chủ đang nghe trên cổng không chính xác. Nếu điều này xảy ra, lời nhắc Đăng nhập bằng một lần nhấn có vẻ hoạt động nhưng bạn sẽ thấy một lỗi trong thẻ mạng của Công cụ cho nhà phát triển khi thông tin đăng nhập được trả về.- Tên máy chủ hoặc cổng mà máy chủ web của bạn đang sử dụng không có trong phần Các nguồn gốc JavaScript được cho phép cho Mã ứng dụng khách OAuth. Bạn sẽ thấy một thông báo trên bảng điều khiển: "Khi tìm nạp điểm cuối xác nhận mã nhận dạng, hệ thống đã nhận được mã phản hồi HTTP 400.". Nếu bạn thấy thông báo này trong lớp học lập trình, hãy kiểm tra để đảm bảo cả
http://localhost/
vàhttp://localhost:3000
đều có trong danh sách.
5. Trang động
Giờ đây, chúng ta sẽ hiển thị tính năng Đăng nhập bằng một lần nhấn bằng lệnh gọi JavaScript. Trong ví dụ này, chúng ta sẽ luôn hiển thị tính năng Đăng nhập bằng một lần nhấn khi trang tải, nhưng bạn có thể chọn chỉ hiển thị lời nhắc khi cần. Ví dụ: bạn có thể kiểm tra xem phiên hoạt động của người dùng đã quá 28 ngày hay chưa và hiển thị lại lời nhắc đăng nhập.
Thêm đoạn mã mẫu này vào một tệp có tên là 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>
Mở dynamic-page.html
và thay thế PUT_YOUR_WEB_CLIENT_ID_HERE
bằng Mã ứng dụng khách mà bạn đã dùng trong lớp học lập trình trước về nút Đăng nhập bằng Google.
Mã này là sự kết hợp giữa HTML và JavaScript, có chức năng thực hiện một số việc sau:
- định cấu hình thư viện Dịch vụ nhận dạng của Google (
gsi/client
) bằng cách gọigoogle.accounts.id.initialize()
, - tạo cookie Giả mạo yêu cầu trên nhiều trang web (CSRF),
- thêm một trình xử lý lệnh gọi lại để nhận JWT được mã hoá từ Google và gửi JWT đó bằng phương thức POST của biểu mẫu đến điểm cuối
/user-login
của tập lệnh Python, và - hiển thị lời nhắc One Tap bằng cách sử dụng
google.accounts.id.prompt()
.
Bạn có thể xem danh sách đầy đủ các chế độ cài đặt của tính năng Đăng nhập một lần trong tài liệu tham khảo về JavaScript API.
Hãy đăng nhập!
Mở http://localhost:3000/dynamic-page.html trong trình duyệt.
Hành vi của lời nhắc One Tap giống như trong trường hợp HTML tĩnh, ngoại trừ việc trang này xác định một trình xử lý lệnh gọi lại JavaScript để tạo cookie CSRF, nhận JWT từ Google và POST cookie đó đến điểm cuối user-login
của máy chủ Python. HTML API sẽ tự động thực hiện các bước này cho bạn.
6. Hành vi của câu lệnh
Vì vậy, hãy thử một số thao tác với tính năng Đăng nhập một lần, vì không giống như nút, lời nhắc không phải lúc nào cũng xuất hiện. Trình duyệt và người dùng có thể loại bỏ, đóng hoặc tắt thông báo này.
Trước tiên, hãy lưu nội dung này vào một tệp có tên 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>
Tiếp theo, hãy mở prompt-outcomes.html
, thay thế PUT_YOUR_WEB_CLIENT_ID_HERE
bằng Mã ứng dụng khách rồi lưu tệp.
Trong trình duyệt, hãy mở http://localhost:3000/prompt-outcomes.html
Số lượt nhấp vào trang
Bắt đầu bằng cách nhấp vào vị trí bất kỳ bên ngoài lời nhắc Đăng nhập một lần. Bạn sẽ thấy thông báo "Yêu cầu đã bị huỷ." được ghi vào cả trang và bảng điều khiển.
Đăng nhập
Tiếp theo, bạn chỉ cần đăng nhập như bình thường. Bạn sẽ thấy các bản cập nhật về thông báo và nhật ký có thể dùng để kích hoạt một số hoạt động như thiết lập hoặc làm mới phiên người dùng.
Đóng lời nhắc
Bây giờ, hãy tải lại trang và sau khi thấy One Tap, hãy nhấn vào nút "X" trong thanh tiêu đề của One Tap. Thông báo này sẽ được ghi vào bảng điều khiển:
- "Người dùng từ chối hoặc đóng lời nhắc. Đã kích hoạt chế độ giảm tốc theo hàm mũ của API."
Trong quá trình kiểm thử, bạn sẽ kích hoạt quá trình hạ nhiệt. Lời nhắc Một lần chạm sẽ không xuất hiện trong thời gian chờ. Trong quá trình kiểm thử, có lẽ bạn sẽ muốn đặt lại thay vì chờ thiết bị tự động đặt lại... trừ phi bạn thực sự muốn đi uống cà phê hoặc về nhà ngủ một giấc. Cách đặt lại thời gian chờ:
- Nhấp vào biểu tượng "thông tin trang web" ở bên trái thanh địa chỉ của trình duyệt,
- nhấn vào nút "Đặt lại quyền" và
- tải lại trang.
Sau khi bạn đặt lại thời gian chờ và tải lại trang, lời nhắc Đăng nhập bằng một lần nhấn sẽ xuất hiện.
7. Kết luận
Vì vậy, trong lớp học lập trình này, bạn đã tìm hiểu một số điều, chẳng hạn như cách hiển thị tính năng Đăng nhập bằng một lần nhấn chỉ bằng HTML tĩnh hoặc động bằng JavaScript.
Bạn thiết lập một máy chủ web Python rất cơ bản để kiểm thử cục bộ và tìm hiểu các bước cần thiết để giải mã và xác thực mã thông báo nhận dạng.
Bạn đã thử những cách phổ biến nhất mà người dùng tương tác và đóng lời nhắc Một lần chạm, đồng thời có một trang web có thể dùng để gỡ lỗi hành vi của lời nhắc.
Xin chúc mừng!
Để có thêm điểm, hãy thử quay lại và sử dụng tính năng Đăng nhập một lần trong các trình duyệt mà tính năng này hỗ trợ.
Các đường liên kết này có thể giúp bạn thực hiện các bước tiếp theo:
- HTML API của Dịch vụ nhận dạng của Google
- API JavaScript của Dịch vụ nhận dạng của Google
- Cách Thiết lập tính năng Đăng nhập bằng Google cho web
- Xác minh mã thông báo nhận dạng của Google
- Tìm hiểu thêm về Dự án trên Google Cloud
- Phương thức xác thực danh tính trên Google