zzxworld

基于 React 的登录认证

这几天我正在学习 React,目前已经掌握了如何结合 Webpack 搭建出一个 React 前端开发环境,以及配合 React router 来实现基于前端的路由和页面 js 按需加载功能。为了能在实际项目中使用 React,我还需要解决最后一个问题:用户登录认证功能。

关于搭建 React 基础开发环境和使用 React router 的文章,可以通过下面的链接查看了解:

对于还不熟悉 React 的朋友,相信上面两篇文章对你入门 React 会有所帮助。本文是在以上两篇文章的基础之上继续的探索,所以如果你有兴趣想要尝试本文的内容,请先完成以上两篇文章中所记录的流程。

登录的用途

我准备使用 React 来完成一个企业内部系统前端功能的重写。作为一个内部系统,在用户还没有登陆的前提下,是不允许访问系统中任何页面内容的。所以这次我想要做的就是提供一个登录页面,在用户没有登录时访问系统只能看到这个登陆页面的内容。

另外为了避免刷新页面后登陆状态重置,最好还要保存一下用户的登陆信息。这需要用到浏览器的 Cookie 功能。

实现登录功能

登陆离不开输入帐号和密码的界面,先完成登陆界面的 React 组件。

进入项目目录,在 src/pages/ 目录下创建 login.js 文件,并输入以下代码:

import React, { useState } from 'react';

export default ({ saveToken }) => {
    const [account, setAccount] = useState();
    const [password, setPassword] = useState();

    const handleSubmit = (e) => {
        e.preventDefault();

        // 判断输入帐号和密码
        if (account == 'zzxworld' && password == '123456') {
            saveToken(account);
        } else {
            alert('Invalid account or password!');
        }
    }

    return (
        <div>
            <h2>Login</h2>
            <form onSubmit={ handleSubmit }>
                <label>
                    <p>Account:</p>
                    <input type="text" onChange={ e => setAccount(e.target.value) } />
                </label>
                <label>
                    <p>Password:</p>
                    <input type="password" onChange={ e => setPassword(e.target.value) } />
                </label>
                <p>
                    <button type="submit">Submit</button>
                </p>
            </form>
        </div>
    );
}

和之前创建的两个页面:「首页」和「关于页面」组件代码相比,这次登录页面组件的代码就丰富多了。

首先是使用了 useState,这是 React 从 16.8 这个版本开始提供的新功能:Hook。可以让函数的 React 组件获得和类组件一样的能力。这里使用的 useState 是其中的一个 Hook 函数,可以在函数式组件中定义一个状态绑定变量和设置其值的方法。因为需要获取并设置用户输入的帐号密码,所以使用了两次 useState。

React 提供了不同场景下使用的 Hook,本文的目的不是介绍 Hook,所以这里先不介绍其他 Hook,有兴趣可以前往官网了解学习。

另外是 handleSubmit 方法,它定义在 form 表单提交的事件上。当点击 Submit 提交按钮时会执行这个方法。你可能注意到了,这个方法里我「写死」了正确的帐号和密码。原因是这是一个试验项目,为了突出本文的目的,我省略了和后端的交互过程。在正式项目里,这个方法会调用后端登录接口并取得登录凭证和用户信息。

最后是这个登录组件有了传参,会从外部提供一个 saveToken 方法,它用来保存登录成功的凭证。此处简单起见,直接把帐号作为了登录凭证。

有了登录页面组件,接下来就是在 src/main.js 项目入口代码中添加登录相关的处理逻辑。修改后的 src/main.js 代码如下:

import React, { Suspense, lazy, useState } from 'react';
import ReactDOM from 'react-dom';
import { HashRouter, Routes, Route, Link } from 'react-router-dom';
import PageLogin from './pages/login';

const PageHome = lazy(() => import('./pages/home'))
const PageAbout = lazy(() => import('./pages/about'))

function AppLayout() {
    const [token, setToken] = useState();

    const handleLogout = (e) => {
        e.preventDefault();
        setToken(null);
    }

    if (!token) {
        return <PageLogin saveToken={ setToken } />;
    }

    return (
        <HashRouter>
            <nav>
                <Link to="/">Home</Link> | <Link to="/about">Abount</Link>
            </nav>
            { token ? <p>Welcome, { token }。<a href="#" onClick={ handleLogout }>Logout</a></p> : '' }
            <Routes>
                <Route path="/" element={
                        <Suspense fallback={<div>Loading...</div>}>
                            <PageHome />
                        </Suspense>
                    } />
                <Route path="/about" element={
                        <Suspense fallback={<div>Loading...</div>}>
                            <PageAbout />
                        </Suspense>
                    } />
            </Routes>
        </HashRouter>
    );
}

