使用 React.js 进行身份验证
我们需要做什么?
我们将如何做?
我们将使用hooks和context。我们只会使用基本概念,您无需在此用例中深入研究此主题。
我们需要做什么?
- 创建一个只有登录后才能访问的页面(我们需要创建两个页面:用户登录的登录页面和用户登录后进入的面板页面。用户只有在登录后才能访问面板页面。如果他试图直接访问面板,我们需要将他重定向到登录页面);
- 如果用户已经登录并刷新页面,他应该停留在面板页面,而不是被重定向到登录页面;
我们将如何做?
- 我们将创建一个名为PrivateRoute的组件,只有通过 SignIn 页面后才能访问;
- 我们将把用户令牌保存在localStorage中,以便当他退出或刷新页面时,他可以直接访问面板。
现在我们明白了我们要做什么,我们可以开始编码了。
创建我们的组件:Panel 和 SignIn
首先,在src文件夹中,创建一个名为screens的新文件夹。在这里,我们将创建Panel.js和SignIn.js。我将使用 bootstrap 来更快地设置组件的样式。如果你想这样做,但不知道如何安装 bootstrap,请查看此处。
在src/screens/Panel.js中:
import React from "react";
import { Button } from "react-bootstrap";
const Panel = () => {
const onLogOut = () => {
console.log('LogOut pressed.'); // we will change it later
}
return (
<div
style={{ height: "100vh" }}
className="d-flex justify-content-center align-items-center"
>
<div style={{ width: 300 }}>
<h1 className="text-center"> Hello, user </h1>
<Button
variant="primary"
type="button"
className="w-100 mt-3 border-radius"
onClick={onLogOut}
>
Log out
</Button>
</div>
</div>
);
};
export default Panel;
在src/screens/SignIn.js中:
import React, { useState} from 'react';
import { Form, Button } from 'react-bootstrap';
const SignIn = () => {
const [email, setEmail] = useState();
const [password, setPassword] = useState();
const onFormSubmit = e => {
e.preventDefault();
console.log(email);
console.log(password);
// we will change it later;
};
return (
<div
style={{ height: "100vh" }}
className="d-flex justify-content-center align-items-center"
>
<div style={{ width: 300 }}>
<h1 className="text-center">Sign in</h1>
<Form onSubmit={onFormSubmit}>
<Form.Group>
<Form.Label>Email address</Form.Label>
<Form.Control
type="email"
placeholder="Enter email"
onChange={e => {
setEmail(e.target.value);
}}
/>
</Form.Group>
<Form.Group>
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
placeholder="Password"
onChange={e => {
setPassword(e.target.value);
}}
/>
</Form.Group>
<Button
variant="primary"
type="submit"
className="w-100 mt-3"
>
Sign in
</Button>
</Form>
</div>
</div>
);
};
export default SignIn;
现在我们需要创建路由器。我们将在App.js中创建。为了在应用中导航,我们将使用react-router-dom。我们需要使用 yarn 或 npm 安装它:
yarn add react-router-dom
现在在src/App.js中我们将为我们的应用程序创建路由。
import React from 'react';
import { Switch, BrowserRouter, Route } from 'react-router-dom';
import SignIn from './screens/SignIn';
import Panel from './screens/Panel';
function App() {
return (
<BrowserRouter>
<Switch>
<Route path="/sign-in" component={SignIn} />
<Route path="/" component={Panel} />
</Switch>
</BrowserRouter>
);
}
export default App;
在上下文中保存用户令牌
现在我们需要创建一个上下文,以便能够在多个组件中访问用户令牌。即使在这个例子中我们只有两个组件,但在实际应用中,我们会有更多组件,其中很多组件都需要用户信息。
我们将在 src 文件夹中创建一个名为contexts的文件夹,并创建AuthContext.js。
在src/contexts/AuthContext.js中:
import React, { createContext, useState } from 'react';
export const authContext = createContext({});
const AuthProvider = ({ children }) => {
const [auth, setAuth] = useState({ loading: true, data: null });
// we will use loading later
const setAuthData = (data) => {
setAuth({data: data});
};
// a function that will help us to add the user data in the auth;
return (
<authContext.Provider value={{ auth, setAuthData }}>
{children}
</authContext.Provider>
);
};
export default AuthProvider;
为了能够在整个应用程序中使用我们的上下文,我们需要将App组件包装在AuthProvider中。为此,我们进入src/index.js:
...
import AuthProvider from './contexts/AuthContext';
ReactDOM.render(
(
<AuthProvider>
<App />
</AuthProvider>
),
document.getElementById('root'),
);
...
现在我们需要将用户凭证从SignIn组件传递给上下文。理想情况下,你只需要将 token 发送到上下文,但在本例中,我们将发送用户电子邮件,因为我们没有后端提供电子邮件。
在src/screens/SignIn.js中:
...
import React, { useState, useContext } from 'react';
import { authContext } from '../contexts/AuthContext';
const SignIn = ({history}) => {
...
const { setAuthData } = useContext(authContext);
const onFormSubmit = e => {
e.preventDefault();
setAuthData(email); // typically here we send a request to our API and in response, we receive the user token.
//As this article is about the front-end part of authentication, we will save in the context the user's email.
history.replace('/'); //after saving email the user will be sent to Panel;
};
...
};
export default SignIn;
另外,当用户点击面板上的“注销”按钮时,我们需要清除上下文。我们将添加用户邮箱地址,而不是“Hello, user”。在src/screens/Panel.js中:
import React, {useContext} from "react";
import { Button } from "react-bootstrap";
import { authContext } from "../contexts/AuthContext";
const Panel = () => {
const { setAuthData, auth } = useContext(authContext);
const onLogOut = () => {
setAuthData(null);
} //clearing the context
return (
<div
style={{ height: "100vh" }}
className="d-flex justify-content-center align-items-center"
>
<div style={{ width: 300 }}>
<h1 className="text-center"> {`Hello, ${auth.data}`} </h1>
<Button
variant="primary"
type="button"
className="w-100 mt-3"
onClick={onLogOut}
>
Log out
</Button>
</div>
</div>
);
};
export default Panel;
创建 PrivateRoute
现在我们需要让面板仅在登录后才能访问。为此,我们需要创建一个名为PrivateRoute的新组件。我们正在创建src/components/PrivateRote.js:
import React, { useContext } from 'react';
import { Route, Redirect } from 'react-router-dom';
import { authContext } from '../contexts/AuthContext';
const PrivateRoute = ({ component: Component, ...rest }) => {
const { auth } = useContext(authContext);
return (
<Route
{...rest}
render={(routeProps) => (
auth.data ? <Component {...routeProps} /> : <Redirect to="/sign-in" />
)}
/>
);
/* we are spreading routeProps to be able to access this routeProps in the component. */
};
export default PrivateRoute;
如果用户未登录,我们将把他重定向到SignIn组件。现在我们需要在src/App.js
中使用 PrivateRoute :
...
import PrivateRoute from './components/PrivateRoute';
function App() {
return (
<BrowserRouter>
<Switch>
<Route path="/sign-in" component={SignIn} />
<PrivateRoute path="/" component={Panel} />
</Switch>
</BrowserRouter>
);
}
export default App;
管理 localStorage
现在一切正常,但如果刷新Panel页面,我们将返回到SignIn页面。我们希望浏览器记住用户。因此,我们将使用localStorage 。LocalStorage 是浏览器中存储数据的地方。localStorage 的问题在于它会降低应用程序的速度。我们需要明智地使用它,并加入 useEffect 函数来确保代码只执行一次。我们将在src/contexts/AuthContext.js中完成所有操作:
import React, { createContext, useState, useEffect } from 'react';
export const authContext = createContext({});
const AuthProvider = ({ children }) => {
const [auth, setAuth] = useState({ loading: true, data: null });
const setAuthData = (data) => {
setAuth({data: data});
};
useEffect(() => {
setAuth({ loading: false, data: JSON.parse(window.localStorage.getItem('authData'))});
}, []);
//2. if object with key 'authData' exists in localStorage, we are putting its value in auth.data and we set loading to false.
//This function will be executed every time component is mounted (every time the user refresh the page);
useEffect(() => {
window.localStorage.setItem('authData', JSON.stringify(auth.data));
}, [auth.data]);
// 1. when **auth.data** changes we are setting **auth.data** in localStorage with the key 'authData'.
return (
<authContext.Provider value={{ auth, setAuthData }}>
{children}
</authContext.Provider>
);
};
export default AuthProvider;
现在在src/components/PrivateRoute.js中:
const PrivateRoute = ({ component: Component, ...rest }) => {
const { auth } = useContext(authContext);
const { loading } = auth;
if (loading) {
return (
<Route
{...rest}
render={() => {
return <p>Loading...</p>;
}}
/>
);
}
// if loading is set to true (when our function useEffect(() => {}, []) is not executed), we are rendering a loading component;
return (
<Route
{...rest}
render={routeProps => {
return auth.data ? (
<Component {...routeProps} />
) : (
<Redirect to="/sign-in" />
);
}}
/>
);
};
export default PrivateRoute;
就是这样。现在,如果用户已登录并刷新页面,他将停留在面板上,而不会重定向到登录页面。但是,如果用户已注销,他只能通过登录页面访问面板。
为什么我们在上下文中使用了loading对象?因为我们在上下文中使用的setAuth
函数是异步的,这意味着它需要一些时间才能真正更新状态。如果没有loading对象,auth.data会在几毫秒内为null。因此,我们在上下文中将loading设置为false,并在PrivateRoute组件中返回所需的路由。