国产av日韩一区二区三区精品,成人性爱视频在线观看,国产,欧美,日韩,一区,www.成色av久久成人,2222eeee成人天堂

首頁 web前端 js教程 單點登錄 (SSO):React 和 ExpressJS 綜合指南

單點登錄 (SSO):React 和 ExpressJS 綜合指南

Jan 06, 2025 am 01:17 AM

單點登錄 (SSO) 是一種身份驗證機制,允許用戶登錄一次并訪問多個連接的應(yīng)用程序或系統(tǒng),而無需對每個應(yīng)用程序或系統(tǒng)進行重新身份驗證。 SSO 將用戶身份驗證集中到單個受信任的系統(tǒng)(通常稱為身份提供商或 IdP)中,然后該系統(tǒng)管理憑據(jù)并頒發(fā)令牌或會話數(shù)據(jù)以跨其他服務(wù)(稱為服務(wù)提供商或 SP)驗證用戶的身份。 ??>

在本指南中,我們將探討 SSO 的工作原理、其優(yōu)點和缺點、常見用例以及 API(帶有 Express 的 Node.js)、主應(yīng)用程序 (React) 和外部應(yīng)用程序中 SSO 實現(xiàn)的示例應(yīng)用程序(反應(yīng))。通過了解 SSO 的原理和實踐,組織可以增強其應(yīng)用程序和系統(tǒng)的用戶體驗、安全性和運營效率。

目錄

    單點登錄 (SSO)
    • SSO 如何工作?
    • SSO 的好處
    • SSO 的缺點
    • SSO 用例
    • SSO 實施示例
    • 1. API(帶有 Express 的 Node.js)
    • 2.主要應(yīng)用程序(React)
    • 3.外部應(yīng)用程序(React)
  • 結(jié)論
鏈接

    GitHub 存儲庫
演示視頻

Single Sign-On (SSO): A Comprehensive Guide with React and ExpressJS

單點登錄 (SSO)

單點登錄 (SSO) 是一種身份驗證機制,允許用戶登錄一次即可訪問多個連接的應(yīng)用程序或系統(tǒng),而無需對每個應(yīng)用程序或系統(tǒng)進行重新身份驗證。

SSO 將用戶身份驗證集中到單個受信任的系統(tǒng)(通常稱為身份提供商或 IdP)中,然后該系統(tǒng)管理憑據(jù)并頒發(fā)令牌或會話數(shù)據(jù),以跨其他服務(wù)(稱為服務(wù)提供商或 SP)驗證用戶身份).

單點登錄如何工作?

SSO 通過基于安全令牌的機制(例如 OAuth 2.0、OpenID Connect (OIDC) 或安全斷言標(biāo)記語言 (SAML))進行操作。這是一個簡化的流程:

  • 用戶登錄:用戶在身份提供商 (IdP) 中輸入其憑據(jù)。

  • 令牌頒發(fā):IdP 驗證憑據(jù)并頒發(fā)身份驗證令牌(例如 JWT 或 SAML 斷言)。

  • 服務(wù)訪問:令牌被傳遞給服務(wù)提供商,服務(wù)提供商對其進行驗證并授予訪問權(quán)限,而無需進一步登錄。

單點登錄的好處

  • 增強的用戶體驗:用戶只需一次登錄即可訪問多項服務(wù),減少摩擦并提高可用性。

  • 提高安全性

    • 減少密碼疲勞,避免密碼重復(fù)使用等不安全行為。
    • 集中式身份驗證可實現(xiàn)更強大的密碼策略并實施多重身份驗證 (MFA)。
  • 簡化的用戶管理

    • 管理員可以更輕松地管理跨連接應(yīng)用程序的用戶訪問。
    • 從 IdP 撤銷對用戶的訪問權(quán)限會禁用他們對所有集成系統(tǒng)的訪問權(quán)限。
  • 時間和成本效率

    • 通過減少與登錄相關(guān)的幫助臺請求,為用戶和支持團隊節(jié)省時間。
    • 通過利用現(xiàn)有的身份驗證機制減少開發(fā)時間和成本。
  • 合規(guī)與審核

    • 集中式身份驗證和訪問控制使執(zhí)行安全策略和跟蹤用戶活動變得更加容易。