ReactDOM.render(<AppLayout />, document.getElementById('app'))

这次的调整主要如下:

  1. 同样引用了 useState 来保存登录凭证。
  2. 在 AppLayout 组件方法中,添加了登录凭证的判断。如果没有值就会直接渲染登录页面组件。
  3. 提供了 handleLogout 退出方法,只有在有登录凭证时才会在 Logout 链接被点击时调用。它会删除当前的登录凭证。

执行 yarn start 运行项目看看效果。这是现在打开首页时的界面:

image-20211118132841321

可以看到地址栏虽然是首页地址,但显示的是登录界面,这说明权限逻辑起作用了。输入代码中定义的正确帐号和密码后登录:

image-20211118133033638

地址栏没变,但内容已经是首页内容了。中间还多了一项欢迎信息。点击后面的 Logout 链接,又会回到登录界面。

保存登录状态

上面虽然完成了使用 React 制作登录认证的功能,但有点小问题。当登录成功后,即便不点击后面的退出链接,只是刷新一下页面,依然会回到登录页面。

这是因为在刚才的逻辑中,只是通过 setToken 把登录凭证保存在变量中。这个变量只会跟当前这次请求的环境相关。当刷新页面时,浏览器会重新发起请求,之前的登录凭证也就跟随页面一起消失了,所以这意味着又要重新进行一次登录操作。在实际的业务系统中,这样的情况是不能允许发生的。解决这个问题的方案就是把登录凭证存储到一个脱离会话的环境中。比如浏览器提供的 Cookie。

关于浏览器 Cookie 的介绍网上有很多,这里不做过多解释。从需求来说,它可以让我在同一个网址下,跨请求的存放并使用数据。把登录凭证保存到 Cookie,只要登录成功,在不主动推出,或者是数据过期的前提下,这个登录状态就会一直保持着。这正是我需要达成的目的。

首先安装一个 Cookie 操作的扩展库:

yarn add js-cookie

然后再次调整下 src/main.js 文件中的功能,调整后的代码如下:

import React, { Suspense, lazy, useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
import { HashRouter, Routes, Route, Link } from 'react-router-dom';
import Cookies from 'js-cookie';
import PageLogin from './pages/login';

const PageHome = lazy(() => import('./pages/home'))
const PageAbout = lazy(() => import('./pages/about'))

function AppLayout() {
    const [token, setToken] = useState();

    useEffect(() => {
        setToken(Cookies.get('token'))
    }, [])

    const saveToken = (token) => {
        Cookies.set('token', token, {
            expires: 7,
            sameSite: 'strict'
        })

        setToken(token)
    }

    const handleLogout = (e) => {
        e.preventDefault();

        Cookies.remove('token', {
            path: ''
        })

        setToken(null);
    }

    if (!token) {
        return <PageLogin saveToken={ saveToken } />;
    }

    return (
        <HashRouter>
            <nav>
                <Link to="/">Home</Link> | <Link to="/about">Abount</Link>
            </nav>
            { token ? <p>Welcome, { token }。<a href="#" onClick={ handleLogout }>Logout</a></p> : '' }
            <Routes>
                <Route path="/" element={
                        <Suspense fallback={<div>Loading...</div>}>
                            <PageHome />
                        </Suspense>
                    } />
                <Route path="/about" element={
                        <Suspense fallback={<div>Loading...</div>}>
                            <PageAbout />
                        </Suspense>
                    } />
            </Routes>
        </HashRouter>
    );
}

ReactDOM.render(<AppLayout />, document.getElementById('app'))

这次使用了一个新的 React Hook:useEffect。它会用来在每次加载页面时自动从 Cookie 中获取保存的登录凭证,以判断当前用户有没有登录。

另外在保存登录凭证和退出方法中,都添加了 Cookie 的处理功能。具体的逻辑可以通过以上代码来了解。

现在任凭我怎么刷新,只要不点退出,登录状态都不会丢失了。