空闲注销登录是一种重要的安全措施,即用户在一段时间内无活动后自动终止其会话。在涉及敏感信息的场景中,这一功能尤为重要,因为它能有效防止未经授权的访问和用户忘记注销登录时可能引发的滥用,比如很多银行 App 在五分钟无活动就会自动注销登录。
那我们该如何实现这个功能呢?今天就来分享一个很实用的工具库——React Idle Timer,帮助你快速实现空闲注销登录!
概念
React Idle Timer 是一个 JavaScript 库,用于检测和监控Web应用上的用户活动。它提供了一个IdleTimer组件,可以监测用户的鼠标移动、键盘输入和触摸等事件,以确定用户是否处于活动状态。在React应用中,可以通过React Idle Timer实现空闲注销登录功能,即在用户在一段时间内没有活动的情况下,自动结束其会话,从而提高应用的安全性。
空闲注销登录的实现通常需要前端和后端的协同工作。前端部分通过捕捉用户活动,如鼠标移动、点击或键盘输入等事件,来不断更新用户的活跃状态。一旦这些事件停止发生,前端会启动一个计时器,并持续监控用户是否恢复活动。与此同时,后端服务器也在跟踪用户的最后活动时间。如果后端在一定时间内没有收到来自前端的任何新活动信号,并且计时器超过了预设的阈值,服务器就会判定用户已处于空闲状态,并主动结束其会话。这种前后端结合的实现方式,确保了空闲注销登录的准确性和可靠性。即便在前端出现意外情况(如浏览器崩溃),后端仍能通过监控最后活动时间来保障用户会话的安全。而前端部分就可以借助 React Idle Timer 来实现。
React Idle Timer 的应用场景如下:
- 自动注销登录:在涉及敏感信息的应用中,如银行应用、电商网站等,空闲注销登录功能尤为重要。React Idle Timer可以监测用户在一段时间内的活动状态,如果用户长时间没有交互操作,可以自动结束其会话,防止未经授权的访问。
- 显示闲置提示:当用户在一段时间内没有进行任何操作时,应用可以利用React Idle Timer来触发闲置提示的显示。这可以提醒用户他们当前的会话仍然处于活跃状态,或者提供继续操作的指引。
- 保存用户数据:在用户可能因为各种原因暂时离开应用时,React Idle Timer可以检测到用户的空闲状态,并触发数据保存机制。这有助于防止用户数据因意外情况而丢失,提高数据的安全性。
- 优化广告展示:在广告展示类的应用中,React Idle Timer可以帮助开发者更精准地判断用户的活跃状态,从而调整广告的展示策略。例如,在用户长时间未进行交互时,可以暂停或调整广告的播放,提升用户体验。
- 提升应用性能:通过监测用户的空闲状态,React Idle Timer还可以帮助开发者优化应用的性能。例如,在用户空闲时,可以降低应用的刷新率或暂停某些后台进程,从而节省系统资源。
使用
首先,在终端中输入以下命令来创建一个基于 Vite 的 React 项目:
yarn create vite vite-idle-timer --template react-ts
cd vite-idle-timer
然后,通过以下命令来安装 react-idle-timer:
yarn add react-idle-timer
安装完成之后就可以在项目中使用 react-idle-timer 了。下面就来看看如何在项目中使用 react-idle-timer。
useIdleTimer
react-idle-timer 中的 useIdleTimer Hook 是用于检测用户是否空闲的主要机制,即用户在一定时间内没有与应用进行交互。其语法如下:
function useIdleTimer({
timeout,
promptTimeout,
promptBeforeIdle,
element,
events,
timers,
immediateEvents,
onPresenceChange,
onPrompt,
onIdle,
onActive,
onAction,
onMessage,
debounce,
throttle,
eventsThrottle,
startOnMount,
startManually,
stopOnIdle,
crossTab,
name,
syncTimers,
leaderElection,
disabled
}?: IIdleTimerProps): IIdleTimer
这个 Hook 包含了很多参数,那咱就来通过 的类型来看看这些参数都是干嘛用的:
export interface IIdleTimerProps {
/**
* 用于类组件的 IdleTimer 引用。
*
* @default undefined
*/
ref?: RefObject<IIdleTimer>;
/**
* 空闲超时时间,以毫秒为单位。
*
* @default 1200000(20分钟)
*/
timeout?: number;
/**
* 当用户处于空闲状态时,会调用 onPrompt 函数,并且在达到提示超时时间(以毫秒为单位)后,会调用 onIdle 函数。
*
* @default 0(已弃用,请使用 promptBeforeIdle)
* @deprecated use promptBeforeIdle
*/
promptTimeout?: number;
/**
* 在超时之前的毫秒数内调用 onPrompt 事件处理程序。
*
* @default 0
*/
promptBeforeIdle?: number;
/**
* 绑定活动监听器的元素。
*
* @default document
*/
element?: Document | HTMLElement;
/**
* 监视活动事件的 DOM 事件。
*
* @default DefaultEvents
* @link [默认事件](https://idletimer.dev/docs/props#events).
*/
events?: EventsType[];
/**
* 将绕过超时并立即触发 onPrompt/onIdle 事件的 DOM 事件。此数组中的事件优先于 events 数组中的事件。
*
* @default []
*/
immediateEvents?: EventsType[];
/**
* 当用户的在线状态发生变化时调用的函数。
*
* @default () => {}
*/
onPresenceChange?: (presence: PresenceType, idleTimer?: IIdleTimer) => void;
/**
* 当设置了 promptTimeout 时,此函数在用户处于空闲状态后被调用。这对于显示确认提示很有用。如果达到了提示超时,随后会调用 onIdle。
*
* @default () => {}
*/
onPrompt?: (event?: Event, idleTimer?: IIdleTimer) => void;
/**
* 当用户处于空闲状态时调用的函数。
*
* @default () => {}
*/
onIdle?: (event?: Event, idleTimer?: IIdleTimer) => void;
/**
* 当用户变得活跃时调用的函数。
*
* @default () => {}
*/
onActive?: (event?: Event, idleTimer?: IIdleTimer) => void;
/**
* 在用户活动时调用的函数。可以使用 `throttle` 和 `debounce` 属性进行节流或防抖。
*
* @default () => {}
*/
onAction?: (event?: Event, idleTimer?: IIdleTimer) => void;
/**
* 当消息被发出时调用的函数。
*
* @default () => {}
*/
onMessage?: (data: any, idleTimer?: IIdleTimer) => void;
/**
* 通过设置延迟(以毫秒为单位)来防抖 onAction 函数。
*
* @default 0
*/
debounce?: number;
/**
* 通过设置延迟(以毫秒为单位)来节流 onAction 函数。
*
* @default 0
*/
throttle?: number;
/**
* 节流活动事件。如果你在监听鼠标事件,这很有用。有助于减少 CPU 使用率。
*
* @default 200
*/
eventsThrottle?: number;
/**
* 当钩子挂载时启动计时器。
*
* @default true
*/
startOnMount?: boolean;
/**
* 要求手动启动计时器。
*
* @default false
*/
startManually?: boolean;
/**
* 一旦用户处于空闲状态,IdleTimer 将不会在用户输入时重置,而是必须手动调用 start() 或 reset() 来重启计时器。
*
* @default false
*/
stopOnIdle?: boolean;
/**
* 要使用的计时器接口。默认情况下使用主线程计时器以保持模块可树摇。如果你想使用工作线程计时器,导入它们并在这里设置。
*
* @default 主线程计时器
*/
timers?: ITimers;
/**
* 启用跨标签页事件复制。
*
* @default false
*/
crossTab?: boolean;
/**
* 此 IdleTimer 实例的名称。当你启用 crossTab 并实例化多个 IdleTimer 时非常有用。
*/
name?: string;
/**
* 跨所有标签页同步计时器。该值是计时器同步的间隔。将其设置为 0 相当于关闭该功能。
*
* @default 0
*/
syncTimers?: number;
/**
* 启用领导者功能。领导者选举将指定一个标签页作为领导者。使用 `isLeader` 方法确定一个标签页是否是领导者。
*/
leaderElection?: boolean;
/**
* 禁用计时器。禁用计时器会重置内部状态。当属性设置为 true(启用)时,计时器将重新启动,尊重 `startManually` 属性。当计时器被禁用时,控制方法 `start`、`reset`、`activate`、`pause` 和 `resume` 将不会产生任何效果。
*/
disabled?: boolean;
}
此外,该 Hook 还提供了很多有用的方法,用于检测空闲状态、计算已过去和剩余的时间、以及在多个标签页之间传播消息等功能。
export interface IIdleTimer {
/**
* 恢复初始状态并重启计时器。
*
* @returns 实例是否已启动。
*/
start(): boolean;
/**
* 恢复初始状态。
*
* @returns 实例是否已重置。
*/
reset(): boolean;
/**
* 恢复初始状态,并在用户被提示或处于空闲状态时发出 onActive 事件。
*
* @returns 实例是否已激活。
*/
activate(): boolean;
/**
* 存储剩余时间并停止计时器。
*
* @returns 实例是否已暂停。
*/
pause(): boolean;
/**
* 恢复已暂停的计时器。
*
* @returns 实例是否已恢复。
*/
resume(): boolean;
/**
* 向所有 IdleTimer 实例广播任意消息。
*
* @param data 要发送到 `onMessage` 回调的数据。
* @param emitOnSelf 在调用者实例上发出事件。
*
* @returns 是否已发送消息。
*/
message(data: string | number | object, emitOnSelf?: boolean): boolean;
/**
* 返回用户是否处于空闲状态。
*
* @returns 空闲状态。
*/
isIdle(): boolean;
/**
* 返回当前标签页是否为领导者。
*
* @returns 领导者状态。
*/
isLeader(): boolean;
/**
* 返回是否激活了提示。
*
* @returns 提示状态。
*/
isPrompted(): boolean;
/**
* 返回这是否是最后一个活动的标签页。
*
* @returns 最后活跃状态。
*/
isLastActiveTab(): boolean;
/**
* 返回当前标签页的 ID。
*/
getTabId(): string;
/**
* 在空闲或提示之前的剩余时间。
*
* @returns 直到空闲或提示的毫秒数。
*/
getRemainingTime(): number;
/**
* 自上次重置以来经过的时间。
*
* @returns 自上次重置以来的毫秒数。
*/
getElapsedTime(): number;
/**
* 自挂载以来经过的总时间。
*
* @returns 自挂载以来的毫秒数。
*/
getTotalElapsedTime(): number;
/**
* 用户最后一次处于空闲状态的时间。
*
* @returns 可以格式化的 Date 对象。
*/
getLastIdleTime(): Date | null;
/**
* 用户最后一次活跃的时间。
*
* @returns 可以格式化的 Date 对象。
*/
getLastActiveTime(): Date | null;
/**
* 自上次重置以来用户处于空闲状态的时间(以毫秒为单位)。
*
* @returns 用户处于空闲状态的毫秒数。
*/
getIdleTime(): number;
/**
* 自挂载以来用户处于空闲状态的总时间(以毫秒为单位)。
*
* @returns 用户处于空闲状态的毫秒数。
*/
getTotalIdleTime(): number;
/**
* 自上次重置以来用户活跃的总时间(以毫秒为单位)。
*
* @returns 用户活跃的毫秒数。
*/
getActiveTime(): number;
/**
* 自挂载以来用户活跃的总时间(以毫秒为单位)。
*
* @returns 用户活跃的毫秒数。
*/
getTotalActiveTime(): number;
}
空闲检测
React Idle Timer的主要功能是空闲检测。它旨在监控用户活动,重点关注需要跟踪的事件。它具备了在用户未在规定时间范围内参与这些指定事件时通知应用的能力。
下面来看一个简单的例子:
import { useEffect, useState } from 'react';
import { useIdleTimer } from 'react-idle-timer';
function App() {
const [state, setState] = useState<string>('Active');
const [eventInfo, setEventInfo] = useState<string>('No event');
const [remaining, setRemaining] = useState<number>(0);
const onIdle = () => {
setState('Idle');
};
const onActive = () => {
setState('Active');
};
const onAction = (event?: Event) => {
if (event) {
setEventInfo(
`事件 ${event.type} 发生在 ${new Date}`
);
}
};
const { getRemainingTime } = useIdleTimer({
onIdle,
onActive,
onAction,
timeout: 10_000,
throttle: 500,
});
useEffect(() => {
const interval = setInterval(() => {
setRemaining(Math.ceil(getRemainingTime() / 1000));
}, 500);
return () => {
clearInterval(interval);
};
});
return (
<>
<h1>React Idle Timer</h1>
<h2>useIdleTimer</h2>
<p>当前状态: {state}</p>
<p>{eventInfo}</p>
<p>{remaining} 秒后进入空闲状态</p>
</>
);
}
export default App;
在这个例子中,通过执行 yarn dev 命令,onAction 回调会根据鼠标移动或键盘事件进行更新。每当发生事件时,会触发 onActive,有效地重置空闲计时器。如果在 10 秒内没有任何活动,getRemainingTime 方法将返回 0 的值,随后会调用 onIdle 回调。
确认提示
一个使用空闲计时器的典型应用是用来识别用户不活跃的时间,并提示他们确认自己还在使用。这一功能现在已经无缝地集成到 React Idle Timer 的核心特性中。
下面的例子实现了在剩余5秒时弹出确认提示的功能。
import { useEffect, useState } from 'react';
import { useIdleTimer } from 'react-idle-timer';
function App() {
const [state, setState] = useState<string>('Active');
const [eventInfo, setEventInfo] = useState<string>('No event');
const [remaining, setRemaining] = useState<number>(0);
const [prompt, setPrompt] = useState<string>('');
const onIdle = () => {
setState('Idle');
setPrompt('');
};
const onActive = () => {
setState('Active');
setPrompt('');
};
const onAction = (event?: Event) => {
if (event) {
setEventInfo(
`事件 ${event.type} 发生在 ${new Date}`
);
}
};
const onPrompt = () => {
setPrompt('即将进入空闲状态');
}
const { getRemainingTime } = useIdleTimer({
onIdle,
onActive,
onAction,
onPrompt,
timeout: 10_000,
promptBeforeIdle: 5_000,
throttle: 500,
});
useEffect(() => {
const interval = setInterval(() => {
setRemaining(Math.ceil(getRemainingTime() / 1000));
}, 500);
return () => {
clearInterval(interval);
};
});
return (
<>
<h1 style={{color: 'red'}}>{prompt}</h1>
<h1>React Idle Timer</h1>
<h2>useIdleTimer</h2>
<p>当前状态: {state}</p>
<p>{eventInfo}</p>
<p>{remaining} 秒后进入空闲状态</p>
</>
);
}
export default App;
在这个例子中,执行yarn dev命令后,如果未检测到用户活动持续5秒,将会出现一个确认提示。之后任何用户活动都会导致该提示消失。然而,如果继续不活跃,确认提示将保持可见,直到空闲时间达到10秒。此时,getRemainingTime方法将返回0,触发onIdle回调函数。
跨标签页支持
那如果我们同时在多个标签打开应用,React Idle Timer 还能正常工作吗?实际上,React Idle Timer 提供了跨标签页支持。跨标签页功能实现了在应用的多个标签页间同步事件和状态,进而提升用户整体的使用体验。利用这一功能,我们可以向所有标签页发送消息,无论它们当前是处于空闲状态还是活跃状态,确保信息流的连贯性和一致性。
下面是src/App.tsx文件,它设置了主路由/main,其中包含两个子路由:/one和/two。如果访问任何未知路由,用户将被重定向到登录页面。
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { MainPage } from './MainPage';
import { PageOne, PageTwo } from './Pages';
import { Login } from './Login';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/main" element={<MainPage />}>
<Route index element={<div>No page is selected</div>} />
<Route path="one" element={<PageOne />} />
<Route path="two" element={<PageTwo />} />
</Route>
<Route path="*" element={<Login />} />
</Routes>
</BrowserRouter>
);
}
export default App;
Login.tsx文件定义了一个按钮,用于导航到/main路由。
import { useNavigate } from 'react-router-dom';
export const Login = () => {
const navigate = useNavigate();
return <button onClick={() => navigate('/main')}>登录</button>
};
src/Main.tsx文件定义了一个指向/one的链接,一个指向/two的链接,一个<Outlet />组件,以及一个注销按钮。
import { Link, Outlet, useNavigate } from 'react-router-dom';
export const MainPage = () => {
const navigate = useNavigate();
return (
<>
<nav>
<ul>
<li>
<Link to="one">Page One</Link>
</li>
<li>
<Link to="two">Page Two</Link>
</li>
</ul>
</nav>
<hr />
<Outlet />
<button style={{marginTop: '20px'}} onClick={() => navigate('/')}>注销</button>
</>
);
};
在这个例子中,执行yarn dev命令后,成功登录将重定向到/main页面。该页面提供了指向页面一和页面二的链接。在浏览应用后,点击注销按钮将注销用户,使应用返回到登录前的初始状态。
在src/useConfiguredIdleTimer.ts文件中创建了一个名为useConfiguredIdleTimer的自定义 Hook。如果用户在所有标签页/窗口中保持空闲状态达10秒,它将导航应用到根路由。
import { useIdleTimer } from 'react-idle-timer';
import { useNavigate } from 'react-router-dom';
export const useConfiguredIdleTimer = () => {
const navigate = useNavigate();
useIdleTimer({
timeout: 10_000,
crossTab: true,
onIdle: () => {
navigate('/');
},
});
};
将这个 Hook 将按照以下方式添加到src/Main.tsx文件中:
import { Link, Outlet, useNavigate } from 'react-router-dom';
import { useConfiguredIdleTimer } from './useConfiguredIdleTimer';
export const MainPage = () => {
useConfiguredIdleTimer();
const navigate = useNavigate();
return (
<>
<nav>
<ul>
<li>
<Link to="one">Page One</Link>
</li>
<li>
<Link to="two">Page Two</Link>
</li>
</ul>
</nav>
<hr />
<Outlet />
<button style={{marginTop: '20px'}} onClick={() => navigate('/')}>注销</button>
</>
);
};
在这个例子中,执行yarn dev命令后,两个浏览器窗口都已登录并导航到多个页面。如果它们保持空闲状态达10秒,两个窗口都会自动注销。
React Idle Timer 的基本功能到这里就介绍完了,更多功能详见官方文档。
- GitHub:https://github.com/SupremeTechnopriest/react-idle-timer。
- 官方文档:https://idletimer.dev/。