單點登錄的缺點

  • 單點故障

    • 如果 IdP 不可用或受到威脅,用戶將無法訪問任何連接的系統(tǒng)。
    • 緩解措施:使用冗余 IdP 并確保高可用性。
  • 復(fù)雜的實現(xiàn)

    • 集成 SSO 需要大量的規(guī)劃和專業(yè)知識,尤其是在具有不同應(yīng)用程序和協(xié)議的環(huán)境中。
    • 緩解措施:利用 OAuth 2.0 或 SAML 等既定協(xié)議以及強大的 SSO 庫。
  • 安全風(fēng)險

    • 如果攻擊者獲得對用戶 SSO 憑據(jù)的訪問權(quán)限,他們就有可能訪問所有連接的系統(tǒng)。
    • 緩解措施:實施強大的 MFA 并監(jiān)控可疑的登錄活動。
  • 供應(yīng)商鎖定

    • 組織可能嚴(yán)重依賴特定的 IdP 供應(yīng)商,這使得遷移充滿挑戰(zhàn)。
    • 緩解措施:選擇開放標(biāo)準(zhǔn)并避免專有解決方案。
  • 代幣管理挑戰(zhàn)

    • 過期或被盜的令牌可能會中斷訪問或造成安全漏洞。
    • 緩解措施:實施令牌過期、刷新機制和安全令牌存儲。

SSO 用例

  • 企業(yè)應(yīng)用

    • 員工只需登錄即可訪問各種內(nèi)部工具和服務(wù)。
    • 簡化入職和離職流程。
  • 云服務(wù)

    • 用戶可以在云應(yīng)用之間無縫切換,無需重復(fù)登錄。
    • 提高生產(chǎn)力和用戶體驗。
  • 客戶門戶

    • 為不同服務(wù)的客戶提供統(tǒng)一的登錄體驗。
    • 實現(xiàn)個性化和有針對性的營銷。
  • 合作伙伴集成

    • 促進對合作伙伴組織之間共享資源的安全訪問。
    • 簡化協(xié)作和數(shù)據(jù)交換。

SSO 實施示例

1. API(Node.js 和 Express)

API 充當(dāng)身份提供商 (IdP)。它對用戶進行身份驗證并頒發(fā) JWT 令牌進行訪問。

下面是所提供代碼的結(jié)構(gòu)化細(xì)分,為您的關(guān)注者解釋了每個部分的目的。這是如何在 API 層實現(xiàn) SSO 功能的可靠示例。

設(shè)置和依賴關(guān)系

此設(shè)置中使用了以下軟件包:

  • express:用于處理 HTTP 請求和路由。
  • jsonwebtoken:用于生成和驗證 JWT。
  • cors:用于處理來自不同客戶端應(yīng)用程序的跨源請求。
  • @faker-js/faker:用于生成模擬用戶和待辦事項數(shù)據(jù)。
  • cookie-parser:用于解析請求中發(fā)送的cookie。
  • dotenv:用于安全地加載環(huán)境變量。
配置
  • dotenv 用于安全地管理密鑰。
  • 為開發(fā)環(huán)境提供了后備秘密。
dotenv.config();
const SECRET_KEY = process.env.SECRET_KEY || "secret";
中間件
  • CORS 確保允許來自特定前端來源(主應(yīng)用程序和外部應(yīng)用程序)的請求。
  • cookieParser 解析客戶端發(fā)送的 cookie。
  • express.json 允許解析 JSON 請求主體。
app.use(
  cors({
    origin: ["http://localhost:5173", "http://localhost:5174"],
    credentials: true,
  })
);
app.use(express.json());
app.use(cookieParser());

用戶身份驗證和令牌生成

模擬數(shù)據(jù)模擬用戶及其關(guān)聯(lián)的待辦事項。

用戶擁有角色(管理員或用戶)和基本個人資料信息。
待辦事項鏈接到用戶 ID 以進行個性化訪問。

  • /login:根據(jù)電子郵件和密碼對用戶進行身份驗證。

用戶成功登錄后會收到包含 JWT 的 cookie (sso_token)。
該令牌是安全的、僅限 HTTP 且有時間限制以防止篡改。

app.post("/login", (req, res) => {
  const { email, password } = req.body;
  const user = users.find(
    (user) => user.email === email && user.password === password
  );

  if (user) {
    const token = jwt.sign({ user }, SECRET_KEY, { expiresIn: "1h" });
    res.cookie("sso_token", token, {
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      maxAge: 3600000,
      sameSite: "strict",
    });
    res.json({ message: "Login successful" });
  } else {
    res.status(400).json({ error: "Invalid credentials" });
  }
});
  • /verify:通過解碼令牌來驗證用戶的身份。無效令牌會導(dǎo)致未經(jīng)授權(quán)的響應(yīng)。
app.get("/verify", (req, res) => {
  const token = req.cookies.sso_token;

  if (!token) {
    return res.status(401).json({ authenticated: false });
  }

  try {
    const decoded = jwt.verify(token, SECRET_KEY);
    res.json({ authenticated: true, user: decoded });
  } catch {
    res.status(401).json({ authenticated: false, error: "Invalid token" });
  }
});
  • /logout:清除包含 JWT 令牌的 cookie。

確保用戶可以通過清除令牌來安全注銷。

dotenv.config();
const SECRET_KEY = process.env.SECRET_KEY || "secret";
  • /todos:檢索與經(jīng)過身份驗證的用戶關(guān)聯(lián)的待辦事項。
app.use(
  cors({
    origin: ["http://localhost:5173", "http://localhost:5174"],
    credentials: true,
  })
);
app.use(express.json());
app.use(cookieParser());
  • /todos:為經(jīng)過身份驗證的用戶添加新的待辦事項。
app.post("/login", (req, res) => {
  const { email, password } = req.body;
  const user = users.find(
    (user) => user.email === email && user.password === password
  );

  if (user) {
    const token = jwt.sign({ user }, SECRET_KEY, { expiresIn: "1h" });
    res.cookie("sso_token", token, {
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      maxAge: 3600000,
      sameSite: "strict",
    });
    res.json({ message: "Login successful" });
  } else {
    res.status(400).json({ error: "Invalid credentials" });
  }
});
  • /todos/:id:根據(jù)提供的 ID 更新待辦事項。
app.get("/verify", (req, res) => {
  const token = req.cookies.sso_token;

  if (!token) {
    return res.status(401).json({ authenticated: false });
  }

  try {
    const decoded = jwt.verify(token, SECRET_KEY);
    res.json({ authenticated: true, user: decoded });
  } catch {
    res.status(401).json({ authenticated: false, error: "Invalid token" });
  }
});
  • /todos/:id:根據(jù)提供的 ID 刪除待辦事項。
app.post("/logout", (req, res) => {
  res.clearCookie("sso_token");
  res.json({ message: "Logout successful" });
});

2. 主要應(yīng)用程序(React)

主應(yīng)用程序充當(dāng)服務(wù)提供商 (SP),使用 API 并管理用戶交互。

下面是所提供代碼的結(jié)構(gòu)化細(xì)分,為您的關(guān)注者解釋了每個部分的目的。這是如何在主應(yīng)用程序?qū)又袑崿F(xiàn) SSO 功能的可靠示例。

  • 應(yīng)用程序組件

App 組件管理用戶身份驗證并根據(jù)登錄狀態(tài)進行重定向。

app.get("/todos/:userId", (req, res) => {
  const ssoToken = req.cookies.sso_token;
  const user = getUser(ssoToken);

  if (!user) {
    return res.status(401).json({ error: "Unauthorized" });
  }

  const userTodos = todos.filter((todo) => todo.userId === user.id);
  res.json(userTodos);
});
  • 登錄組件

登錄組件處理用戶登錄并在身份驗證成功后重定向到 Todos 頁面。

app.post("/todos", (req, res) => {
  const ssoToken = req.cookies.sso_token;
  const user = getUser(ssoToken);

  if (!user) {
    return res.status(401).json({ error: "Unauthorized" });
  }

  const { title, description } = req.body;
  const newTodo = {
    id: faker.string.uuid(),
    userId: user.id,
    title,
    description,
  };

  todos.push(newTodo);
  res.status(201).json({ message: "Todo added successfully", data: newTodo });
});
  • 待辦事項組件

Todos 組件顯示用戶特定的待辦事項并允許添加和刪除待辦事項。

// Update a todo
app.put("/todos/:id", (req, res) => {
  const ssotoken = req.cookies.sso_token;
  const user = getUser(ssotoken);
  if (!user) {
    return res.status(401).json({ message: "Unauthorized" });
  }

  const { id } = req.params;
  const { title, description } = req.body;
  const index = todos.findIndex((todo) => todo.id === id);

  if (index !== -1) {
    todos[index] = {
      ...todos[index],
      title,
      description,
    };
    res.json({
      message: "Todo updated successfully",
      data: todos[index],
    });
  } else {
    res.status(404).json({ message: "Todo not found" });
  }
});

3. 外部應(yīng)用程序(React)

外部應(yīng)用程序充當(dāng)另一個服務(wù)提供商 (SP),使用 API 并管理用戶交互。

下面是所提供代碼的結(jié)構(gòu)化細(xì)分,為您的關(guān)注者解釋了每個部分的目的。這是如何在外部應(yīng)用程序?qū)訉崿F(xiàn) SSO 功能的可靠示例。

  • 應(yīng)用程序組件

App 組件管理用戶身份驗證并根據(jù)登錄狀態(tài)進行重定向。

// Delete a todo
app.delete("/todos/:id", (req, res) => {
  const ssoToken = req.cookies.sso_token;
  const user = getUser(ssoToken);
  if (!user) {
    return res.status(401).json({ message: "Unauthorized" });
  }

  const { id } = req.params;
  const index = todos.findIndex((todo) => todo.id === id);

  if (index !== -1) {
    todos = todos.filter((todo) => todo.id !== id);
    res.json({ message: "Todo deleted successfully" });
  } else {
    res.status(404).json({ message: "Todo not found" });
  }
});
  • 待辦事項組件

Todos 組件顯示用戶特定的待辦事項。

import { useState, useEffect } from "react";
import {
  Navigate,
  Route,
  Routes,
  useNavigate,
  useSearchParams,
} from "react-router-dom";
import Todos from "./components/Todos";
import Login from "./components/Login";
import { toast } from "react-toastify";
import api from "./api";

function App() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const [searchParams] = useSearchParams();
  const navigate = useNavigate();

  useEffect(() => {
    const verifyLogin = async () => {
      const returnUrl = searchParams.get("returnUrl");
      try {
        const response = await api.get("/verify", {
          withCredentials: true,
        });
        if (response.data.authenticated) {
          setIsLoggedIn(true);
          toast.success("You are logged in.");
          navigate("/todos");
        } else {
          setIsLoggedIn(false);
          if (!returnUrl) {
            toast.error("You are not logged in.");
          }
        }
      } catch (error) {
        setIsLoggedIn(false);
        console.error("Verification failed:", error);
      }
    };

    verifyLogin();

    const handleVisibilityChange = () => {
      if (document.visibilityState === "visible") {
        verifyLogin();
      }
    };

    document.addEventListener("visibilitychange", handleVisibilityChange);

    return () => {
      document.removeEventListener("visibilitychange", handleVisibilityChange);
    };
  }, [navigate, searchParams]);

  return (
    <div className="container p-4 mx-auto">
      <Routes>
        <Route path="/" element={<Login />} />
        <Route
          path="/todos"
          element={isLoggedIn ? <Todos /> : <Navigate to={"/"} />}
        />
      </Routes>
    </div>
  );
}

export default App;

結(jié)論

單點登錄 (SSO) 簡化了跨多個應(yīng)用程序的用戶身份驗證和訪問管理,從而增強了用戶體驗、安全性和運營效率。通過集中身份驗證并利用基于安全令牌的機制,組織可以簡化用戶訪問、降低與密碼相關(guān)的風(fēng)險并提高合規(guī)性和審核能力。

雖然 SSO 提供了眾多好處,但它也帶來了挑戰(zhàn),例如單點故障、復(fù)雜的實施要求、安全風(fēng)險和潛在的供應(yīng)商鎖定。組織必須仔細(xì)規(guī)劃和實施 SSO 解決方案,以減輕這些風(fēng)險并最大限度地發(fā)揮集中式身份驗證的優(yōu)勢。

通過遵循最佳實踐、利用既定協(xié)議并選擇開放標(biāo)準(zhǔn),組織可以成功實施 SSO,以增強其應(yīng)用程序和系統(tǒng)的用戶體驗、安全性和運營效率。

以上是單點登錄 (SSO):React 和 ExpressJS 綜合指南的詳細(xì)內(nèi)容。更多信息請關(guān)注PHP中文網(wǎng)其他相關(guān)文章!

本站聲明
本文內(nèi)容由網(wǎng)友自發(fā)貢獻,版權(quán)歸原作者所有,本站不承擔(dān)相應(yīng)法律責(zé)任。如您發(fā)現(xiàn)有涉嫌抄襲侵權(quán)的內(nèi)容,請聯(lián)系admin@php.cn

熱AI工具

Undress AI Tool

Undress AI Tool

免費脫衣服圖片

Undresser.AI Undress

Undresser.AI Undress

人工智能驅(qū)動的應(yīng)用程序,用于創(chuàng)建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用于從照片中去除衣服的在線人工智能工具。

Clothoff.io

Clothoff.io

AI脫衣機

Video Face Swap

Video Face Swap

使用我們完全免費的人工智能換臉工具輕松在任何視頻中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的代碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

功能強大的PHP集成開發(fā)環(huán)境

Dreamweaver CS6

Dreamweaver CS6

視覺化網(wǎng)頁開發(fā)工具

SublimeText3 Mac版

SublimeText3 Mac版

神級代碼編輯軟件(SublimeText3)

熱門話題

Laravel 教程
1597
29
PHP教程
1488
72
如何在node.js中提出HTTP請求? 如何在node.js中提出HTTP請求? Jul 13, 2025 am 02:18 AM

在Node.js中發(fā)起HTTP請求有三種常用方式:使用內(nèi)置模塊、axios和node-fetch。1.使用內(nèi)置的http/https模塊無需依賴,適合基礎(chǔ)場景,但需手動處理數(shù)據(jù)拼接和錯誤監(jiān)聽,例如用https.get()獲取數(shù)據(jù)或通過.write()發(fā)送POST請求;2.axios是基于Promise的第三方庫,語法簡潔且功能強大,支持async/await、自動JSON轉(zhuǎn)換、攔截器等,推薦用于簡化異步請求操作;3.node-fetch提供類似瀏覽器fetch的風(fēng)格,基于Promise且語法簡單

JavaScript數(shù)據(jù)類型:原始與參考 JavaScript數(shù)據(jù)類型:原始與參考 Jul 13, 2025 am 02:43 AM

JavaScript的數(shù)據(jù)類型分為原始類型和引用類型。原始類型包括string、number、boolean、null、undefined和symbol,其值不可變且賦值時復(fù)制副本,因此互不影響;引用類型如對象、數(shù)組和函數(shù)存儲的是內(nèi)存地址,指向同一對象的變量會相互影響。判斷類型可用typeof和instanceof,但需注意typeofnull的歷史問題。理解這兩類差異有助于編寫更穩(wěn)定可靠的代碼。

JavaScript時間對象,某人構(gòu)建了一個eactexe,在Google Chrome上更快的網(wǎng)站等等 JavaScript時間對象,某人構(gòu)建了一個eactexe,在Google Chrome上更快的網(wǎng)站等等 Jul 08, 2025 pm 02:27 PM

JavaScript開發(fā)者們,大家好!歡迎閱讀本周的JavaScript新聞!本周我們將重點關(guān)注:Oracle與Deno的商標(biāo)糾紛、新的JavaScript時間對象獲得瀏覽器支持、GoogleChrome的更新以及一些強大的開發(fā)者工具。讓我們開始吧!Oracle與Deno的商標(biāo)之爭Oracle試圖注冊“JavaScript”商標(biāo)的舉動引發(fā)爭議。Node.js和Deno的創(chuàng)建者RyanDahl已提交請愿書,要求取消該商標(biāo),他認(rèn)為JavaScript是一個開放標(biāo)準(zhǔn),不應(yīng)由Oracle

什么是緩存API?如何與服務(wù)人員使用? 什么是緩存API?如何與服務(wù)人員使用? Jul 08, 2025 am 02:43 AM

CacheAPI是瀏覽器提供的一種緩存網(wǎng)絡(luò)請求的工具,常與ServiceWorker配合使用,以提升網(wǎng)站性能和離線體驗。1.它允許開發(fā)者手動存儲如腳本、樣式表、圖片等資源;2.可根據(jù)請求匹配緩存響應(yīng);3.支持刪除特定緩存或清空整個緩存;4.通過ServiceWorker監(jiān)聽fetch事件實現(xiàn)緩存優(yōu)先或網(wǎng)絡(luò)優(yōu)先等策略;5.常用于離線支持、加快重復(fù)訪問速度、預(yù)加載關(guān)鍵資源及后臺更新內(nèi)容;6.使用時需注意緩存版本控制、存儲限制及與HTTP緩存機制的區(qū)別。

處理諾言:鏈接,錯誤處理和承諾在JavaScript中 處理諾言:鏈接,錯誤處理和承諾在JavaScript中 Jul 08, 2025 am 02:40 AM

Promise是JavaScript中處理異步操作的核心機制,理解鏈?zhǔn)秸{(diào)用、錯誤處理和組合器是掌握其應(yīng)用的關(guān)鍵。1.鏈?zhǔn)秸{(diào)用通過.then()返回新Promise實現(xiàn)異步流程串聯(lián),每個.then()接收上一步結(jié)果并可返回值或Promise;2.錯誤處理應(yīng)統(tǒng)一使用.catch()捕獲異常,避免靜默失敗,并可在catch中返回默認(rèn)值繼續(xù)流程;3.組合器如Promise.all()(全成功才成功)、Promise.race()(首個完成即返回)和Promise.allSettled()(等待所有完成)

利用Array.Prototype方法用于JavaScript中的數(shù)據(jù)操作 利用Array.Prototype方法用于JavaScript中的數(shù)據(jù)操作 Jul 06, 2025 am 02:36 AM

JavaScript數(shù)組內(nèi)置方法如.map()、.filter()和.reduce()可簡化數(shù)據(jù)處理;1).map()用于一對一轉(zhuǎn)換元素生成新數(shù)組;2).filter()按條件篩選元素;3).reduce()用于聚合數(shù)據(jù)為單一值;使用時應(yīng)避免誤用導(dǎo)致副作用或性能問題。

JS綜述:深入研究JavaScript事件循環(huán) JS綜述:深入研究JavaScript事件循環(huán) Jul 08, 2025 am 02:24 AM

JavaScript的事件循環(huán)通過協(xié)調(diào)調(diào)用棧、WebAPI和任務(wù)隊列來管理異步操作。1.調(diào)用棧執(zhí)行同步代碼,遇到異步任務(wù)時交由WebAPI處理;2.WebAPI在后臺完成任務(wù)后將回調(diào)放入相應(yīng)的隊列(宏任務(wù)或微任務(wù));3.事件循環(huán)檢查調(diào)用棧是否為空,若為空則從隊列中取出回調(diào)推入調(diào)用棧執(zhí)行;4.微任務(wù)(如Promise.then)優(yōu)先于宏任務(wù)(如setTimeout)執(zhí)行;5.理解事件循環(huán)有助于避免阻塞主線程并優(yōu)化代碼執(zhí)行順序。

了解事件在JavaScript DOM事件中冒泡和捕獲 了解事件在JavaScript DOM事件中冒泡和捕獲 Jul 08, 2025 am 02:36 AM

事件冒泡是從目標(biāo)元素向外傳播到祖先節(jié)點,事件捕獲則是從外層向內(nèi)傳播到目標(biāo)元素。1.事件冒泡:點擊子元素后,事件依次向上觸發(fā)父級元素的監(jiān)聽器,例如點擊按鈕后先輸出Childclicked,再輸出Parentclicked。2.事件捕獲:設(shè)置第三個參數(shù)為true,使監(jiān)聽器在捕獲階段執(zhí)行,如點擊按鈕前先觸發(fā)父元素的捕獲監(jiān)聽器。3.實際用途包括統(tǒng)一管理子元素事件、攔截預(yù)處理和性能優(yōu)化。4.DOM事件流分為捕獲、目標(biāo)和冒泡三個階段,默認(rèn)監(jiān)聽器在冒泡階段執(zhí)行。

See all articles