1657710840
可以通过多种方式处理用户身份验证。由于此功能的重要性,我们已经看到越来越多的公司提供身份验证解决方案来简化流程 - Firebase、Auth0 和 NextAuth.js 等等。
无论此类服务如何处理最终的身份验证和授权,实现过程通常涉及调用一些 API 端点并接收要在前端基础架构中使用的私有令牌(通常是JSON Web 令牌或 JWT)。
在本文中,我们将学习如何使用 Redux Toolkit (RTK) 在 React 中创建前端身份验证工作流。我们将使用基本的 Toolkit API,例如createAsyncThunk
向 Express 后端发出异步请求。我们还将用于createSlice
处理状态更改。
该项目的后端是使用 Express 和 MongoDB 数据库构建的,但前端工作流程仍应适用于您使用的任何提供令牌的身份验证服务。您可以下载项目存储库中的源文件,其中包含有关如何设置数据库并可以在本地运行应用程序的说明。在此处查看现场演示。
要继续学习,您需要:
现在,让我们开始认证吧!
存储库包含一个[starter-files]
分支,其中包含引导此应用程序所需的文件。前端文件夹还包括演示中看到的各种用户界面,例如Home、Login、Register和Profile屏幕以及各自的路由/
、/login
、/register
和/user-profile
。
以下是当前的路由结构:
// App.js
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import Header from './components/Header'
import LoginScreen from './screens/LoginScreen'
import RegisterScreen from './screens/RegisterScreen'
import ProfileScreen from './screens/ProfileScreen'
import HomeScreen from './screens/HomeScreen'
import './App.css'
function App() {
return (
<Router>
<Header />
<main className='container content'>
<Routes>
<Route path='/' element={<HomeScreen />} />
<Route path='/login' element={<LoginScreen />} />
<Route path='/register' element={<RegisterScreen />} />
<Route path='/user-profile' element={<ProfileScreen />} />
</Routes>
</main>
</Router>
)
}
export default App
注册页面上的输入字段连接到React Hook Form。React Hook Form 干净利落地抓取输入值并在handleSubmit
函数中返回它们。
// Login.js
import { useForm } from 'react-hook-form'
const LoginScreen = () => {
const { register, handleSubmit } = useForm()
const submitForm = (data) => {
console.log(data.email)
}
return (
<form onSubmit={handleSubmit(submitForm)}>
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
type='email'
className='form-input'
{...register('email')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<input
type='password'
className='form-input'
{...register('password')}
required
/>
</div>
<button type='submit' className='button'>
Login
</button>
</form>
)
}
export default LoginScreen
在您的终端中,starter-files
使用以下命令克隆分支:
git clone --branch starter-files --single-branch https://github.com/Chinwike1/redux-user-auth.git
并安装所有项目依赖项:
npm install
cd frontend
npm install
上述过程在前端安装了以下包
如果需要,您可以在此处熟悉这些 Redux 术语。
最后,使用以下命令运行应用程序:
cd ..
npm run dev
Redux Toolkit 引入了一种创建 store 的新方法。它将存储的各个部分分成不同的文件,称为切片。
切片代表 Redux 状态的单个单元。它是应用程序中单个功能的 reducer 逻辑和操作的集合,通常在单个文件中一起定义。对我们来说,这个文件是features/user
.
通过使用 RTK 的createSlice
API,我们可以像这样创建一个 Redux 切片:
// features/user/userSlice.js
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
loading: false,
userInfo: {}, // for user object
userToken: null, // for storing the JWT
error: null,
success: false, // for monitoring the registration process.
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {},
})
export default userSlice.reducer
将reducer
属性 fromuserSlice
导入到 store 中,以便它反映在根 Redux 状态对象中。
// app/store.js
import { configureStore } from '@reduxjs/toolkit'
import userReducer from '../features/user/userSlice'
const store = configureStore({
reducer: {
user: userReducer
}
})
export default store
Redux Toolkit 文档建议将 action 和 reducer 合并到一个 slice 文件中。相反,当我拆分这两部分并将操作存储在单独的文件中时,我发现我的代码更具可读性,例如features/user/userActions.js
. 在这里,我们将编写向后端发出请求的异步操作。
但在此之前,我将快速概述当前可用的后端路由。
我们的 Express 服务器,托管在 上localhost:5000
,目前有 3 条路由:
api/user/login
: 登录路径。它接受POST
请求并需要用户的电子邮件和密码作为参数。然后在成功验证或错误消息后返回 JWT。此令牌的使用寿命为 12 小时api/user/register
:注册路线。它接受POST
请求并需要用户的名字、电子邮件和密码api/user/profile
:授权路线。它接受GET
请求并要求用户的令牌从数据库中获取他们的详细信息。它在授权成功或错误消息后返回用户的对象。现在我们可以继续编写 Redux 操作,从注册操作开始。
在userAction.js
中,您将使用createAsyncThunk
在将处理后的结果发送到减速器之前执行延迟的异步逻辑。
createAsyncThunk
接受三个参数:字符串操作类型、回调函数和可选options
对象。
回调函数是一个重要参数,因为它有两个您应该考虑的关键参数:
argdispatch
:这是调用动作时传递给方法的单个值。如果需要传递多个值,可以传入一个对象thunkAPI
: 包含通常传递给 Redux thunk 函数的参数的对象。参数包括getState
、dispatch
、rejectWithValue
等// userAction.js
export const registerUser = createAsyncThunk(
// action type string
'user/register',
// callback function
async ({ firstName, email, password }, { rejectWithValue }) => {
try {
// configure header's Content-Type as JSON
const config = {
headers: {
'Content-Type': 'application/json',
},
}
// make request to backend
await axios.post(
'/api/user/register',
{ firstName, email, password },
config
)
} catch (error) {
// return custom error message from API if any
if (error.response && error.response.data.message) {
return rejectWithValue(error.response.data.message)
} else {
return rejectWithValue(error.message)
}
}
}
)
In the code block above, we've taken the values from the register form and made a POST request to the register route using Axios. In the event of an error, thunkAPI.rejectWithValue sends the custom error message from the backend as a payload to the reducer. You may notice that the register API is called without referencing the server's base URL. This is possible with the proxy configuration existing in frontend/package.json.
.{
"proxy": "http://127.0.0.1:5000",
}
extraReducers
使用创建的操作createAsyncThunk
生成三种可能的生命周期操作类型:pending
、fulfilled
和rejected
。
extraReducers
您可以利用属性中的这些操作类型userSlice
对您的状态进行适当的更改。
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { registerUser } from './userActions'
const initialState = {
loading: false,
userInfo: null,
userToken: null,
error: null,
success: false,
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {
// register user
[registerUser.pending]: (state) => {
state.loading = true
state.error = null
},
[registerUser.fulfilled]: (state, { payload }) => {
state.loading = false
state.success = true // registration successful
},
[registerUser.rejected]: (state, { payload }) => {
state.loading = false
state.error = payload
},
},
})
export default userSlice.reducer
在这种情况下,我们将success
值设置true
为操作完成以表示成功注册。
useDispatch
和useSelector
钩子通过使用useSelector
和useDispatch
从react-redux
您之前安装的包中,您可以分别从 Redux 存储读取状态并从组件分派任何操作。
// RegisterScreen.js
import { useForm } from 'react-hook-form'
import { useDispatch, useSelector } from 'react-redux'
import Error from '../components/Error'
import { registerUser } from '../features/user/userActions'
const RegisterScreen = () => {
const { loading, error } = useSelector(
(state) => state.user
)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const submitForm = (data) => {
// check if passwords match
if (data.password !== data.confirmPassword) {
alert('Password mismatch')
return
}
// transform email string to lowercase to avoid case sensitivity issues during login
data.email = data.email.toLowerCase()
dispatch(registerUser(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{/* render error message with Error component, if any */}
{error && <Error>{error}</Error>}
<div className='form-group'>
<label htmlFor='firstName'>First Name</label>
<input
type='text'
className='form-input'
{...register('firstName')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
type='email'
className='form-input'
{...register('email')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<input
type='password'
className='form-input'
{...register('password')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='email'>Confirm Password</label>
<input
type='password'
className='form-input'
{...register('confirmPassword')}
required
/>
</div>
<button type='submit' className='button' disabled={loading}>
Register
</button>
</form>
)
}
export default RegisterScreen
提交表单后,我们首先验证password
andconfirmPassword
字段是否匹配。如果他们这样做了,registerUser
则分派该操作,并将表单数据作为其参数。
该useSelector
钩子用于从Redux 存储中的对象中提取loading
和error
状态值。user
然后使用这些值进行某些 UI 更改,例如在请求进行时禁用提交按钮并显示错误消息。
目前,当用户完成注册时,没有迹象表明他们所做的事情是成功的。使用React 路由器的钩子和钩子旁边的success
值,我们可以在注册后将用户重定向到登录页面。userSliceuseNavigateuseEffect
看起来是这样的:
// RegisterScreen.js
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { useDispatch, useSelector } from 'react-redux'
import { useNavigate } from 'react-router-dom'
import Error from '../components/Error'
import { registerUser } from '../features/user/userActions'
const RegisterScreen = () => {
const { loading, userInfo, error, success } = useSelector(
(state) => state.user
)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const navigate = useNavigate()
useEffect(() => {
// redirect user to login page if registration was successful
if (success) navigate('/login')
// redirect authenticated user to profile screen
if (userInfo) navigate('/user-profile')
}, [navigate, userInfo, success])
const submitForm = (data) => {
// check if passwords match
if (data.password !== data.confirmPassword) {
alert('Password mismatch')
return
}
// transform email string to lowercase to avoid case sensitivity issues in login
data.email = data.email.toLowerCase()
dispatch(registerUser(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{/* form markup... */}
</form>
)
}
export default RegisterScreen
在编写 Redux 逻辑时,您会注意到一个熟悉的模式。它通常是这样的:
它不必按那个顺序,但它们通常是重复发生的步骤。让我们通过登录操作重复此操作。
登录操作与注册操作类似,不同之处在于我们将后端返回的 JWT 存储在本地存储中,然后再将结果传递给减速器。
// userActions.js
export const userLogin = createAsyncThunk(
'user/login',
async ({ email, password }, { rejectWithValue }) => {
try {
// configure header's Content-Type as JSON
const config = {
headers: {
'Content-Type': 'application/json',
},
}
const { data } = await axios.post(
'/api/user/login',
{ email, password },
config
)
// store user's token in local storage
localStorage.setItem('userToken', data.userToken)
return data
} catch (error) {
// return custom error message from API if any
if (error.response && error.response.data.message) {
return rejectWithValue(error.response.data.message)
} else {
return rejectWithValue(error.message)
}
}
}
)
现在您可以处理userSlice.js
.
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { registerUser, userLogin } from './userActions'
// initialize userToken from local storage
const userToken = localStorage.getItem('userToken')
? localStorage.getItem('userToken')
: null
const initialState = {
loading: false,
userInfo: null,
userToken,
error: null,
success: false,
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {
// login user
[userLogin.pending]: (state) => {
state.loading = true
state.error = null
},
[userLogin.fulfilled]: (state, { payload }) => {
state.loading = false
state.userInfo = payload
state.userToken = payload.userToken
},
[userLogin.rejected]: (state, { payload }) => {
state.loading = false
state.error = payload
},
// register user reducer...
},
})
export default userSlice.reducer
因为 的值userToken
取决于来自 的令牌的值localStorage
,所以最好在开始时对其进行初始化,如上所示。
现在,您可以在提交表单时分派此操作并进行首选的 UI 更新。
// LoginScreen.js
import { useForm } from 'react-hook-form'
import { useDispatch, useSelector } from 'react-redux'
import { userLogin } from '../features/user/userActions'
import Error from '../components/Error'
const LoginScreen = () => {
const { loading, error } = useSelector((state) => state.user)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const submitForm = (data) => {
dispatch(userLogin(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{error && <Error>{error}</Error>}
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
type='email'
className='form-input'
{...register('email')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<input
type='password'
className='form-input'
{...register('password')}
required
/>
</div>
<button type='submit' className='button' disabled={loading}>
Login
</button>
</form>
)
}
export default LoginScreen
您还需要确保以前经过身份验证的用户无法访问此页面。userInfo
的值可用于使用 和 将经过身份验证的用户重定向到登录页面。useNavigateuseEffect
// LoginScreen.js
import { useForm } from 'react-hook-form'
import { useNavigate } from 'react-router-dom'
import { useDispatch, useSelector } from 'react-redux'
import { userLogin } from '../features/user/userActions'
import { useEffect } from 'react'
import Error from '../components/Error'
const LoginScreen = () => {
const { loading, userInfo, error } = useSelector((state) => state.user)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const navigate = useNavigate()
// redirect authenticated user to profile screen
useEffect(() => {
if (userInfo) {
navigate('/user-profile')
}
}, [navigate, userInfo])
const submitForm = (data) => {
dispatch(userLogin(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{/* form markup... */}
</form>
)
}
export default LoginScreen
这样就完成了注册过程!接下来,您将看到如何自动验证其有效令牌仍存储在浏览器中的用户。
getUserProfile
行动对于此操作,您将访问需要一些凭据才能与客户端请求一起使用的授权路由(用作端点)。在这里,所需的凭证是本地存储的 JWT。
与通过表单传递用户凭据的身份验证路由不同,授权路由要求使用以下语法通过HTTPAuthorization
标头Authorization: <auth-scheme> <credentials>
更安全地传递凭据:
auth-scheme
表示您希望使用的身份验证方案。JWT 旨在支持Bearer
身份验证方案,这就是我们将采用的方式。有关这方面的更多信息,请参阅RFC 6750不记名令牌。
使用 Axios,您可以配置请求的标头对象以发送 JWT:
const config = {
headers: {
Authorization: 'Bearer eyJhbGInR5cCI6IkpXVCJ9.eyJpZTI4MTI1MywiZXhwIjoxNjU1MzI0NDUzfQ.FWMexh',
},
}
因为用户的token是在Redux store中发起的,所以需要将它的值提取出来并包含在这个请求中。
createAsyncThunk
的回调函数中的第二个参数thunkAPI
,提供了一个getState
方法,可以让您读取 Redux 存储的当前值。
// userActions.js
export const getUserDetails = createAsyncThunk(
'user/getUserDetails',
async (arg, { getState, rejectWithValue }) => {
try {
// get user data from store
const { user } = getState()
// configure authorization header with user's token
const config = {
headers: {
Authorization: `Bearer ${user.userToken}`,
},
}
const { data } = await axios.get(`/api/user/profile`, config)
return data
} catch (error) {
if (error.response && error.response.data.message) {
return rejectWithValue(error.response.data.message)
} else {
return rejectWithValue(error.message)
}
}
}
)
接下来,我们将处理生命周期操作:
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { getUserDetails, registerUser, userLogin } from './userActions'
const initialState = {
// state values...
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {
// login user reducer ...
// register user reducer ...
[getUserDetails.pending]: (state) => {
state.loading = true
},
[getUserDetails.fulfilled]: (state, { payload }) => {
state.loading = false
state.userInfo = payload
},
[getUserDetails.rejected]: (state, { payload }) => {
state.loading = false
},
},
})
export default userSlice.reducer
该Header
组件是调度此操作的合适位置,因为它是在整个应用程序中保持可见的唯一组件。userToken
在这里,我们希望在应用程序注意到' 值发生变化时调度此操作。这可以通过useEffect
钩子来实现。
// Header.js
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { NavLink } from 'react-router-dom'
import { getUserDetails } from '../features/user/userActions'
import '../styles/header.css'
const Header = () => {
const { userInfo, userToken } = useSelector((state) => state.user)
const dispatch = useDispatch()
// automatically authenticate user if token is found
useEffect(() => {
if (userToken) {
dispatch(getUserDetails())
}
}, [userToken, dispatch])
return (
<header>
<div className='header-status'>
<span>
{userInfo ? `Logged in as ${userInfo.email}` : "You're not logged in"}
</span>
<div className='cta'>
{userInfo ? (
<button className='button'>
Logout
</button>
) : (
<NavLink className='button' to='/login'>
Login
</NavLink>
)}
</div>
</div>
<nav className='container navigation'>
<NavLink to='/'>Home</NavLink>
<NavLink to='/login'>Login</NavLink>
<NavLink to='/register'>Register</NavLink>
<NavLink to='/user-profile'>Profile</NavLink>
</nav>
</header>
)
}
export default Header
请注意,我们还用于userInfo
在导航栏上呈现与用户身份验证状态相关的适当消息和元素。现在您可以继续在“个人资料”屏幕上呈现用户的详细信息。
// ProfileScreen.js
import { useSelector } from 'react-redux'
import '../styles/profile.css'
const ProfileScreen = () => {
const { userInfo } = useSelector((state) => state.user)
return (
<div>
<figure>{userInfo?.firstName.charAt(0).toUpperCase()}</figure>
<span>
Welcome <strong>{userInfo?.firstName}!</strong> You can view this page
because you're logged in
</span>
</div>
)
}
export default ProfileScreen
目前,无论身份验证状态如何,每个人都可以访问“个人资料”页面。我们希望通过在授予用户访问页面之前验证用户是否存在来保护此路由。这个逻辑可以提取到单个ProtectedRoute
组件中,接下来我们将创建它。
创建一个名为routing
insrc
的文件夹和一个名为ProtectedRoute.js
. ProtectedRoute
旨在用作父路由元素,其子元素受此组件中的逻辑保护。
在这里,我们可以使用userInfo
' 值来检测用户是否登录。如果userInfo
不存在,则返回未经授权的模板。否则,我们使用 React Router 的Outlet
组件来渲染子路由。
// ProtectedRoute.js
import { useSelector } from 'react-redux'
import { NavLink, Outlet } from 'react-router-dom'
const ProtectedRoute = () => {
const { userInfo } = useSelector((state) => state.user)
// show unauthorized screen if no user is found in redux store
if (!userInfo) {
return (
<div className='unauthorized'>
<h1>Unauthorized :(</h1>
<span>
<NavLink to='/login'>Login</NavLink> to gain access
</span>
</div>
)
}
// returns child route elements
return <Outlet />
}
export default ProtectedRoute
根据文档,<Outlet>
应该在父路由元素中使用来渲染它们的子路由元素。这意味着<Outlet>
不会在屏幕上呈现任何标记,而是被子路由元素替换。
ProfileScreen
现在您可以像这样使用受保护的路由进行包装:
// App.js
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import Header from './components/Header'
import LoginScreen from './screens/LoginScreen'
import RegisterScreen from './screens/RegisterScreen'
import ProfileScreen from './screens/ProfileScreen'
import HomeScreen from './screens/HomeScreen'
import ProtectedRoute from './routing/ProtectedRoute'
import './App.css'
function App() {
return (
<Router>
<Header />
<main className='container content'>
<Routes>
<Route path='/' element={<HomeScreen />} />
<Route path='/login' element={<LoginScreen />} />
<Route path='/register' element={<RegisterScreen />} />
<Route element={<ProtectedRoute />}>
<Route path='/user-profile' element={<ProfileScreen />} />
</Route>
</Routes>
</main>
</Router>
)
}
export default App
这就是应用程序完成的大部分内容!现在让我们看看如何注销用户。
要注销用户,我们将创建一个操作,将 Redux 存储重置为其初始值并从本地存储中清除令牌。因为这不是一个异步任务,所以我们可以userSlice
使用reducer
属性直接创建它。
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { getUserDetails, registerUser, userLogin } from './userActions'
// initialize userToken from local storage
const userToken = localStorage.getItem('userToken')
? localStorage.getItem('userToken')
: null
const initialState = {
loading: false,
userInfo: null,
userToken,
error: null,
success: false,
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
logout: (state) => {
localStorage.removeItem('userToken') // deletes token from storage
state.loading = false
state.userInfo = null
state.userToken = null
state.error = null
},
},
extraReducers: {
// userLogin reducer ...
// registerUser reducer ...
// getUserDetails reducer ...
},
})
// export actions
export const { logout } = userSlice.actions
export default userSlice.reducer
并在Header
组件中调度它:
// Header.js
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { NavLink } from 'react-router-dom'
import { getUserDetails } from '../features/user/userActions'
import { logout } from '../features/user/userSlice'
import '../styles/header.css'
const Header = () => {
const { userInfo, userToken } = useSelector((state) => state.user)
const dispatch = useDispatch()
// automatically authenticate user if token is found
useEffect(() => {
if (userToken) {
dispatch(getUserDetails())
}
}, [userToken, dispatch])
return (
<header>
<div className='header-status'>
<span>
{userInfo ? `Logged in as ${userInfo.email}` : "You're not logged in"}
</span>
<div className='cta'>
{userInfo ? (
<button className='button' onClick={() => dispatch(logout())}>
Logout
</button>
) : (
<NavLink className='button' to='/login'>
Login
</NavLink>
)}
</div>
</div>
<nav className='container navigation'>
<NavLink to='/'>Home</NavLink>
<NavLink to='/login'>Login</NavLink>
<NavLink to='/register'>Register</NavLink>
<NavLink to='/user-profile'>Profile</NavLink>
</nav>
</header>
)
}
export default Header
这样就完成了我们的应用程序!您现在拥有一个 MERN 堆栈应用程序,其中包含使用 Redux Toolkit 管理的前端身份验证工作流。
在我看来,Redux Toolkit 提供了更好的开发者体验,特别是与 RTK 发布之前 Redux 的难度相比。
当我需要实现状态管理并且不想使用React.Context
.
在 WebStorage 中存储令牌,即 localStorage 和 sessionStorage,也是一个重要的讨论。当令牌的寿命很短并且不存储密码、卡详细信息等私人详细信息时,我个人认为 localStorage 是安全的。
请随时在评论中分享您个人如何处理前端身份验证!
来源:https ://blog.logrocket.com/handling-user-authentication-redux-toolkit/
1657707840
L'authentification des utilisateurs peut être gérée de multiples façons. En raison de l'importance de cette fonctionnalité, nous avons vu de plus en plus d'entreprises proposer des solutions d'authentification pour faciliter le processus - Firebase, Auth0 et NextAuth.js pour n'en nommer que quelques-unes.
Quelle que soit la manière dont ces services gèrent l'authentification et l'autorisation de leur côté, le processus de mise en œuvre implique généralement l'appel de certains points de terminaison d'API et la réception d'un jeton privé (généralement un jeton Web JSON ou JWT) à utiliser dans votre infrastructure frontale.
Dans cet article, nous allons apprendre à utiliser Redux Toolkit (RTK) pour créer un flux de travail d'authentification frontal dans React. Nous utiliserons les API essentielles de Toolkit, telles que createAsyncThunk
, pour envoyer des requêtes asynchrones à un backend Express. Nous utiliserons également createSlice
pour gérer les changements d'état.
Le backend de ce projet est construit à l'aide d'Express avec une base de données MongoDB, mais le flux de travail frontal doit toujours s'appliquer à tout service d'authentification que vous utilisez et qui fournit un jeton. Vous pouvez télécharger les fichiers source dans le référentiel du projet avec des instructions sur la configuration d'une base de données et exécuter l'application localement. Voir la démo en direct ici .
Pour suivre, vous aurez besoin de :
Maintenant, commençons à nous authentifier !
Le référentiel comprend une [starter-files]
branche qui contient les fichiers nécessaires pour démarrer cette application. Le dossier frontal comprend également les différentes interfaces utilisateur vues dans la démo, telles que les écrans Accueil , Connexion , Enregistrement et Profil avec les itinéraires respectifs /
, /login
, /register
et /user-profile
.
Voici à quoi ressemble actuellement la structure de routage :
// App.js
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import Header from './components/Header'
import LoginScreen from './screens/LoginScreen'
import RegisterScreen from './screens/RegisterScreen'
import ProfileScreen from './screens/ProfileScreen'
import HomeScreen from './screens/HomeScreen'
import './App.css'
function App() {
return (
<Router>
<Header />
<main className='container content'>
<Routes>
<Route path='/' element={<HomeScreen />} />
<Route path='/login' element={<LoginScreen />} />
<Route path='/register' element={<RegisterScreen />} />
<Route path='/user-profile' element={<ProfileScreen />} />
</Routes>
</main>
</Router>
)
}
export default App
Les champs de saisie sur les pages d'inscription sont connectés à React Hook Form . React Hook Form saisit proprement les valeurs d'entrée et les renvoie dans une handleSubmit
fonction.
// Login.js
import { useForm } from 'react-hook-form'
const LoginScreen = () => {
const { register, handleSubmit } = useForm()
const submitForm = (data) => {
console.log(data.email)
}
return (
<form onSubmit={handleSubmit(submitForm)}>
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
type='email'
className='form-input'
{...register('email')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<input
type='password'
className='form-input'
{...register('password')}
required
/>
</div>
<button type='submit' className='button'>
Login
</button>
</form>
)
}
export default LoginScreen
Dans votre terminal, clonez la starter-files
branche avec la commande suivante :
git clone --branch starter-files --single-branch https://github.com/Chinwike1/redux-user-auth.git
Et installez toutes les dépendances du projet :
npm install
cd frontend
npm install
Le processus ci-dessus installe les packages suivants sur le frontend
Vous pouvez vous familiariser avec ces termes Redux ici si vous en avez besoin.
Enfin, lancez l'application avec la commande suivante :
cd ..
npm run dev
Redux Toolkit introduit une nouvelle façon de créer un magasin . Il sépare les parties du magasin en différents fichiers, appelés tranches.
Une tranche représente une seule unité de l'état Redux. Il s'agit d'un ensemble de logiques et d'actions de réducteur pour une seule fonctionnalité de votre application, généralement définies ensemble dans un seul fichier. Pour nous, ce fichier est features/user
.
En utilisant l'API de RTK createSlice
, nous pouvons créer une tranche Redux comme ceci :
// features/user/userSlice.js
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
loading: false,
userInfo: {}, // for user object
userToken: null, // for storing the JWT
error: null,
success: false, // for monitoring the registration process.
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {},
})
export default userSlice.reducer
Importez la reducer
propriété userSlice
dans le magasin afin qu'elle se reflète dans l'objet d'état Redux racine.
// app/store.js
import { configureStore } from '@reduxjs/toolkit'
import userReducer from '../features/user/userSlice'
const store = configureStore({
reducer: {
user: userReducer
}
})
export default store
La documentation de Redux Toolkit suggère de consolider les actions et les réducteurs dans un seul fichier de tranche. Inversement, j'ai trouvé mon code plus lisible lorsque j'ai divisé ces deux parties et stocké les actions dans un fichier séparé, par exemple features/user/userActions.js
. Ici, nous allons écrire des actions asynchrones qui font des requêtes au backend.
Mais avant cela, je vais donner un aperçu rapide des routes backend actuellement disponibles.
Notre serveur Express, hébergé sur localhost:5000
, propose actuellement trois routes :
api/user/login
: la route de connexion. Il accepte POST
les requêtes et requiert l'e-mail et le mot de passe de l'utilisateur comme arguments. Il renvoie ensuite un JWT après une authentification réussie ou un message d'erreur. Ce jeton a une durée de vie de 12 heuresapi/user/register
: la route du registre. Il accepte POST
les demandes et nécessite le prénom, l'e-mail et le mot de passe de l'utilisateurapi/user/profile
: une route d'autorisation. Il accepte GET
les demandes et nécessite le jeton de l'utilisateur pour récupérer ses détails dans la base de données. Il renvoie l'objet de l'utilisateur après une autorisation réussie ou un message d'erreur.Nous pouvons maintenant passer à l'écriture des actions Redux, en commençant par l'action register.
Dans userAction.js
, vous utiliserez createAsyncThunk
pour exécuter une logique asynchrone retardée avant d'envoyer les résultats traités aux réducteurs.
createAsyncThunk
accepte trois paramètres : un type d'action de chaîne, une fonction de rappel et un options
objet facultatif.
La fonction de rappel est un paramètre important car elle a deux arguments clés que vous devez prendre en considération :
arg
: il s'agit de la valeur unique transmise à la dispatch
méthode lorsque l'action est appelée. Si vous devez transmettre plusieurs valeurs, vous pouvez transmettre un objetthunkAPI
: un objet contenant des paramètres normalement passés à une fonction thunk Redux. Les paramètres incluent getState
, dispatch
, rejectWithValue
, etc.// userAction.js
export const registerUser = createAsyncThunk(
// action type string
'user/register',
// callback function
async ({ firstName, email, password }, { rejectWithValue }) => {
try {
// configure header's Content-Type as JSON
const config = {
headers: {
'Content-Type': 'application/json',
},
}
// make request to backend
await axios.post(
'/api/user/register',
{ firstName, email, password },
config
)
} catch (error) {
// return custom error message from API if any
if (error.response && error.response.data.message) {
return rejectWithValue(error.response.data.message)
} else {
return rejectWithValue(error.message)
}
}
}
)
In the code block above, we've taken the values from the register form and made a POST request to the register route using Axios. In the event of an error, thunkAPI.rejectWithValue sends the custom error message from the backend as a payload to the reducer. You may notice that the register API is called without referencing the server's base URL. This is possible with the proxy configuration existing in frontend/package.json.
.{
"proxy": "http://127.0.0.1:5000",
}
extraReducers
Les actions créées avec createAsyncThunk
génèrent trois types d'action de cycle de vie possibles : pending
, fulfilled
et rejected
.
Vous pouvez utiliser ces types d'action dans la extraReducers
propriété de userSlice
pour apporter les modifications appropriées à votre état.
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { registerUser } from './userActions'
const initialState = {
loading: false,
userInfo: null,
userToken: null,
error: null,
success: false,
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {
// register user
[registerUser.pending]: (state) => {
state.loading = true
state.error = null
},
[registerUser.fulfilled]: (state, { payload }) => {
state.loading = false
state.success = true // registration successful
},
[registerUser.rejected]: (state, { payload }) => {
state.loading = false
state.error = payload
},
},
})
export default userSlice.reducer
Dans ce cas, nous définissons la success
valeur true
lorsque l'action est accomplie pour signifier un enregistrement réussi.
useDispatch
et useSelector
crochetsEn utilisant useSelector
et à useDispatch
partir du react-redux
package que vous avez installé précédemment, vous pouvez lire l'état d'un magasin Redux et envoyer toute action à partir d'un composant, respectivement.
// RegisterScreen.js
import { useForm } from 'react-hook-form'
import { useDispatch, useSelector } from 'react-redux'
import Error from '../components/Error'
import { registerUser } from '../features/user/userActions'
const RegisterScreen = () => {
const { loading, error } = useSelector(
(state) => state.user
)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const submitForm = (data) => {
// check if passwords match
if (data.password !== data.confirmPassword) {
alert('Password mismatch')
return
}
// transform email string to lowercase to avoid case sensitivity issues during login
data.email = data.email.toLowerCase()
dispatch(registerUser(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{/* render error message with Error component, if any */}
{error && <Error>{error}</Error>}
<div className='form-group'>
<label htmlFor='firstName'>First Name</label>
<input
type='text'
className='form-input'
{...register('firstName')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
type='email'
className='form-input'
{...register('email')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<input
type='password'
className='form-input'
{...register('password')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='email'>Confirm Password</label>
<input
type='password'
className='form-input'
{...register('confirmPassword')}
required
/>
</div>
<button type='submit' className='button' disabled={loading}>
Register
</button>
</form>
)
}
export default RegisterScreen
Lorsque le formulaire est soumis, nous commençons par vérifier si les champs password
et correspondent. confirmPassword
Si c'est le cas, l' registerUser
action est envoyée en prenant les données du formulaire comme argument.
Le useSelector
crochet est utilisé pour extraire les valeurs d'état loading
et de l' objet dans le magasin Redux. Ces valeurs sont ensuite utilisées pour effectuer certaines modifications de l'interface utilisateur, comme la désactivation du bouton d'envoi pendant que la demande est en cours et l'affichage d'un message d'erreur.erroruser
Actuellement, lorsqu'un utilisateur termine son inscription, rien n'indique que ce qu'il a fait a réussi. Avec la success
valeur à userSlice
côté du crochet du routeur React useNavigate
et du useEffect
crochet, nous pouvons rediriger l'utilisateur vers la page de connexion après l'inscription.
Voici à quoi cela ressemblerait :
// RegisterScreen.js
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { useDispatch, useSelector } from 'react-redux'
import { useNavigate } from 'react-router-dom'
import Error from '../components/Error'
import { registerUser } from '../features/user/userActions'
const RegisterScreen = () => {
const { loading, userInfo, error, success } = useSelector(
(state) => state.user
)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const navigate = useNavigate()
useEffect(() => {
// redirect user to login page if registration was successful
if (success) navigate('/login')
// redirect authenticated user to profile screen
if (userInfo) navigate('/user-profile')
}, [navigate, userInfo, success])
const submitForm = (data) => {
// check if passwords match
if (data.password !== data.confirmPassword) {
alert('Password mismatch')
return
}
// transform email string to lowercase to avoid case sensitivity issues in login
data.email = data.email.toLowerCase()
dispatch(registerUser(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{/* form markup... */}
</form>
)
}
export default RegisterScreen
Vous remarquerez un schéma familier lors de l'écriture de la logique Redux. Cela se passe généralement comme ceci :
Il n'est pas nécessaire que ce soit dans cet ordre, mais ce sont généralement des étapes récurrentes. Répétons cela avec l'action de connexion.
L'action de connexion sera similaire à l'action d'enregistrement, sauf qu'ici nous stockons le JWT renvoyé par le backend dans le stockage local avant de transmettre le résultat au réducteur.
// userActions.js
export const userLogin = createAsyncThunk(
'user/login',
async ({ email, password }, { rejectWithValue }) => {
try {
// configure header's Content-Type as JSON
const config = {
headers: {
'Content-Type': 'application/json',
},
}
const { data } = await axios.post(
'/api/user/login',
{ email, password },
config
)
// store user's token in local storage
localStorage.setItem('userToken', data.userToken)
return data
} catch (error) {
// return custom error message from API if any
if (error.response && error.response.data.message) {
return rejectWithValue(error.response.data.message)
} else {
return rejectWithValue(error.message)
}
}
}
)
Vous pouvez désormais gérer les types d'action de cycle de vie dans userSlice.js
.
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { registerUser, userLogin } from './userActions'
// initialize userToken from local storage
const userToken = localStorage.getItem('userToken')
? localStorage.getItem('userToken')
: null
const initialState = {
loading: false,
userInfo: null,
userToken,
error: null,
success: false,
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {
// login user
[userLogin.pending]: (state) => {
state.loading = true
state.error = null
},
[userLogin.fulfilled]: (state, { payload }) => {
state.loading = false
state.userInfo = payload
state.userToken = payload.userToken
},
[userLogin.rejected]: (state, { payload }) => {
state.loading = false
state.error = payload
},
// register user reducer...
},
})
export default userSlice.reducer
Étant donné que la valeur de userToken
dépend de la valeur du jeton de localStorage
, il est préférable de l'initialiser au début, comme indiqué ci-dessus.
Vous pouvez maintenant envoyer cette action lorsque le formulaire est soumis et effectuer vos mises à jour préférées de l'interface utilisateur.
// LoginScreen.js
import { useForm } from 'react-hook-form'
import { useDispatch, useSelector } from 'react-redux'
import { userLogin } from '../features/user/userActions'
import Error from '../components/Error'
const LoginScreen = () => {
const { loading, error } = useSelector((state) => state.user)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const submitForm = (data) => {
dispatch(userLogin(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{error && <Error>{error}</Error>}
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
type='email'
className='form-input'
{...register('email')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<input
type='password'
className='form-input'
{...register('password')}
required
/>
</div>
<button type='submit' className='button' disabled={loading}>
Login
</button>
</form>
)
}
export default LoginScreen
Vous voulez également vous assurer que les utilisateurs précédemment authentifiés ne peuvent pas accéder à cette page. userInfo
La valeur de peut être utilisée pour rediriger un utilisateur authentifié vers la page de connexion avec useNavigate
et useEffect
.
// LoginScreen.js
import { useForm } from 'react-hook-form'
import { useNavigate } from 'react-router-dom'
import { useDispatch, useSelector } from 'react-redux'
import { userLogin } from '../features/user/userActions'
import { useEffect } from 'react'
import Error from '../components/Error'
const LoginScreen = () => {
const { loading, userInfo, error } = useSelector((state) => state.user)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const navigate = useNavigate()
// redirect authenticated user to profile screen
useEffect(() => {
if (userInfo) {
navigate('/user-profile')
}
}, [navigate, userInfo])
const submitForm = (data) => {
dispatch(userLogin(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{/* form markup... */}
</form>
)
}
export default LoginScreen
Et cela achève le processus d'inscription ! Ensuite, vous verrez comment authentifier automatiquement un utilisateur dont le jeton valide est toujours stocké dans le navigateur.
getUserProfile
actionPour cette action, vous accéderez à une route d'autorisation (servant de point de terminaison) qui nécessite des informations d'identification pour accompagner la demande du client. Ici, les informations d'identification requises sont le JWT stocké localement.
Contrairement aux routes d'authentification où les informations d'identification de l'utilisateur étaient transmises via des formulaires, les routes d'autorisation nécessitent que les informations d'identification soient transmises de manière plus sécurisée via des en -têtes HTTPAuthorization
à l'aide de cette syntaxe : Authorization: <auth-scheme> <credentials>
.
auth-scheme
représente le schéma d'authentification que vous souhaitez utiliser. Les JWT sont conçus pour prendre en charge le Bearer
schéma d'authentification, et c'est ce que nous allons faire. Voir jetons de porteur RFC 6750 pour plus d'informations à ce sujet.
Avec Axios, vous pouvez configurer l'objet d'en-tête de la requête pour envoyer le JWT :
const config = {
headers: {
Authorization: 'Bearer eyJhbGInR5cCI6IkpXVCJ9.eyJpZTI4MTI1MywiZXhwIjoxNjU1MzI0NDUzfQ.FWMexh',
},
}
Étant donné que le jeton de l'utilisateur est initié dans le magasin Redux, sa valeur doit être extraite et incluse dans cette requête.
Le deuxième paramètre de createAsyncThunk
la fonction de rappel de thunkAPI
, fournit une getState
méthode qui vous permet de lire la valeur actuelle du magasin Redux.
// userActions.js
export const getUserDetails = createAsyncThunk(
'user/getUserDetails',
async (arg, { getState, rejectWithValue }) => {
try {
// get user data from store
const { user } = getState()
// configure authorization header with user's token
const config = {
headers: {
Authorization: `Bearer ${user.userToken}`,
},
}
const { data } = await axios.get(`/api/user/profile`, config)
return data
} catch (error) {
if (error.response && error.response.data.message) {
return rejectWithValue(error.response.data.message)
} else {
return rejectWithValue(error.message)
}
}
}
)
Ensuite, nous allons gérer les actions du cycle de vie :
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { getUserDetails, registerUser, userLogin } from './userActions'
const initialState = {
// state values...
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {
// login user reducer ...
// register user reducer ...
[getUserDetails.pending]: (state) => {
state.loading = true
},
[getUserDetails.fulfilled]: (state, { payload }) => {
state.loading = false
state.userInfo = payload
},
[getUserDetails.rejected]: (state, { payload }) => {
state.loading = false
},
},
})
export default userSlice.reducer
Le Header
composant est un emplacement approprié pour envoyer cette action, car c'est le seul composant qui reste visible dans toute l'application. Ici, nous voulons que cette action soit envoyée lorsque l'application remarque un changement dans userToken
la valeur de . Ceci peut être réalisé avec le useEffect
crochet.
// Header.js
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { NavLink } from 'react-router-dom'
import { getUserDetails } from '../features/user/userActions'
import '../styles/header.css'
const Header = () => {
const { userInfo, userToken } = useSelector((state) => state.user)
const dispatch = useDispatch()
// automatically authenticate user if token is found
useEffect(() => {
if (userToken) {
dispatch(getUserDetails())
}
}, [userToken, dispatch])
return (
<header>
<div className='header-status'>
<span>
{userInfo ? `Logged in as ${userInfo.email}` : "You're not logged in"}
</span>
<div className='cta'>
{userInfo ? (
<button className='button'>
Logout
</button>
) : (
<NavLink className='button' to='/login'>
Login
</NavLink>
)}
</div>
</div>
<nav className='container navigation'>
<NavLink to='/'>Home</NavLink>
<NavLink to='/login'>Login</NavLink>
<NavLink to='/register'>Register</NavLink>
<NavLink to='/user-profile'>Profile</NavLink>
</nav>
</header>
)
}
export default Header
Notez que nous avions également l'habitude userInfo
d'afficher des messages et des éléments appropriés sur la barre de navigation qui correspondent au statut d'authentification de l'utilisateur. Vous pouvez maintenant passer à l'affichage des détails de l'utilisateur sur l' écran Profil .
// ProfileScreen.js
import { useSelector } from 'react-redux'
import '../styles/profile.css'
const ProfileScreen = () => {
const { userInfo } = useSelector((state) => state.user)
return (
<div>
<figure>{userInfo?.firstName.charAt(0).toUpperCase()}</figure>
<span>
Welcome <strong>{userInfo?.firstName}!</strong> You can view this page
because you're logged in
</span>
</div>
)
}
export default ProfileScreen
Actuellement, la page Profil est accessible à tous, quel que soit le statut d'authentification. Nous voulons protéger cette route en vérifiant si un utilisateur existe avant de lui accorder l'accès à la page. Cette logique peut être extraite en un seul ProtectedRoute
composant, et nous allons le créer ensuite.
Créez un dossier appelé routing
dans src
et un fichier nommé ProtectedRoute.js
. ProtectedRoute
est destiné à être utilisé comme élément de route parent, dont les éléments enfants sont protégés par la logique résidant dans ce composant.
Ici, nous pouvons utiliser userInfo
la valeur de pour détecter si un utilisateur est connecté. Si userInfo
est absent, un modèle non autorisé est renvoyé. Sinon, nous utilisons le composant de React Router Outlet
pour restituer les routes enfants.
// ProtectedRoute.js
import { useSelector } from 'react-redux'
import { NavLink, Outlet } from 'react-router-dom'
const ProtectedRoute = () => {
const { userInfo } = useSelector((state) => state.user)
// show unauthorized screen if no user is found in redux store
if (!userInfo) {
return (
<div className='unauthorized'>
<h1>Unauthorized :(</h1>
<span>
<NavLink to='/login'>Login</NavLink> to gain access
</span>
</div>
)
}
// returns child route elements
return <Outlet />
}
export default ProtectedRoute
Selon la documentation , <Outlet>
doit être utilisé dans les éléments de route parents pour restituer leurs éléments de route enfants. Cela signifie que <Outlet>
cela n'affiche aucun balisage à l'écran, mais est remplacé par les éléments de route enfants.
Vous pouvez maintenant envelopper ProfileScreen
la route protégée en tant que telle :
// App.js
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import Header from './components/Header'
import LoginScreen from './screens/LoginScreen'
import RegisterScreen from './screens/RegisterScreen'
import ProfileScreen from './screens/ProfileScreen'
import HomeScreen from './screens/HomeScreen'
import ProtectedRoute from './routing/ProtectedRoute'
import './App.css'
function App() {
return (
<Router>
<Header />
<main className='container content'>
<Routes>
<Route path='/' element={<HomeScreen />} />
<Route path='/login' element={<LoginScreen />} />
<Route path='/register' element={<RegisterScreen />} />
<Route element={<ProtectedRoute />}>
<Route path='/user-profile' element={<ProfileScreen />} />
</Route>
</Routes>
</main>
</Router>
)
}
export default App
Et c'est la plupart de l'application complète! Voyons maintenant comment déconnecter un utilisateur.
Pour déconnecter un utilisateur, nous allons créer une action qui réinitialise le magasin Redux à sa valeur initiale et efface le jeton du stockage local. Comme il ne s'agit pas d'une tâche asynchrone, nous pouvons la créer directement userSlice
avec la reducer
propriété.
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { getUserDetails, registerUser, userLogin } from './userActions'
// initialize userToken from local storage
const userToken = localStorage.getItem('userToken')
? localStorage.getItem('userToken')
: null
const initialState = {
loading: false,
userInfo: null,
userToken,
error: null,
success: false,
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
logout: (state) => {
localStorage.removeItem('userToken') // deletes token from storage
state.loading = false
state.userInfo = null
state.userToken = null
state.error = null
},
},
extraReducers: {
// userLogin reducer ...
// registerUser reducer ...
// getUserDetails reducer ...
},
})
// export actions
export const { logout } = userSlice.actions
export default userSlice.reducer
Et répartissez-le dans le Header
composant :
// Header.js
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { NavLink } from 'react-router-dom'
import { getUserDetails } from '../features/user/userActions'
import { logout } from '../features/user/userSlice'
import '../styles/header.css'
const Header = () => {
const { userInfo, userToken } = useSelector((state) => state.user)
const dispatch = useDispatch()
// automatically authenticate user if token is found
useEffect(() => {
if (userToken) {
dispatch(getUserDetails())
}
}, [userToken, dispatch])
return (
<header>
<div className='header-status'>
<span>
{userInfo ? `Logged in as ${userInfo.email}` : "You're not logged in"}
</span>
<div className='cta'>
{userInfo ? (
<button className='button' onClick={() => dispatch(logout())}>
Logout
</button>
) : (
<NavLink className='button' to='/login'>
Login
</NavLink>
)}
</div>
</div>
<nav className='container navigation'>
<NavLink to='/'>Home</NavLink>
<NavLink to='/login'>Login</NavLink>
<NavLink to='/register'>Register</NavLink>
<NavLink to='/user-profile'>Profile</NavLink>
</nav>
</header>
)
}
export default Header
Et cela complète notre application! Vous avez maintenant une application de pile MERN avec un flux de travail d'authentification frontal géré avec Redux Toolkit.
À mon avis, Redux Toolkit offre une meilleure expérience de développement, en particulier par rapport à la difficulté de Redux avant la sortie de RTK.
Je trouve Toolkit facile à connecter à mes applications lorsque j'ai besoin d'implémenter la gestion d'état et que je ne veux pas le créer à partir de zéro en utilisant React.Context
.
Le stockage des jetons dans WebStorage, c'est-à-dire localStorage et sessionStorage, est également une discussion importante . Personnellement, je trouve localStorage sûr lorsque le jeton a une courte durée de vie et ne stocke pas de détails privés tels que les mots de passe, les détails de la carte, etc.
N'hésitez pas à partager comment vous gérez personnellement l'authentification frontale dans les commentaires !
Source : https://blog.logrocket.com/handling-user-authentication-redux-toolkit/
1657707420
A autenticação do usuário pode ser tratada de várias maneiras. Devido à importância desse recurso, vimos mais empresas fornecerem soluções de autenticação para facilitar o processo — Firebase, Auth0 e NextAuth.js, para citar alguns.
Independentemente de como esses serviços lidam com autenticação e autorização, o processo de implementação normalmente envolve chamar alguns terminais de API e receber um token privado (geralmente um JSON Web Token ou JWT) para ser usado em sua infraestrutura de front-end.
Neste artigo, aprenderemos como usar o Redux Toolkit (RTK) para criar um fluxo de trabalho de autenticação de front-end no React. Usaremos APIs essenciais do Toolkit, como createAsyncThunk
, para fazer solicitações assíncronas a um back-end Express. Também usaremos createSlice
para lidar com mudanças de estado.
O back-end para este projeto é criado usando o Express com um banco de dados MongoDB, mas o fluxo de trabalho do front-end ainda deve ser aplicado a qualquer serviço de autenticação que você usar que forneça um token. Você pode baixar os arquivos de origem no repositório do projeto com instruções sobre como configurar um banco de dados e pode executar o aplicativo localmente. Veja a demonstração ao vivo aqui .
Para acompanhar, você precisará de:
Agora, vamos começar a autenticar!
O repositório inclui uma [starter-files]
ramificação que contém os arquivos necessários para inicializar esse aplicativo. A pasta frontend também inclui as várias interfaces de usuário vistas na demonstração, como as telas Home , Login , Register e Profile com as respectivas rotas /
, /login
, /register
e /user-profile
.
Veja como está a estrutura de roteamento atualmente:
// App.js
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import Header from './components/Header'
import LoginScreen from './screens/LoginScreen'
import RegisterScreen from './screens/RegisterScreen'
import ProfileScreen from './screens/ProfileScreen'
import HomeScreen from './screens/HomeScreen'
import './App.css'
function App() {
return (
<Router>
<Header />
<main className='container content'>
<Routes>
<Route path='/' element={<HomeScreen />} />
<Route path='/login' element={<LoginScreen />} />
<Route path='/register' element={<RegisterScreen />} />
<Route path='/user-profile' element={<ProfileScreen />} />
</Routes>
</main>
</Router>
)
}
export default App
Os campos de entrada nas páginas de registro são conectados ao React Hook Form . O React Hook Form pega os valores de entrada de forma limpa e os retorna em uma handleSubmit
função.
// Login.js
import { useForm } from 'react-hook-form'
const LoginScreen = () => {
const { register, handleSubmit } = useForm()
const submitForm = (data) => {
console.log(data.email)
}
return (
<form onSubmit={handleSubmit(submitForm)}>
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
type='email'
className='form-input'
{...register('email')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<input
type='password'
className='form-input'
{...register('password')}
required
/>
</div>
<button type='submit' className='button'>
Login
</button>
</form>
)
}
export default LoginScreen
No seu terminal, clone o starter-files
branch com o seguinte comando:
git clone --branch starter-files --single-branch https://github.com/Chinwike1/redux-user-auth.git
E instale todas as dependências do projeto:
npm install
cd frontend
npm install
O processo acima instala os seguintes pacotes no frontend
Você pode se familiarizar com esses termos do Redux aqui, se precisar.
Por fim, execute o aplicativo com o seguinte comando:
cd ..
npm run dev
O Redux Toolkit apresenta uma nova maneira de criar uma loja . Ele separa partes da loja em diferentes arquivos, conhecidos como fatias.
Uma fatia representa uma única unidade do estado Redux. É uma coleção de lógica redutora e ações para um único recurso em seu aplicativo, normalmente definido em conjunto em um único arquivo. Para nós, este arquivo é features/user
.
Usando a API do RTK createSlice
, podemos criar uma fatia do Redux assim:
// features/user/userSlice.js
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
loading: false,
userInfo: {}, // for user object
userToken: null, // for storing the JWT
error: null,
success: false, // for monitoring the registration process.
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {},
})
export default userSlice.reducer
Importe a reducer
propriedade da userSlice
loja para que ela reflita no objeto de estado Redux raiz.
// app/store.js
import { configureStore } from '@reduxjs/toolkit'
import userReducer from '../features/user/userSlice'
const store = configureStore({
reducer: {
user: userReducer
}
})
export default store
Os documentos do Redux Toolkit sugerem consolidar ações e redutores em um arquivo de fatia. Por outro lado, descobri que meu código é mais legível quando divido essas duas partes e armazeno as ações em um arquivo separado, por exemplo, features/user/userActions.js
. Aqui, escreveremos ações assíncronas que fazem solicitações ao back-end.
Mas antes disso, darei uma rápida visão geral das rotas de back-end atualmente disponíveis.
Nosso servidor Express, hospedado em localhost:5000
, atualmente possui três rotas:
api/user/login
: a rota de login. Ele aceita POST
solicitações e requer o e-mail e a senha do usuário como argumentos. Em seguida, ele retorna um JWT após a autenticação bem-sucedida ou uma mensagem de erro. Este token tem uma vida útil de 12 horasapi/user/register
: a rota de registro. Ele aceita POST
solicitações e exige o nome, e-mail e senha do usuárioapi/user/profile
: uma rota de autorização. Ele aceita GET
solicitações e requer o token do usuário para buscar seus detalhes no banco de dados. Ele retorna o objeto do usuário após a autorização bem-sucedida ou uma mensagem de erro.Agora podemos passar a escrever ações do Redux, começando com a ação de registro.
Em userAction.js
, você usará createAsyncThunk
para executar lógica assíncrona atrasada antes de enviar os resultados processados para os redutores.
createAsyncThunk
aceita três parâmetros: um tipo de ação de string, uma função de retorno de chamada e um options
objeto opcional.
A função callback é um parâmetro importante, pois possui dois argumentos principais que você deve levar em consideração:
arg
: este é o valor único passado para o dispatch
método quando a ação é chamada. Se você precisar passar vários valores, poderá passar um objetothunkAPI
: um objeto contendo parâmetros normalmente passados para uma função de conversão do Redux. Os parâmetros incluem getState
, dispatch
, rejectWithValue
e mais// userAction.js
export const registerUser = createAsyncThunk(
// action type string
'user/register',
// callback function
async ({ firstName, email, password }, { rejectWithValue }) => {
try {
// configure header's Content-Type as JSON
const config = {
headers: {
'Content-Type': 'application/json',
},
}
// make request to backend
await axios.post(
'/api/user/register',
{ firstName, email, password },
config
)
} catch (error) {
// return custom error message from API if any
if (error.response && error.response.data.message) {
return rejectWithValue(error.response.data.message)
} else {
return rejectWithValue(error.message)
}
}
}
)
In the code block above, we've taken the values from the register form and made a POST request to the register route using Axios. In the event of an error, thunkAPI.rejectWithValue sends the custom error message from the backend as a payload to the reducer. You may notice that the register API is called without referencing the server's base URL. This is possible with the proxy configuration existing in frontend/package.json.
.{
"proxy": "http://127.0.0.1:5000",
}
extraReducers
As ações criadas com createAsyncThunk
geram três tipos de ação de ciclo de vida possíveis: pending
, fulfilled
e rejected
.
Você pode utilizar esses tipos de ação na extraReducers
propriedade de userSlice
para fazer as alterações apropriadas em seu estado.
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { registerUser } from './userActions'
const initialState = {
loading: false,
userInfo: null,
userToken: null,
error: null,
success: false,
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {
// register user
[registerUser.pending]: (state) => {
state.loading = true
state.error = null
},
[registerUser.fulfilled]: (state, { payload }) => {
state.loading = false
state.success = true // registration successful
},
[registerUser.rejected]: (state, { payload }) => {
state.loading = false
state.error = payload
},
},
})
export default userSlice.reducer
Nesse caso, definimos o success
valor para true
quando a ação for cumprida para significar um registro bem-sucedido.
useDispatch
e useSelector
ganchosAo usar useSelector
e useDispatch
do react-redux
pacote que você instalou anteriormente, você pode ler o estado de uma loja Redux e despachar qualquer ação de um componente, respectivamente.
// RegisterScreen.js
import { useForm } from 'react-hook-form'
import { useDispatch, useSelector } from 'react-redux'
import Error from '../components/Error'
import { registerUser } from '../features/user/userActions'
const RegisterScreen = () => {
const { loading, error } = useSelector(
(state) => state.user
)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const submitForm = (data) => {
// check if passwords match
if (data.password !== data.confirmPassword) {
alert('Password mismatch')
return
}
// transform email string to lowercase to avoid case sensitivity issues during login
data.email = data.email.toLowerCase()
dispatch(registerUser(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{/* render error message with Error component, if any */}
{error && <Error>{error}</Error>}
<div className='form-group'>
<label htmlFor='firstName'>First Name</label>
<input
type='text'
className='form-input'
{...register('firstName')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
type='email'
className='form-input'
{...register('email')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<input
type='password'
className='form-input'
{...register('password')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='email'>Confirm Password</label>
<input
type='password'
className='form-input'
{...register('confirmPassword')}
required
/>
</div>
<button type='submit' className='button' disabled={loading}>
Register
</button>
</form>
)
}
export default RegisterScreen
Quando o formulário é enviado, começamos verificando se os campos password
e correspondem. confirmPassword
Se o fizerem, a registerUser
ação é despachada, tomando os dados do formulário como seu argumento.
O useSelector
gancho é usado para extrair os valores de estado loading
e do objeto no armazenamento do Redux. Esses valores são usados para fazer algumas alterações na interface do usuário, como desabilitar o botão de envio enquanto a solicitação está em andamento e exibir uma mensagem de erro.erroruser
Atualmente, quando um usuário conclui o registro, não há indicação de que o que ele fez foi bem-sucedido. Com o success
valor ao lado do gancho e do gancho userSlice
do roteador React , podemos redirecionar o usuário para a página de login após o registro.useNavigateuseEffect
Veja como isso ficaria:
// RegisterScreen.js
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { useDispatch, useSelector } from 'react-redux'
import { useNavigate } from 'react-router-dom'
import Error from '../components/Error'
import { registerUser } from '../features/user/userActions'
const RegisterScreen = () => {
const { loading, userInfo, error, success } = useSelector(
(state) => state.user
)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const navigate = useNavigate()
useEffect(() => {
// redirect user to login page if registration was successful
if (success) navigate('/login')
// redirect authenticated user to profile screen
if (userInfo) navigate('/user-profile')
}, [navigate, userInfo, success])
const submitForm = (data) => {
// check if passwords match
if (data.password !== data.confirmPassword) {
alert('Password mismatch')
return
}
// transform email string to lowercase to avoid case sensitivity issues in login
data.email = data.email.toLowerCase()
dispatch(registerUser(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{/* form markup... */}
</form>
)
}
export default RegisterScreen
Há um padrão familiar que você notará ao escrever a lógica do Redux. Geralmente fica assim:
Não precisa estar nessa ordem, mas geralmente são etapas recorrentes. Vamos repetir isso com a ação de login.
A ação de login será semelhante à ação de registro, exceto que aqui armazenamos o JWT retornado do backend no armazenamento local antes de passar o resultado para o redutor.
// userActions.js
export const userLogin = createAsyncThunk(
'user/login',
async ({ email, password }, { rejectWithValue }) => {
try {
// configure header's Content-Type as JSON
const config = {
headers: {
'Content-Type': 'application/json',
},
}
const { data } = await axios.post(
'/api/user/login',
{ email, password },
config
)
// store user's token in local storage
localStorage.setItem('userToken', data.userToken)
return data
} catch (error) {
// return custom error message from API if any
if (error.response && error.response.data.message) {
return rejectWithValue(error.response.data.message)
} else {
return rejectWithValue(error.message)
}
}
}
)
Agora você pode manipular os tipos de ação do ciclo de vida no userSlice.js
.
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { registerUser, userLogin } from './userActions'
// initialize userToken from local storage
const userToken = localStorage.getItem('userToken')
? localStorage.getItem('userToken')
: null
const initialState = {
loading: false,
userInfo: null,
userToken,
error: null,
success: false,
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {
// login user
[userLogin.pending]: (state) => {
state.loading = true
state.error = null
},
[userLogin.fulfilled]: (state, { payload }) => {
state.loading = false
state.userInfo = payload
state.userToken = payload.userToken
},
[userLogin.rejected]: (state, { payload }) => {
state.loading = false
state.error = payload
},
// register user reducer...
},
})
export default userSlice.reducer
Como o valor de userToken
depende do valor do token de localStorage
, é melhor inicializá-lo no início, conforme mostrado acima.
Agora você pode despachar essa ação quando o formulário for enviado e fazer suas atualizações de interface do usuário preferidas.
// LoginScreen.js
import { useForm } from 'react-hook-form'
import { useDispatch, useSelector } from 'react-redux'
import { userLogin } from '../features/user/userActions'
import Error from '../components/Error'
const LoginScreen = () => {
const { loading, error } = useSelector((state) => state.user)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const submitForm = (data) => {
dispatch(userLogin(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{error && <Error>{error}</Error>}
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
type='email'
className='form-input'
{...register('email')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<input
type='password'
className='form-input'
{...register('password')}
required
/>
</div>
<button type='submit' className='button' disabled={loading}>
Login
</button>
</form>
)
}
export default LoginScreen
Você também deseja garantir que usuários autenticados anteriormente não possam acessar esta página. userInfo
O valor de pode ser usado para redirecionar um usuário autenticado para a página de login com useNavigate
e useEffect
.
// LoginScreen.js
import { useForm } from 'react-hook-form'
import { useNavigate } from 'react-router-dom'
import { useDispatch, useSelector } from 'react-redux'
import { userLogin } from '../features/user/userActions'
import { useEffect } from 'react'
import Error from '../components/Error'
const LoginScreen = () => {
const { loading, userInfo, error } = useSelector((state) => state.user)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const navigate = useNavigate()
// redirect authenticated user to profile screen
useEffect(() => {
if (userInfo) {
navigate('/user-profile')
}
}, [navigate, userInfo])
const submitForm = (data) => {
dispatch(userLogin(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{/* form markup... */}
</form>
)
}
export default LoginScreen
E isso completa o processo de registro! A seguir, você verá como autenticar automaticamente um usuário cujo token válido ainda está armazenado no navegador.
getUserProfile
açãoPara esta ação, você entrará em contato com uma rota de autorização (servindo como ponto de extremidade) que requer algumas credenciais para acompanhar a solicitação do cliente. Aqui, a credencial necessária é o JWT armazenado localmente.
Ao contrário das rotas de autenticação em que as credenciais do usuário foram passadas por meio de formulários, as rotas de autorização exigem que as credenciais sejam passadas com mais segurança por meio de cabeçalhos HTTPAuthorization
usando esta sintaxe: Authorization: <auth-scheme> <credentials>
.
auth-scheme
representa qual esquema de autenticação você deseja usar. Os JWTs são projetados para suportar o Bearer
esquema de autenticação, e é com isso que iremos. Consulte tokens de portador RFC 6750 para obter mais informações sobre isso.
Com o Axios, você pode configurar o objeto de cabeçalho da solicitação para enviar o JWT:
const config = {
headers: {
Authorization: 'Bearer eyJhbGInR5cCI6IkpXVCJ9.eyJpZTI4MTI1MywiZXhwIjoxNjU1MzI0NDUzfQ.FWMexh',
},
}
Como o token do usuário é iniciado no repositório Redux, seu valor precisa ser extraído e incluído nesta solicitação.
O segundo parâmetro na createAsyncThunk
função de retorno de chamada do , thunkAPI
, fornece um getState
método que permite ler o valor atual do armazenamento Redux.
// userActions.js
export const getUserDetails = createAsyncThunk(
'user/getUserDetails',
async (arg, { getState, rejectWithValue }) => {
try {
// get user data from store
const { user } = getState()
// configure authorization header with user's token
const config = {
headers: {
Authorization: `Bearer ${user.userToken}`,
},
}
const { data } = await axios.get(`/api/user/profile`, config)
return data
} catch (error) {
if (error.response && error.response.data.message) {
return rejectWithValue(error.response.data.message)
} else {
return rejectWithValue(error.message)
}
}
}
)
Em seguida, lidaremos com as ações do ciclo de vida:
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { getUserDetails, registerUser, userLogin } from './userActions'
const initialState = {
// state values...
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {
// login user reducer ...
// register user reducer ...
[getUserDetails.pending]: (state) => {
state.loading = true
},
[getUserDetails.fulfilled]: (state, { payload }) => {
state.loading = false
state.userInfo = payload
},
[getUserDetails.rejected]: (state, { payload }) => {
state.loading = false
},
},
})
export default userSlice.reducer
O Header
componente é um local adequado para despachar essa ação, pois é o único componente que permanece visível em todo o aplicativo. Aqui, queremos que essa ação seja despachada quando o aplicativo perceber uma alteração no userToken
valor de . Isto pode ser conseguido com o useEffect
gancho.
// Header.js
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { NavLink } from 'react-router-dom'
import { getUserDetails } from '../features/user/userActions'
import '../styles/header.css'
const Header = () => {
const { userInfo, userToken } = useSelector((state) => state.user)
const dispatch = useDispatch()
// automatically authenticate user if token is found
useEffect(() => {
if (userToken) {
dispatch(getUserDetails())
}
}, [userToken, dispatch])
return (
<header>
<div className='header-status'>
<span>
{userInfo ? `Logged in as ${userInfo.email}` : "You're not logged in"}
</span>
<div className='cta'>
{userInfo ? (
<button className='button'>
Logout
</button>
) : (
<NavLink className='button' to='/login'>
Login
</NavLink>
)}
</div>
</div>
<nav className='container navigation'>
<NavLink to='/'>Home</NavLink>
<NavLink to='/login'>Login</NavLink>
<NavLink to='/register'>Register</NavLink>
<NavLink to='/user-profile'>Profile</NavLink>
</nav>
</header>
)
}
export default Header
Observe que também costumávamos userInfo
renderizar mensagens e elementos apropriados na barra de navegação que se correlacionam com o status de autenticação do usuário. Agora você pode seguir em frente para renderizar os detalhes do usuário na tela Perfil .
// ProfileScreen.js
import { useSelector } from 'react-redux'
import '../styles/profile.css'
const ProfileScreen = () => {
const { userInfo } = useSelector((state) => state.user)
return (
<div>
<figure>{userInfo?.firstName.charAt(0).toUpperCase()}</figure>
<span>
Welcome <strong>{userInfo?.firstName}!</strong> You can view this page
because you're logged in
</span>
</div>
)
}
export default ProfileScreen
Atualmente, a página Perfil é acessível a todos, independentemente do status de autenticação. Queremos proteger essa rota verificando se um usuário existe antes de conceder acesso à página. Essa lógica pode ser extraída em um único ProtectedRoute
componente, e vamos criar isso a seguir.
Crie uma pasta chamada routing
e src
um arquivo chamado ProtectedRoute.js
. ProtectedRoute
destina-se a ser usado como um elemento de rota pai, cujos elementos filho são protegidos pela lógica que reside nesse componente.
Aqui, podemos usar userInfo
o valor de 's para detectar se um usuário está logado. Se userInfo
estiver ausente, um modelo não autorizado é retornado. Caso contrário, usamos o Outlet
componente do React Router para renderizar as rotas filhas.
// ProtectedRoute.js
import { useSelector } from 'react-redux'
import { NavLink, Outlet } from 'react-router-dom'
const ProtectedRoute = () => {
const { userInfo } = useSelector((state) => state.user)
// show unauthorized screen if no user is found in redux store
if (!userInfo) {
return (
<div className='unauthorized'>
<h1>Unauthorized :(</h1>
<span>
<NavLink to='/login'>Login</NavLink> to gain access
</span>
</div>
)
}
// returns child route elements
return <Outlet />
}
export default ProtectedRoute
De acordo com a documentação , <Outlet>
deve ser usado em elementos de rota pai para renderizar seus elementos de rota filho. Isso significa que <Outlet>
não renderiza nenhuma marcação na tela, mas é substituído pelos elementos de rota filho.
Agora você pode envolver ProfileScreen
com a rota protegida como tal:
// App.js
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import Header from './components/Header'
import LoginScreen from './screens/LoginScreen'
import RegisterScreen from './screens/RegisterScreen'
import ProfileScreen from './screens/ProfileScreen'
import HomeScreen from './screens/HomeScreen'
import ProtectedRoute from './routing/ProtectedRoute'
import './App.css'
function App() {
return (
<Router>
<Header />
<main className='container content'>
<Routes>
<Route path='/' element={<HomeScreen />} />
<Route path='/login' element={<LoginScreen />} />
<Route path='/register' element={<RegisterScreen />} />
<Route element={<ProtectedRoute />}>
<Route path='/user-profile' element={<ProfileScreen />} />
</Route>
</Routes>
</main>
</Router>
)
}
export default App
E isso é a maior parte do aplicativo completo! Vamos agora ver como desconectar um usuário.
Para desconectar um usuário, criaremos uma ação que redefine o armazenamento Redux para seu valor inicial e limpa o token do armazenamento local. Como essa não é uma tarefa assíncrona, podemos criá-la diretamente userSlice
com a reducer
propriedade.
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { getUserDetails, registerUser, userLogin } from './userActions'
// initialize userToken from local storage
const userToken = localStorage.getItem('userToken')
? localStorage.getItem('userToken')
: null
const initialState = {
loading: false,
userInfo: null,
userToken,
error: null,
success: false,
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
logout: (state) => {
localStorage.removeItem('userToken') // deletes token from storage
state.loading = false
state.userInfo = null
state.userToken = null
state.error = null
},
},
extraReducers: {
// userLogin reducer ...
// registerUser reducer ...
// getUserDetails reducer ...
},
})
// export actions
export const { logout } = userSlice.actions
export default userSlice.reducer
E despachá-lo no Header
componente:
// Header.js
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { NavLink } from 'react-router-dom'
import { getUserDetails } from '../features/user/userActions'
import { logout } from '../features/user/userSlice'
import '../styles/header.css'
const Header = () => {
const { userInfo, userToken } = useSelector((state) => state.user)
const dispatch = useDispatch()
// automatically authenticate user if token is found
useEffect(() => {
if (userToken) {
dispatch(getUserDetails())
}
}, [userToken, dispatch])
return (
<header>
<div className='header-status'>
<span>
{userInfo ? `Logged in as ${userInfo.email}` : "You're not logged in"}
</span>
<div className='cta'>
{userInfo ? (
<button className='button' onClick={() => dispatch(logout())}>
Logout
</button>
) : (
<NavLink className='button' to='/login'>
Login
</NavLink>
)}
</div>
</div>
<nav className='container navigation'>
<NavLink to='/'>Home</NavLink>
<NavLink to='/login'>Login</NavLink>
<NavLink to='/register'>Register</NavLink>
<NavLink to='/user-profile'>Profile</NavLink>
</nav>
</header>
)
}
export default Header
E isso completa nossa aplicação! Agora você tem um aplicativo de pilha MERN com um fluxo de trabalho de autenticação de front-end gerenciado com o Redux Toolkit.
Na minha opinião, o Redux Toolkit oferece uma melhor experiência de desenvolvedor, especialmente em comparação com o quão difícil o Redux costumava ser antes do lançamento do RTK.
Acho o Toolkit fácil de conectar aos meus aplicativos quando preciso implementar o gerenciamento de estado e não quero criá-lo do zero usando o React.Context
.
Armazenar tokens no WebStorage, ou seja, localStorage e sessionStorage, também é uma discussão importante . Pessoalmente, acho localStorage seguro quando o token tem uma vida útil curta e não armazena detalhes privados, como senhas, detalhes do cartão etc.
Sinta-se à vontade para compartilhar como você lida pessoalmente com a autenticação de front-end nos comentários!
Fonte: https://blog.logrocket.com/handling-user-authentication-redux-toolkit/
1657703520
La autenticación de usuario se puede manejar de muchas maneras. Debido a la importancia de esta función, hemos visto que más empresas brindan soluciones de autenticación para facilitar el proceso: Firebase, Auth0 y NextAuth.js, por nombrar algunas.
Independientemente de cómo dichos servicios manejen la autenticación y la autorización por su parte, el proceso de implementación generalmente implica llamar a algunos puntos finales de la API y recibir un token privado (generalmente un token web JSON o JWT) para usar en su infraestructura frontend.
En este artículo, aprenderemos a usar Redux Toolkit (RTK) para crear un flujo de trabajo de autenticación frontend en React. Haremos uso de las API esenciales de Toolkit, como createAsyncThunk
, para realizar solicitudes asincrónicas a un backend de Express. También lo usaremos createSlice
para manejar los cambios de estado.
El backend de este proyecto se crea con Express con una base de datos MongoDB, pero el flujo de trabajo del frontend aún debe aplicarse a cualquier servicio de autenticación que utilice que proporcione un token. Puede descargar los archivos fuente en el repositorio del proyecto con instrucciones sobre cómo configurar una base de datos y puede ejecutar la aplicación localmente. Vea la demostración en vivo aquí .
Para seguir, necesitarás:
Ahora, ¡comencemos a autenticar!
El repositorio incluye una [starter-files]
rama que contiene los archivos necesarios para iniciar esta aplicación. La carpeta frontend también incluye las diversas interfaces de usuario que se ven en la demostración, como las pantallas Inicio , Inicio de sesión , Registro y Perfil con las rutas respectivas /
, /login
, /register
y /user-profile
.
Así es como se ve actualmente la estructura de enrutamiento:
// App.js
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import Header from './components/Header'
import LoginScreen from './screens/LoginScreen'
import RegisterScreen from './screens/RegisterScreen'
import ProfileScreen from './screens/ProfileScreen'
import HomeScreen from './screens/HomeScreen'
import './App.css'
function App() {
return (
<Router>
<Header />
<main className='container content'>
<Routes>
<Route path='/' element={<HomeScreen />} />
<Route path='/login' element={<LoginScreen />} />
<Route path='/register' element={<RegisterScreen />} />
<Route path='/user-profile' element={<ProfileScreen />} />
</Routes>
</main>
</Router>
)
}
export default App
Los campos de entrada en las páginas de registro están conectados a React Hook Form . React Hook Form toma limpiamente los valores de entrada y los devuelve en una handleSubmit
función.
// Login.js
import { useForm } from 'react-hook-form'
const LoginScreen = () => {
const { register, handleSubmit } = useForm()
const submitForm = (data) => {
console.log(data.email)
}
return (
<form onSubmit={handleSubmit(submitForm)}>
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
type='email'
className='form-input'
{...register('email')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<input
type='password'
className='form-input'
{...register('password')}
required
/>
</div>
<button type='submit' className='button'>
Login
</button>
</form>
)
}
export default LoginScreen
En tu terminal, clona la starter-files
rama con el siguiente comando:
git clone --branch starter-files --single-branch https://github.com/Chinwike1/redux-user-auth.git
E instale todas las dependencias del proyecto:
npm install
cd frontend
npm install
El proceso anterior instala los siguientes paquetes en la interfaz
Puede familiarizarse con estos términos de Redux aquí si lo necesita.
Finalmente, ejecuta la aplicación con el siguiente comando:
cd ..
npm run dev
Redux Toolkit presenta una nueva forma de crear una tienda . Separa partes de la tienda en diferentes archivos, conocidos como segmentos.
Un segmento representa una sola unidad del estado Redux. Es una colección de acciones y lógica reductora para una sola función en su aplicación, normalmente definidas juntas en un solo archivo. Para nosotros, este archivo es features/user
.
Al usar la API de RTK createSlice
, podemos crear una porción de Redux así:
// features/user/userSlice.js
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
loading: false,
userInfo: {}, // for user object
userToken: null, // for storing the JWT
error: null,
success: false, // for monitoring the registration process.
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {},
})
export default userSlice.reducer
Importe la reducer
propiedad desde userSlice
la tienda para que se refleje en el objeto de estado raíz de Redux.
// app/store.js
import { configureStore } from '@reduxjs/toolkit'
import userReducer from '../features/user/userSlice'
const store = configureStore({
reducer: {
user: userReducer
}
})
export default store
Los documentos de Redux Toolkit sugieren consolidar acciones y reductores en un archivo de segmento. Por el contrario, he encontrado que mi código es más legible cuando divido estas dos partes y almaceno las acciones en un archivo separado, por ejemplo, features/user/userActions.js
. Aquí, escribiremos acciones asincrónicas que realizan solicitudes al backend.
Pero antes de eso, daré una descripción general rápida de las rutas de back-end disponibles actualmente.
Nuestro servidor Express, alojado en localhost:5000
, actualmente tiene tres rutas:
api/user/login
: la ruta de inicio de sesión. Acepta POST
solicitudes y requiere el correo electrónico y la contraseña del usuario como argumentos. Luego devuelve un JWT después de una autenticación exitosa o un mensaje de error. Este token tiene una vida útil de 12 horas.api/user/register
: la ruta de registro. Acepta POST
solicitudes y requiere el nombre, correo electrónico y contraseña del usuario.api/user/profile
: una ruta de autorización. Acepta GET
solicitudes y requiere el token del usuario para obtener sus detalles de la base de datos. Devuelve el objeto del usuario después de una autorización exitosa o un mensaje de error.Ahora podemos pasar a escribir acciones de Redux, comenzando con la acción de registro.
En userAction.js
, utilizará createAsyncThunk
para realizar una lógica asincrónica retrasada antes de enviar los resultados procesados a los reductores.
createAsyncThunk
acepta tres parámetros: un tipo de acción de cadena, una función de devolución de llamada y un options
objeto opcional.
La función de devolución de llamada es un parámetro importante ya que tiene dos argumentos clave que debe tener en cuenta:
arg
: este es el valor único que se pasa al dispatch
método cuando se llama a la acción. Si necesita pasar varios valores, puede pasar un objetothunkAPI
: un objeto que contiene parámetros que normalmente se pasan a una función thunk de Redux. Los parámetros incluyen getState
, dispatch
, rejectWithValue
y más// userAction.js
export const registerUser = createAsyncThunk(
// action type string
'user/register',
// callback function
async ({ firstName, email, password }, { rejectWithValue }) => {
try {
// configure header's Content-Type as JSON
const config = {
headers: {
'Content-Type': 'application/json',
},
}
// make request to backend
await axios.post(
'/api/user/register',
{ firstName, email, password },
config
)
} catch (error) {
// return custom error message from API if any
if (error.response && error.response.data.message) {
return rejectWithValue(error.response.data.message)
} else {
return rejectWithValue(error.message)
}
}
}
)
In the code block above, we've taken the values from the register form and made a POST request to the register route using Axios. In the event of an error, thunkAPI.rejectWithValue sends the custom error message from the backend as a payload to the reducer. You may notice that the register API is called without referencing the server's base URL. This is possible with the proxy configuration existing in frontend/package.json.
.{
"proxy": "http://127.0.0.1:5000",
}
extraReducers
Las acciones creadas con createAsyncThunk
generan tres posibles tipos de acciones de ciclo de vida: pending
, fulfilled
y rejected
.
Puede utilizar estos tipos de acción en la extraReducers
propiedad de userSlice
para realizar los cambios apropiados en su estado.
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { registerUser } from './userActions'
const initialState = {
loading: false,
userInfo: null,
userToken: null,
error: null,
success: false,
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {
// register user
[registerUser.pending]: (state) => {
state.loading = true
state.error = null
},
[registerUser.fulfilled]: (state, { payload }) => {
state.loading = false
state.success = true // registration successful
},
[registerUser.rejected]: (state, { payload }) => {
state.loading = false
state.error = payload
},
},
})
export default userSlice.reducer
En este caso, establecemos el success
valor true
cuando se cumple la acción para indicar un registro exitoso.
useDispatch
y useSelector
ganchosAl usar useSelector
y useDispatch
desde el react-redux
paquete que instaló anteriormente, puede leer el estado de una tienda Redux y enviar cualquier acción desde un componente, respectivamente.
// RegisterScreen.js
import { useForm } from 'react-hook-form'
import { useDispatch, useSelector } from 'react-redux'
import Error from '../components/Error'
import { registerUser } from '../features/user/userActions'
const RegisterScreen = () => {
const { loading, error } = useSelector(
(state) => state.user
)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const submitForm = (data) => {
// check if passwords match
if (data.password !== data.confirmPassword) {
alert('Password mismatch')
return
}
// transform email string to lowercase to avoid case sensitivity issues during login
data.email = data.email.toLowerCase()
dispatch(registerUser(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{/* render error message with Error component, if any */}
{error && <Error>{error}</Error>}
<div className='form-group'>
<label htmlFor='firstName'>First Name</label>
<input
type='text'
className='form-input'
{...register('firstName')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
type='email'
className='form-input'
{...register('email')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<input
type='password'
className='form-input'
{...register('password')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='email'>Confirm Password</label>
<input
type='password'
className='form-input'
{...register('confirmPassword')}
required
/>
</div>
<button type='submit' className='button' disabled={loading}>
Register
</button>
</form>
)
}
export default RegisterScreen
Cuando se envía el formulario, comenzamos verificando si los campos password
y coinciden. confirmPassword
Si lo hacen, la registerUser
acción se despacha, tomando los datos del formulario como argumento.
El useSelector
gancho se usa para extraer los valores de estado loading
y estado del objeto en la tienda Redux. Luego, estos valores se usan para realizar ciertos cambios en la interfaz de usuario, como deshabilitar el botón de envío mientras la solicitud está en curso y mostrar un mensaje de error.erroruser
Actualmente, cuando un usuario completa el registro, no hay indicios de que lo que haya hecho sea exitoso. Con el success
valor userSlice
junto al gancho del enrutador React useNavigate
y el useEffect
gancho, podemos redirigir al usuario a la página de inicio de sesión después del registro.
Así es como se vería:
// RegisterScreen.js
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { useDispatch, useSelector } from 'react-redux'
import { useNavigate } from 'react-router-dom'
import Error from '../components/Error'
import { registerUser } from '../features/user/userActions'
const RegisterScreen = () => {
const { loading, userInfo, error, success } = useSelector(
(state) => state.user
)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const navigate = useNavigate()
useEffect(() => {
// redirect user to login page if registration was successful
if (success) navigate('/login')
// redirect authenticated user to profile screen
if (userInfo) navigate('/user-profile')
}, [navigate, userInfo, success])
const submitForm = (data) => {
// check if passwords match
if (data.password !== data.confirmPassword) {
alert('Password mismatch')
return
}
// transform email string to lowercase to avoid case sensitivity issues in login
data.email = data.email.toLowerCase()
dispatch(registerUser(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{/* form markup... */}
</form>
)
}
export default RegisterScreen
Hay un patrón familiar que notarás al escribir la lógica de Redux. Suele ser así:
No tiene que ser en ese orden, pero por lo general son pasos recurrentes. Repitamos esto con la acción de inicio de sesión.
La acción de inicio de sesión será similar a la acción de registro, excepto que aquí almacenamos el JWT devuelto desde el backend en el almacenamiento local antes de pasar el resultado al reductor.
// userActions.js
export const userLogin = createAsyncThunk(
'user/login',
async ({ email, password }, { rejectWithValue }) => {
try {
// configure header's Content-Type as JSON
const config = {
headers: {
'Content-Type': 'application/json',
},
}
const { data } = await axios.post(
'/api/user/login',
{ email, password },
config
)
// store user's token in local storage
localStorage.setItem('userToken', data.userToken)
return data
} catch (error) {
// return custom error message from API if any
if (error.response && error.response.data.message) {
return rejectWithValue(error.response.data.message)
} else {
return rejectWithValue(error.message)
}
}
}
)
Ahora puede manejar los tipos de acción del ciclo de vida en userSlice.js
.
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { registerUser, userLogin } from './userActions'
// initialize userToken from local storage
const userToken = localStorage.getItem('userToken')
? localStorage.getItem('userToken')
: null
const initialState = {
loading: false,
userInfo: null,
userToken,
error: null,
success: false,
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {
// login user
[userLogin.pending]: (state) => {
state.loading = true
state.error = null
},
[userLogin.fulfilled]: (state, { payload }) => {
state.loading = false
state.userInfo = payload
state.userToken = payload.userToken
},
[userLogin.rejected]: (state, { payload }) => {
state.loading = false
state.error = payload
},
// register user reducer...
},
})
export default userSlice.reducer
Debido a que el valor de userToken
depende del valor del token from localStorage
, es mejor inicializarlo al principio, como se muestra arriba.
Ahora puede enviar esta acción cuando se envía el formulario y realizar sus actualizaciones de interfaz de usuario preferidas.
// LoginScreen.js
import { useForm } from 'react-hook-form'
import { useDispatch, useSelector } from 'react-redux'
import { userLogin } from '../features/user/userActions'
import Error from '../components/Error'
const LoginScreen = () => {
const { loading, error } = useSelector((state) => state.user)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const submitForm = (data) => {
dispatch(userLogin(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{error && <Error>{error}</Error>}
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
type='email'
className='form-input'
{...register('email')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<input
type='password'
className='form-input'
{...register('password')}
required
/>
</div>
<button type='submit' className='button' disabled={loading}>
Login
</button>
</form>
)
}
export default LoginScreen
También desea asegurarse de que los usuarios previamente autenticados no puedan acceder a esta página. userInfo
El valor de se puede utilizar para redirigir a un usuario autenticado a la página de inicio de sesión con useNavigate
y useEffect
.
// LoginScreen.js
import { useForm } from 'react-hook-form'
import { useNavigate } from 'react-router-dom'
import { useDispatch, useSelector } from 'react-redux'
import { userLogin } from '../features/user/userActions'
import { useEffect } from 'react'
import Error from '../components/Error'
const LoginScreen = () => {
const { loading, userInfo, error } = useSelector((state) => state.user)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const navigate = useNavigate()
// redirect authenticated user to profile screen
useEffect(() => {
if (userInfo) {
navigate('/user-profile')
}
}, [navigate, userInfo])
const submitForm = (data) => {
dispatch(userLogin(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{/* form markup... */}
</form>
)
}
export default LoginScreen
¡Y eso lo completa para el proceso de registro! A continuación, verá cómo autenticar automáticamente a un usuario cuyo token válido todavía está almacenado en el navegador.
getUserProfile
acciónPara esta acción, se comunicará con una ruta de autorización (que actúa como punto final) que requiere algunas credenciales para acompañar la solicitud del cliente. Aquí, la credencial requerida es el JWT almacenado localmente.
A diferencia de las rutas de autenticación en las que las credenciales de usuario se pasaban a través de formularios, las rutas de autorización requieren que las credenciales se pasen de forma más segura a través de encabezados HTTPAuthorization
con esta sintaxis: Authorization: <auth-scheme> <credentials>
.
auth-scheme
representa qué esquema de autenticación desea utilizar. Los JWT están diseñados para admitir el Bearer
esquema de autenticación, y eso es lo que haremos. Consulte tokens de portador RFC 6750 para obtener más información al respecto.
Con Axios, puede configurar el objeto de encabezado de la solicitud para enviar el JWT:
const config = {
headers: {
Authorization: 'Bearer eyJhbGInR5cCI6IkpXVCJ9.eyJpZTI4MTI1MywiZXhwIjoxNjU1MzI0NDUzfQ.FWMexh',
},
}
Debido a que el token del usuario se inicia en la tienda Redux, su valor debe extraerse e incluirse en esta solicitud.
El segundo parámetro en createAsyncThunk
la función de devolución de llamada thunkAPI
, proporciona un getState
método que le permite leer el valor actual de la tienda Redux.
// userActions.js
export const getUserDetails = createAsyncThunk(
'user/getUserDetails',
async (arg, { getState, rejectWithValue }) => {
try {
// get user data from store
const { user } = getState()
// configure authorization header with user's token
const config = {
headers: {
Authorization: `Bearer ${user.userToken}`,
},
}
const { data } = await axios.get(`/api/user/profile`, config)
return data
} catch (error) {
if (error.response && error.response.data.message) {
return rejectWithValue(error.response.data.message)
} else {
return rejectWithValue(error.message)
}
}
}
)
A continuación, manejaremos las acciones del ciclo de vida:
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { getUserDetails, registerUser, userLogin } from './userActions'
const initialState = {
// state values...
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {
// login user reducer ...
// register user reducer ...
[getUserDetails.pending]: (state) => {
state.loading = true
},
[getUserDetails.fulfilled]: (state, { payload }) => {
state.loading = false
state.userInfo = payload
},
[getUserDetails.rejected]: (state, { payload }) => {
state.loading = false
},
},
})
export default userSlice.reducer
El Header
componente es una ubicación adecuada para enviar esta acción, ya que es el único componente que permanece visible en toda la aplicación. Aquí, queremos que esta acción se envíe cuando la aplicación note un cambio en userToken
el valor de . Esto se puede lograr con el useEffect
gancho.
// Header.js
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { NavLink } from 'react-router-dom'
import { getUserDetails } from '../features/user/userActions'
import '../styles/header.css'
const Header = () => {
const { userInfo, userToken } = useSelector((state) => state.user)
const dispatch = useDispatch()
// automatically authenticate user if token is found
useEffect(() => {
if (userToken) {
dispatch(getUserDetails())
}
}, [userToken, dispatch])
return (
<header>
<div className='header-status'>
<span>
{userInfo ? `Logged in as ${userInfo.email}` : "You're not logged in"}
</span>
<div className='cta'>
{userInfo ? (
<button className='button'>
Logout
</button>
) : (
<NavLink className='button' to='/login'>
Login
</NavLink>
)}
</div>
</div>
<nav className='container navigation'>
<NavLink to='/'>Home</NavLink>
<NavLink to='/login'>Login</NavLink>
<NavLink to='/register'>Register</NavLink>
<NavLink to='/user-profile'>Profile</NavLink>
</nav>
</header>
)
}
export default Header
Tenga en cuenta que también solíamos presentar userInfo
mensajes y elementos apropiados en la barra de navegación que se correlacionan con el estado de autenticación del usuario. Ahora puede pasar a representar los detalles del usuario en la pantalla Perfil .
// ProfileScreen.js
import { useSelector } from 'react-redux'
import '../styles/profile.css'
const ProfileScreen = () => {
const { userInfo } = useSelector((state) => state.user)
return (
<div>
<figure>{userInfo?.firstName.charAt(0).toUpperCase()}</figure>
<span>
Welcome <strong>{userInfo?.firstName}!</strong> You can view this page
because you're logged in
</span>
</div>
)
}
export default ProfileScreen
Actualmente, todos pueden acceder a la página Perfil , independientemente del estado de autenticación. Queremos proteger esta ruta verificando si existe un usuario antes de otorgarle acceso a la página. Esta lógica se puede extraer en un solo ProtectedRoute
componente, y lo vamos a crear a continuación.
Cree una carpeta llamada routing
y src
un archivo llamado ProtectedRoute.js
. ProtectedRoute
está destinado a ser utilizado como un elemento de ruta principal, cuyos elementos secundarios están protegidos por la lógica que reside en este componente.
Aquí, podemos usar userInfo
el valor de para detectar si un usuario ha iniciado sesión. Si userInfo
está ausente, se devuelve una plantilla no autorizada. De lo contrario, usamos el Outlet
componente de React Router para representar las rutas secundarias.
// ProtectedRoute.js
import { useSelector } from 'react-redux'
import { NavLink, Outlet } from 'react-router-dom'
const ProtectedRoute = () => {
const { userInfo } = useSelector((state) => state.user)
// show unauthorized screen if no user is found in redux store
if (!userInfo) {
return (
<div className='unauthorized'>
<h1>Unauthorized :(</h1>
<span>
<NavLink to='/login'>Login</NavLink> to gain access
</span>
</div>
)
}
// returns child route elements
return <Outlet />
}
export default ProtectedRoute
De acuerdo con la documentación , <Outlet>
debe usarse en elementos de ruta principales para representar sus elementos de ruta secundarios. Esto significa que <Outlet>
no representa ningún marcado en la pantalla, sino que se reemplaza por los elementos de ruta secundarios.
Ahora puede envolver ProfileScreen
con la ruta protegida como tal:
// App.js
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import Header from './components/Header'
import LoginScreen from './screens/LoginScreen'
import RegisterScreen from './screens/RegisterScreen'
import ProfileScreen from './screens/ProfileScreen'
import HomeScreen from './screens/HomeScreen'
import ProtectedRoute from './routing/ProtectedRoute'
import './App.css'
function App() {
return (
<Router>
<Header />
<main className='container content'>
<Routes>
<Route path='/' element={<HomeScreen />} />
<Route path='/login' element={<LoginScreen />} />
<Route path='/register' element={<RegisterScreen />} />
<Route element={<ProtectedRoute />}>
<Route path='/user-profile' element={<ProfileScreen />} />
</Route>
</Routes>
</main>
</Router>
)
}
export default App
¡Y eso es la mayor parte de la aplicación completa! Veamos ahora cómo cerrar la sesión de un usuario.
Para cerrar la sesión de un usuario, crearemos una acción que restablezca la tienda Redux a su valor inicial y borre el token del almacenamiento local. Debido a que esta no es una tarea asíncrona, podemos crearla directamente userSlice
con la reducer
propiedad.
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { getUserDetails, registerUser, userLogin } from './userActions'
// initialize userToken from local storage
const userToken = localStorage.getItem('userToken')
? localStorage.getItem('userToken')
: null
const initialState = {
loading: false,
userInfo: null,
userToken,
error: null,
success: false,
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
logout: (state) => {
localStorage.removeItem('userToken') // deletes token from storage
state.loading = false
state.userInfo = null
state.userToken = null
state.error = null
},
},
extraReducers: {
// userLogin reducer ...
// registerUser reducer ...
// getUserDetails reducer ...
},
})
// export actions
export const { logout } = userSlice.actions
export default userSlice.reducer
Y enviarlo en el Header
componente:
// Header.js
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { NavLink } from 'react-router-dom'
import { getUserDetails } from '../features/user/userActions'
import { logout } from '../features/user/userSlice'
import '../styles/header.css'
const Header = () => {
const { userInfo, userToken } = useSelector((state) => state.user)
const dispatch = useDispatch()
// automatically authenticate user if token is found
useEffect(() => {
if (userToken) {
dispatch(getUserDetails())
}
}, [userToken, dispatch])
return (
<header>
<div className='header-status'>
<span>
{userInfo ? `Logged in as ${userInfo.email}` : "You're not logged in"}
</span>
<div className='cta'>
{userInfo ? (
<button className='button' onClick={() => dispatch(logout())}>
Logout
</button>
) : (
<NavLink className='button' to='/login'>
Login
</NavLink>
)}
</div>
</div>
<nav className='container navigation'>
<NavLink to='/'>Home</NavLink>
<NavLink to='/login'>Login</NavLink>
<NavLink to='/register'>Register</NavLink>
<NavLink to='/user-profile'>Profile</NavLink>
</nav>
</header>
)
}
export default Header
¡Y eso completa nuestra aplicación! Ahora tiene una aplicación de pila MERN con un flujo de trabajo de autenticación frontend administrado con Redux Toolkit.
En mi opinión, Redux Toolkit ofrece una mejor experiencia para los desarrolladores, especialmente en comparación con lo difícil que solía ser Redux antes del lanzamiento de RTK.
Encuentro Toolkit fácil de conectar a mis aplicaciones cuando necesito implementar la administración de estado y no quiero crearlo desde cero usando React.Context
.
El almacenamiento de tokens en WebStorage, es decir, localStorage y sessionStorage, también es un tema importante . Personalmente, localStorage me parece seguro cuando el token tiene una vida útil corta y no almacena detalles privados como contraseñas, detalles de tarjetas, etc.
¡Siéntete libre de compartir cómo manejas personalmente la autenticación frontend en los comentarios!
Fuente: https://blog.logrocket.com/handling-user-authentication-redux-toolkit/
1657699802
Xác thực người dùng có thể được xử lý theo vô số cách. Vì tính năng này quan trọng như thế nào, chúng tôi đã thấy nhiều công ty cung cấp giải pháp xác thực hơn để dễ dàng quy trình - Firebase, Auth0 và NextAuth.js là một vài trong số đó.
Bất kể các dịch vụ đó xử lý xác thực và ủy quyền bằng cách nào, quá trình triển khai thường liên quan đến việc gọi một số điểm cuối API và nhận mã thông báo riêng (thường là Mã thông báo web JSON hoặc JWT) được sử dụng trong cơ sở hạ tầng giao diện người dùng của bạn.
Trong bài viết này, chúng ta sẽ tìm hiểu cách sử dụng Bộ công cụ Redux (RTK) để tạo quy trình xác thực giao diện người dùng trong React. Chúng tôi sẽ sử dụng các API Bộ công cụ thiết yếu, chẳng hạn như createAsyncThunk
, để thực hiện các yêu cầu không đồng bộ đối với chương trình phụ trợ Express. Chúng tôi cũng sẽ sử dụng createSlice
để xử lý các thay đổi trạng thái.
Phần phụ trợ cho dự án này được xây dựng bằng cách sử dụng Express với cơ sở dữ liệu MongoDB, nhưng quy trình làm việc của giao diện người dùng vẫn phải áp dụng cho bất kỳ dịch vụ xác thực nào bạn sử dụng cung cấp mã thông báo. Bạn có thể tải xuống các tệp nguồn trong kho của dự án với hướng dẫn về cách thiết lập cơ sở dữ liệu và có thể chạy ứng dụng cục bộ. Xem bản demo trực tiếp tại đây .
Để làm theo, bạn sẽ cần:
Bây giờ, chúng ta hãy bắt đầu xác thực!
Kho lưu trữ bao gồm một [starter-files]
nhánh chứa các tệp cần thiết để khởi động ứng dụng này. Thư mục frontend cũng bao gồm các giao diện người dùng khác nhau được thấy trong bản demo, chẳng hạn như màn hình Trang chủ , Đăng nhập , Đăng ký và Hồ sơ với các tuyến tương ứng /
, và ./login/register/user-profile
Đây là cấu trúc định tuyến hiện tại trông như thế nào:
// App.js
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import Header from './components/Header'
import LoginScreen from './screens/LoginScreen'
import RegisterScreen from './screens/RegisterScreen'
import ProfileScreen from './screens/ProfileScreen'
import HomeScreen from './screens/HomeScreen'
import './App.css'
function App() {
return (
<Router>
<Header />
<main className='container content'>
<Routes>
<Route path='/' element={<HomeScreen />} />
<Route path='/login' element={<LoginScreen />} />
<Route path='/register' element={<RegisterScreen />} />
<Route path='/user-profile' element={<ProfileScreen />} />
</Routes>
</main>
</Router>
)
}
export default App
Các trường đầu vào trên các trang đăng ký được kết nối với React Hook Form . React Hook Form lấy sạch các giá trị đầu vào và trả về chúng trong một handleSubmit
hàm.
// Login.js
import { useForm } from 'react-hook-form'
const LoginScreen = () => {
const { register, handleSubmit } = useForm()
const submitForm = (data) => {
console.log(data.email)
}
return (
<form onSubmit={handleSubmit(submitForm)}>
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
type='email'
className='form-input'
{...register('email')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<input
type='password'
className='form-input'
{...register('password')}
required
/>
</div>
<button type='submit' className='button'>
Login
</button>
</form>
)
}
export default LoginScreen
Trong thiết bị đầu cuối của bạn, sao chép starter-files
nhánh bằng lệnh sau:
git clone --branch starter-files --single-branch https://github.com/Chinwike1/redux-user-auth.git
Và cài đặt tất cả các phụ thuộc của dự án:
npm install
cd frontend
npm install
Quá trình trên cài đặt các gói sau trên giao diện người dùng
Bạn có thể làm quen với các thuật ngữ Redux này tại đây nếu cần.
Cuối cùng, chạy ứng dụng bằng lệnh sau:
cd ..
npm run dev
Bộ công cụ Redux giới thiệu một cách mới để tạo cửa hàng . Nó phân tách các phần của cửa hàng thành các tệp khác nhau, được gọi là các lát.
Một lát cắt đại diện cho một đơn vị duy nhất của trạng thái Redux. Đó là một tập hợp các hành động và logic giảm thiểu cho một tính năng duy nhất trong ứng dụng của bạn, thường được xác định cùng nhau trong một tệp duy nhất. Đối với chúng tôi, tập tin này là features/user
.
Bằng cách sử dụng API của RTK createSlice
, chúng ta có thể tạo một lát Redux như sau:
// features/user/userSlice.js
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
loading: false,
userInfo: {}, // for user object
userToken: null, // for storing the JWT
error: null,
success: false, // for monitoring the registration process.
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {},
})
export default userSlice.reducer
Nhập thuộc reducer
tính từ userSlice
vào cửa hàng để nó phản ánh trong đối tượng trạng thái Redux gốc.
// app/store.js
import { configureStore } from '@reduxjs/toolkit'
import userReducer from '../features/user/userSlice'
const store = configureStore({
reducer: {
user: userReducer
}
})
export default store
Các tài liệu của Bộ công cụ Redux đề xuất hợp nhất các hành động và bộ giảm vào một tệp lát cắt. Ngược lại, tôi thấy mã của mình dễ đọc hơn khi tôi tách hai phần này và lưu trữ các hành động trong một tệp riêng biệt, ví dụ features/user/userActions.js
: Ở đây, chúng tôi sẽ viết các hành động không đồng bộ thực hiện các yêu cầu tới phần phụ trợ.
Nhưng trước đó, tôi sẽ trình bày tổng quan nhanh về các tuyến phụ trợ hiện có.
Máy chủ Express của chúng tôi, được lưu trữ trên localhost:5000
, hiện có ba tuyến:
api/user/login
: đường đăng nhập. Nó chấp nhận POST
các yêu cầu và yêu cầu email và mật khẩu của người dùng làm đối số. Sau đó, nó trả về một JWT sau khi xác thực thành công hoặc một thông báo lỗi. Mã thông báo này có tuổi thọ 12 giờapi/user/register
: tuyến đăng ký. Nó chấp nhận POST
các yêu cầu và yêu cầu tên, email và mật khẩu của người dùngapi/user/profile
: một tuyến đường ủy quyền. Nó chấp nhận GET
các yêu cầu và yêu cầu mã thông báo của người dùng để tìm nạp thông tin chi tiết của họ từ cơ sở dữ liệu. Nó trả về đối tượng của người dùng sau khi ủy quyền thành công hoặc một thông báo lỗi.Bây giờ chúng ta có thể chuyển sang viết các hành động Redux, bắt đầu với hành động đăng ký.
Trong userAction.js
, bạn sẽ sử dụng createAsyncThunk
để thực hiện logic bị trễ, không đồng bộ trước khi gửi kết quả đã xử lý đến bộ giảm.
createAsyncThunk
chấp nhận ba tham số: kiểu hành động chuỗi, hàm gọi lại và options
đối tượng tùy chọn.
Hàm gọi lại là một tham số quan trọng vì nó có hai đối số chính mà bạn nên xem xét:
arg
: đây là giá trị duy nhất được truyền vào dispatch
phương thức khi hành động được gọi. Nếu bạn cần chuyển nhiều giá trị, bạn có thể chuyển vào một đối tượngthunkAPI
: một đối tượng chứa các tham số thường được truyền cho một hàm Redux thunk. Các thông số bao gồm , getState
và hơn thế nữadispatchrejectWithValue
// userAction.js
export const registerUser = createAsyncThunk(
// action type string
'user/register',
// callback function
async ({ firstName, email, password }, { rejectWithValue }) => {
try {
// configure header's Content-Type as JSON
const config = {
headers: {
'Content-Type': 'application/json',
},
}
// make request to backend
await axios.post(
'/api/user/register',
{ firstName, email, password },
config
)
} catch (error) {
// return custom error message from API if any
if (error.response && error.response.data.message) {
return rejectWithValue(error.response.data.message)
} else {
return rejectWithValue(error.message)
}
}
}
)
In the code block above, we've taken the values from the register form and made a POST request to the register route using Axios. In the event of an error, thunkAPI.rejectWithValue sends the custom error message from the backend as a payload to the reducer. You may notice that the register API is called without referencing the server's base URL. This is possible with the proxy configuration existing in frontend/package.json.
.{
"proxy": "http://127.0.0.1:5000",
}
extraReducers
Các hành động được tạo bằng createAsyncThunk
cách tạo ra ba loại hành động vòng đời có thể có pending
:, fulfilled
và rejected
.
Bạn có thể sử dụng các loại hành động này trong thuộc extraReducers
tính của userSlice
để thực hiện các thay đổi thích hợp cho trạng thái của mình.
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { registerUser } from './userActions'
const initialState = {
loading: false,
userInfo: null,
userToken: null,
error: null,
success: false,
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {
// register user
[registerUser.pending]: (state) => {
state.loading = true
state.error = null
},
[registerUser.fulfilled]: (state, { payload }) => {
state.loading = false
state.success = true // registration successful
},
[registerUser.rejected]: (state, { payload }) => {
state.loading = false
state.error = payload
},
},
})
export default userSlice.reducer
Trong trường hợp này, chúng tôi đặt success
giá trị thành true
khi hành động được hoàn thành để báo hiệu đăng ký thành công.
useDispatch
và useSelector
hooksBằng cách sử dụng useSelector
và useDispatch
từ react-redux
gói bạn đã cài đặt trước đó, bạn có thể đọc trạng thái từ cửa hàng Redux và gửi bất kỳ hành động nào từ một thành phần tương ứng.
// RegisterScreen.js
import { useForm } from 'react-hook-form'
import { useDispatch, useSelector } from 'react-redux'
import Error from '../components/Error'
import { registerUser } from '../features/user/userActions'
const RegisterScreen = () => {
const { loading, error } = useSelector(
(state) => state.user
)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const submitForm = (data) => {
// check if passwords match
if (data.password !== data.confirmPassword) {
alert('Password mismatch')
return
}
// transform email string to lowercase to avoid case sensitivity issues during login
data.email = data.email.toLowerCase()
dispatch(registerUser(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{/* render error message with Error component, if any */}
{error && <Error>{error}</Error>}
<div className='form-group'>
<label htmlFor='firstName'>First Name</label>
<input
type='text'
className='form-input'
{...register('firstName')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
type='email'
className='form-input'
{...register('email')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<input
type='password'
className='form-input'
{...register('password')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='email'>Confirm Password</label>
<input
type='password'
className='form-input'
{...register('confirmPassword')}
required
/>
</div>
<button type='submit' className='button' disabled={loading}>
Register
</button>
</form>
)
}
export default RegisterScreen
Khi biểu mẫu được gửi, chúng tôi bắt đầu bằng cách xác minh xem trường password
và có confirmPassword
khớp nhau không. Nếu họ làm vậy, registerUser
hành động sẽ được gửi đi, lấy dữ liệu biểu mẫu làm đối số của nó.
useSelector
Hook được sử dụng để lấy ra các loading
giá trị và trạng error
thái từ user
đối tượng trong Redux store. Sau đó, những giá trị này được sử dụng để thực hiện một số thay đổi về giao diện người dùng, chẳng hạn như tắt nút gửi trong khi yêu cầu đang được thực hiện và hiển thị thông báo lỗi.
Hiện tại, khi người dùng hoàn tất đăng ký, không có dấu hiệu nào cho thấy những gì họ đã làm là thành công. Với success
giá trị từ cùng với hook và hook userSlice
của bộ định tuyến React , chúng tôi có thể chuyển hướng người dùng đến trang Đăng nhập sau khi đăng ký.useNavigateuseEffect
Đây là cách nó sẽ trông như thế nào:
// RegisterScreen.js
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { useDispatch, useSelector } from 'react-redux'
import { useNavigate } from 'react-router-dom'
import Error from '../components/Error'
import { registerUser } from '../features/user/userActions'
const RegisterScreen = () => {
const { loading, userInfo, error, success } = useSelector(
(state) => state.user
)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const navigate = useNavigate()
useEffect(() => {
// redirect user to login page if registration was successful
if (success) navigate('/login')
// redirect authenticated user to profile screen
if (userInfo) navigate('/user-profile')
}, [navigate, userInfo, success])
const submitForm = (data) => {
// check if passwords match
if (data.password !== data.confirmPassword) {
alert('Password mismatch')
return
}
// transform email string to lowercase to avoid case sensitivity issues in login
data.email = data.email.toLowerCase()
dispatch(registerUser(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{/* form markup... */}
</form>
)
}
export default RegisterScreen
Có một mẫu quen thuộc mà bạn sẽ nhận thấy khi viết logic Redux. Nó thường diễn ra như thế này:
Nó không nhất thiết phải theo thứ tự đó, nhưng chúng thường là các bước lặp lại. Hãy lặp lại điều này với hành động đăng nhập.
Hành động đăng nhập sẽ tương tự như hành động đăng ký, ngoại trừ ở đây chúng tôi lưu trữ JWT được trả về từ chương trình phụ trợ trong bộ nhớ cục bộ trước khi chuyển kết quả đến trình giảm thiểu.
// userActions.js
export const userLogin = createAsyncThunk(
'user/login',
async ({ email, password }, { rejectWithValue }) => {
try {
// configure header's Content-Type as JSON
const config = {
headers: {
'Content-Type': 'application/json',
},
}
const { data } = await axios.post(
'/api/user/login',
{ email, password },
config
)
// store user's token in local storage
localStorage.setItem('userToken', data.userToken)
return data
} catch (error) {
// return custom error message from API if any
if (error.response && error.response.data.message) {
return rejectWithValue(error.response.data.message)
} else {
return rejectWithValue(error.message)
}
}
}
)
Bây giờ bạn có thể xử lý các loại hành động vòng đời trong userSlice.js
.
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { registerUser, userLogin } from './userActions'
// initialize userToken from local storage
const userToken = localStorage.getItem('userToken')
? localStorage.getItem('userToken')
: null
const initialState = {
loading: false,
userInfo: null,
userToken,
error: null,
success: false,
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {
// login user
[userLogin.pending]: (state) => {
state.loading = true
state.error = null
},
[userLogin.fulfilled]: (state, { payload }) => {
state.loading = false
state.userInfo = payload
state.userToken = payload.userToken
},
[userLogin.rejected]: (state, { payload }) => {
state.loading = false
state.error = payload
},
// register user reducer...
},
})
export default userSlice.reducer
Bởi vì giá trị của userToken
phụ thuộc vào giá trị của mã thông báo từ localStorage
đó, tốt hơn nên khởi tạo nó ngay từ đầu, như được hiển thị ở trên.
Giờ đây, bạn có thể thực hiện hành động này khi biểu mẫu được gửi và thực hiện cập nhật giao diện người dùng ưa thích của bạn.
// LoginScreen.js
import { useForm } from 'react-hook-form'
import { useDispatch, useSelector } from 'react-redux'
import { userLogin } from '../features/user/userActions'
import Error from '../components/Error'
const LoginScreen = () => {
const { loading, error } = useSelector((state) => state.user)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const submitForm = (data) => {
dispatch(userLogin(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{error && <Error>{error}</Error>}
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
type='email'
className='form-input'
{...register('email')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<input
type='password'
className='form-input'
{...register('password')}
required
/>
</div>
<button type='submit' className='button' disabled={loading}>
Login
</button>
</form>
)
}
export default LoginScreen
Bạn cũng muốn đảm bảo rằng những người dùng đã được xác thực trước đó không thể truy cập trang này. userInfo
Giá trị của có thể được sử dụng để chuyển hướng người dùng đã xác thực đến trang Đăng nhập bằng useNavigate
và useEffect
.
// LoginScreen.js
import { useForm } from 'react-hook-form'
import { useNavigate } from 'react-router-dom'
import { useDispatch, useSelector } from 'react-redux'
import { userLogin } from '../features/user/userActions'
import { useEffect } from 'react'
import Error from '../components/Error'
const LoginScreen = () => {
const { loading, userInfo, error } = useSelector((state) => state.user)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const navigate = useNavigate()
// redirect authenticated user to profile screen
useEffect(() => {
if (userInfo) {
navigate('/user-profile')
}
}, [navigate, userInfo])
const submitForm = (data) => {
dispatch(userLogin(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{/* form markup... */}
</form>
)
}
export default LoginScreen
Và điều đó hoàn thành nó cho quá trình đăng ký! Tiếp theo, bạn sẽ thấy cách tự động xác thực người dùng có mã thông báo hợp lệ vẫn được lưu trữ trong trình duyệt.
getUserProfile
hoạt độngĐối với hành động này, bạn sẽ tiếp cận với một tuyến ủy quyền (đóng vai trò là điểm cuối) yêu cầu một số thông tin xác thực đi cùng với yêu cầu của khách hàng. Ở đây, thông tin đăng nhập bắt buộc là JWT được lưu trữ cục bộ.
Không giống như trong các tuyến xác thực nơi thông tin đăng nhập của người dùng được chuyển qua các biểu mẫu, các tuyến ủy quyền yêu cầu thông tin xác thực phải được chuyển an toàn hơn thông qua các tiêu đề HTTPAuthorization
Authorization: <auth-scheme> <credentials>
bằng cú pháp này :.
auth-scheme
đại diện cho lược đồ xác thực mà bạn muốn sử dụng. JWT được thiết kế để hỗ trợ Bearer
lược đồ xác thực và đó là những gì chúng ta sẽ thực hiện. Xem mã thông báo mang RFC 6750 để biết thêm thông tin về điều này.
Với Axios, bạn có thể định cấu hình đối tượng tiêu đề của yêu cầu để gửi JWT:
const config = {
headers: {
Authorization: 'Bearer eyJhbGInR5cCI6IkpXVCJ9.eyJpZTI4MTI1MywiZXhwIjoxNjU1MzI0NDUzfQ.FWMexh',
},
}
Vì mã thông báo của người dùng được khởi tạo trong cửa hàng Redux, giá trị của nó cần được trích xuất và đưa vào yêu cầu này.
Tham số thứ hai trong createAsyncThunk
hàm gọi lại của thunkAPI
, cung cấp một getState
phương thức cho phép bạn đọc giá trị hiện tại của Redux store.
// userActions.js
export const getUserDetails = createAsyncThunk(
'user/getUserDetails',
async (arg, { getState, rejectWithValue }) => {
try {
// get user data from store
const { user } = getState()
// configure authorization header with user's token
const config = {
headers: {
Authorization: `Bearer ${user.userToken}`,
},
}
const { data } = await axios.get(`/api/user/profile`, config)
return data
} catch (error) {
if (error.response && error.response.data.message) {
return rejectWithValue(error.response.data.message)
} else {
return rejectWithValue(error.message)
}
}
}
)
Tiếp theo, chúng tôi sẽ xử lý các hành động trong vòng đời:
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { getUserDetails, registerUser, userLogin } from './userActions'
const initialState = {
// state values...
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {
// login user reducer ...
// register user reducer ...
[getUserDetails.pending]: (state) => {
state.loading = true
},
[getUserDetails.fulfilled]: (state, { payload }) => {
state.loading = false
state.userInfo = payload
},
[getUserDetails.rejected]: (state, { payload }) => {
state.loading = false
},
},
})
export default userSlice.reducer
Thành Header
phần là một vị trí thích hợp để thực hiện hành động này, vì nó là thành phần duy nhất hiển thị trong toàn bộ ứng dụng. Ở đây, chúng tôi muốn hành động này được thực hiện khi ứng dụng nhận thấy sự thay đổi trong userToken
giá trị của. Điều này có thể đạt được với useEffect
hook.
// Header.js
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { NavLink } from 'react-router-dom'
import { getUserDetails } from '../features/user/userActions'
import '../styles/header.css'
const Header = () => {
const { userInfo, userToken } = useSelector((state) => state.user)
const dispatch = useDispatch()
// automatically authenticate user if token is found
useEffect(() => {
if (userToken) {
dispatch(getUserDetails())
}
}, [userToken, dispatch])
return (
<header>
<div className='header-status'>
<span>
{userInfo ? `Logged in as ${userInfo.email}` : "You're not logged in"}
</span>
<div className='cta'>
{userInfo ? (
<button className='button'>
Logout
</button>
) : (
<NavLink className='button' to='/login'>
Login
</NavLink>
)}
</div>
</div>
<nav className='container navigation'>
<NavLink to='/'>Home</NavLink>
<NavLink to='/login'>Login</NavLink>
<NavLink to='/register'>Register</NavLink>
<NavLink to='/user-profile'>Profile</NavLink>
</nav>
</header>
)
}
export default Header
Lưu ý rằng chúng tôi cũng đã sử dụng userInfo
để hiển thị các thông báo và phần tử thích hợp trên thanh điều hướng tương quan với trạng thái xác thực của người dùng. Bây giờ bạn có thể chuyển sang hiển thị thông tin chi tiết của người dùng trên màn hình Hồ sơ .
// ProfileScreen.js
import { useSelector } from 'react-redux'
import '../styles/profile.css'
const ProfileScreen = () => {
const { userInfo } = useSelector((state) => state.user)
return (
<div>
<figure>{userInfo?.firstName.charAt(0).toUpperCase()}</figure>
<span>
Welcome <strong>{userInfo?.firstName}!</strong> You can view this page
because you're logged in
</span>
</div>
)
}
export default ProfileScreen
Hiện tại, mọi người đều có thể truy cập trang Hồ sơ bất kể trạng thái xác thực. Chúng tôi muốn bảo vệ tuyến đường này bằng cách xác minh xem người dùng có tồn tại hay không trước khi cấp cho họ quyền truy cập vào trang. Logic này có thể được trích xuất thành một ProtectedRoute
thành phần duy nhất và chúng ta sẽ tạo nó tiếp theo.
Tạo một thư mục được gọi routing
trong src
và một tệp có tên ProtectedRoute.js
. ProtectedRoute
được dự định sử dụng như một phần tử tuyến mẹ, mà các phần tử con của nó được bảo vệ bởi logic nằm trong thành phần này.
Ở đây, chúng ta có thể sử dụng userInfo
giá trị của để phát hiện xem người dùng đã đăng nhập hay chưa. Nếu userInfo
vắng mặt, một mẫu trái phép sẽ được trả về. Nếu không, chúng tôi sử dụng thành phần của React Router Outlet
để hiển thị các tuyến con.
// ProtectedRoute.js
import { useSelector } from 'react-redux'
import { NavLink, Outlet } from 'react-router-dom'
const ProtectedRoute = () => {
const { userInfo } = useSelector((state) => state.user)
// show unauthorized screen if no user is found in redux store
if (!userInfo) {
return (
<div className='unauthorized'>
<h1>Unauthorized :(</h1>
<span>
<NavLink to='/login'>Login</NavLink> to gain access
</span>
</div>
)
}
// returns child route elements
return <Outlet />
}
export default ProtectedRoute
Theo tài liệu , <Outlet>
nên được sử dụng trong các phần tử tuyến mẹ để hiển thị các phần tử tuyến con của chúng. Điều này có nghĩa là <Outlet>
nó không hiển thị bất kỳ đánh dấu nào trên màn hình, nhưng được thay thế bằng các phần tử tuyến đường con.
Bây giờ bạn có thể kết thúc ProfileScreen
với tuyến đường được bảo vệ như sau:
// App.js
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import Header from './components/Header'
import LoginScreen from './screens/LoginScreen'
import RegisterScreen from './screens/RegisterScreen'
import ProfileScreen from './screens/ProfileScreen'
import HomeScreen from './screens/HomeScreen'
import ProtectedRoute from './routing/ProtectedRoute'
import './App.css'
function App() {
return (
<Router>
<Header />
<main className='container content'>
<Routes>
<Route path='/' element={<HomeScreen />} />
<Route path='/login' element={<LoginScreen />} />
<Route path='/register' element={<RegisterScreen />} />
<Route element={<ProtectedRoute />}>
<Route path='/user-profile' element={<ProfileScreen />} />
</Route>
</Routes>
</main>
</Router>
)
}
export default App
Và đó là hầu hết các ứng dụng hoàn thành! Bây giờ chúng ta hãy xem cách đăng xuất một người dùng.
Để đăng xuất người dùng, chúng tôi sẽ tạo một hành động đặt lại cửa hàng Redux về giá trị ban đầu và xóa mã thông báo khỏi bộ nhớ cục bộ. Bởi vì đây không phải là một tác vụ không đồng bộ, chúng tôi có thể tạo nó trực tiếp userSlice
bằng thuộc reducer
tính.
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { getUserDetails, registerUser, userLogin } from './userActions'
// initialize userToken from local storage
const userToken = localStorage.getItem('userToken')
? localStorage.getItem('userToken')
: null
const initialState = {
loading: false,
userInfo: null,
userToken,
error: null,
success: false,
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
logout: (state) => {
localStorage.removeItem('userToken') // deletes token from storage
state.loading = false
state.userInfo = null
state.userToken = null
state.error = null
},
},
extraReducers: {
// userLogin reducer ...
// registerUser reducer ...
// getUserDetails reducer ...
},
})
// export actions
export const { logout } = userSlice.actions
export default userSlice.reducer
Và gửi nó trong Header
thành phần:
// Header.js
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { NavLink } from 'react-router-dom'
import { getUserDetails } from '../features/user/userActions'
import { logout } from '../features/user/userSlice'
import '../styles/header.css'
const Header = () => {
const { userInfo, userToken } = useSelector((state) => state.user)
const dispatch = useDispatch()
// automatically authenticate user if token is found
useEffect(() => {
if (userToken) {
dispatch(getUserDetails())
}
}, [userToken, dispatch])
return (
<header>
<div className='header-status'>
<span>
{userInfo ? `Logged in as ${userInfo.email}` : "You're not logged in"}
</span>
<div className='cta'>
{userInfo ? (
<button className='button' onClick={() => dispatch(logout())}>
Logout
</button>
) : (
<NavLink className='button' to='/login'>
Login
</NavLink>
)}
</div>
</div>
<nav className='container navigation'>
<NavLink to='/'>Home</NavLink>
<NavLink to='/login'>Login</NavLink>
<NavLink to='/register'>Register</NavLink>
<NavLink to='/user-profile'>Profile</NavLink>
</nav>
</header>
)
}
export default Header
Và điều đó hoàn thành ứng dụng của chúng tôi! Bây giờ bạn có một ứng dụng ngăn xếp MERN với quy trình xác thực giao diện người dùng được quản lý bằng Bộ công cụ Redux.
Theo ý kiến của tôi, Bộ công cụ Redux mang lại trải nghiệm tốt hơn cho nhà phát triển, đặc biệt là so với mức độ khó khăn của Redux trước khi RTK phát hành.
Tôi thấy Bộ công cụ dễ dàng cắm vào các ứng dụng của mình khi tôi cần thực hiện quản lý nhà nước và không muốn tạo nó từ đầu bằng cách sử dụng React.Context
.
Lưu trữ mã thông báo trong WebStorage, tức là localStorage và sessionStorage, cũng là một cuộc thảo luận quan trọng . Cá nhân tôi thấy localStorage an toàn khi mã thông báo có tuổi thọ ngắn và không lưu trữ các chi tiết riêng tư như mật khẩu, chi tiết thẻ, v.v.
Hãy chia sẻ cách cá nhân bạn xử lý xác thực giao diện người dùng trong phần nhận xét!
Nguồn: https://blog.logrocket.com/handling-user-authentication-redux-toolkit/
1657699589
ユーザー認証は、無数の方法で処理できます。この機能がいかに重要であるかにより、プロセスを容易にする認証ソリューションを提供する企業が増えています。たとえば、Firebase、Auth0、NextAuth.jsなどです。
このようなサービスが認証と承認を処理する方法に関係なく、実装プロセスでは通常、一部のAPIエンドポイントを呼び出し、フロントエンドインフラストラクチャで使用されるプライベートトークン(通常はJSON Web Token、またはJWT)を受信します。
この記事では、Redux Toolkit(RTK)を使用してReactでフロントエンド認証ワークフローを作成する方法を学習します。createAsyncThunk
Expressバックエンドへの非同期リクエストを行うために、などの重要なToolkitAPIを利用します。createSlice
状態の変化を処理するためにも使用します。
このプロジェクトのバックエンドは、MongoDBデータベースでExpressを使用して構築されていますが、フロントエンドワークフローは、トークンを提供する使用するすべての認証サービスに引き続き適用する必要があります。プロジェクトのリポジトリにあるソースファイルをダウンロードして、データベースを設定する方法を説明し、アプリケーションをローカルで実行することができます。ここでライブデモをご覧ください。
フォローするには、次のものが必要です。
それでは、認証を始めましょう!
リポジトリには、このアプリケーションをブートストラップするために必要なファイルを含む[starter-files]
ブランチが含まれています。フロントエンドフォルダには、ホーム、ログイン、登録、およびそれぞれのルート、、、およびのプロファイル画面など、デモに表示されるさまざまなユーザーインターフェイスも含まれています。//login/register/user-profile
現在のルーティング構造は次のとおりです。
// App.js
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import Header from './components/Header'
import LoginScreen from './screens/LoginScreen'
import RegisterScreen from './screens/RegisterScreen'
import ProfileScreen from './screens/ProfileScreen'
import HomeScreen from './screens/HomeScreen'
import './App.css'
function App() {
return (
<Router>
<Header />
<main className='container content'>
<Routes>
<Route path='/' element={<HomeScreen />} />
<Route path='/login' element={<LoginScreen />} />
<Route path='/register' element={<RegisterScreen />} />
<Route path='/user-profile' element={<ProfileScreen />} />
</Routes>
</main>
</Router>
)
}
export default App
登録ページの入力フィールドは、ReactHookFormに接続されています。React Hook Formは入力値をきれいに取得し、handleSubmit
関数に返します。
// Login.js
import { useForm } from 'react-hook-form'
const LoginScreen = () => {
const { register, handleSubmit } = useForm()
const submitForm = (data) => {
console.log(data.email)
}
return (
<form onSubmit={handleSubmit(submitForm)}>
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
type='email'
className='form-input'
{...register('email')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<input
type='password'
className='form-input'
{...register('password')}
required
/>
</div>
<button type='submit' className='button'>
Login
</button>
</form>
)
}
export default LoginScreen
starter-files
ターミナルで、次のコマンドを使用してブランチのクローンを作成します。
git clone --branch starter-files --single-branch https://github.com/Chinwike1/redux-user-auth.git
そして、すべてのプロジェクトの依存関係をインストールします。
npm install
cd frontend
npm install
上記のプロセスにより、次のパッケージがフロントエンドにインストールされます
必要に応じて、ここでこれらのRedux用語に慣れることができます。
最後に、次のコマンドを使用してアプリケーションを実行します。
cd ..
npm run dev
Redux Toolkitは、ストアを作成する新しい方法を導入します。ストアの一部をスライスと呼ばれるさまざまなファイルに分割します。
スライスは、Redux状態の単一ユニットを表します。これは、アプリの単一機能のレデューサーロジックとアクションのコレクションであり、通常は1つのファイルにまとめて定義されます。私たちにとって、このファイルはfeatures/user
です。
RTKのcreateSlice
APIを使用すると、次のようにReduxスライスを作成できます。
// features/user/userSlice.js
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
loading: false,
userInfo: {}, // for user object
userToken: null, // for storing the JWT
error: null,
success: false, // for monitoring the registration process.
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {},
})
export default userSlice.reducer
reducer
プロパティをストアにインポートuserSlice
して、ルートのRedux状態オブジェクトに反映されるようにします。
// app/store.js
import { configureStore } from '@reduxjs/toolkit'
import userReducer from '../features/user/userSlice'
const store = configureStore({
reducer: {
user: userReducer
}
})
export default store
Redux Toolkitのドキュメントでは、アクションとレデューサーを1つのスライスファイルに統合することを提案しています。逆に、これら2つの部分を分割し、アクションを別のファイルに保存すると、コードが読みやすくなりますfeatures/user/userActions.js
。ここでは、バックエンドにリクエストを送信する非同期アクションを記述します。
ただし、その前に、現在利用可能なバックエンドルートの概要を簡単に説明します。
でホストされているExpressサーバーにはlocalhost:5000
、現在3つのルートがあります。
api/user/login
:ログインルート。リクエストを受け付けPOST
、引数としてユーザーのメールアドレスとパスワードを要求します。次に、認証が成功した後、またはエラーメッセージが表示された後、JWTを返します。このトークンの有効期間は12時間ですapi/user/register
:登録ルート。リクエストを受け付けPOST
、ユーザーの名、メールアドレス、パスワードが必要ですapi/user/profile
:許可ルート。リクエストを受け入れGET
、データベースから詳細を取得するためにユーザーのトークンが必要です。承認が成功した後、またはエラーメッセージが表示された後、ユーザーのオブジェクトを返します。これで、登録アクションから始めて、Reduxアクションの記述に進むことができます。
ではuserAction.js
、を使用createAsyncThunk
して、処理された結果をレデューサーに送信する前に、遅延した非同期ロジックを実行します。
createAsyncThunk
文字列アクションタイプ、コールバック関数、およびオプションのoptions
オブジェクトの3つのパラメーターを受け入れます。
コールバック関数は、考慮すべき2つの重要な引数があるため、重要なパラメーターです。
argdispatch
:これは、アクションが呼び出されたときにメソッドに渡される単一の値です。複数の値を渡す必要がある場合は、オブジェクトを渡すことができますthunkAPI
:通常Reduxサンク関数に渡されるパラメーターを含むオブジェクト。パラメータには、、、などが含まgetState
れますdispatchrejectWithValue
// userAction.js
export const registerUser = createAsyncThunk(
// action type string
'user/register',
// callback function
async ({ firstName, email, password }, { rejectWithValue }) => {
try {
// configure header's Content-Type as JSON
const config = {
headers: {
'Content-Type': 'application/json',
},
}
// make request to backend
await axios.post(
'/api/user/register',
{ firstName, email, password },
config
)
} catch (error) {
// return custom error message from API if any
if (error.response && error.response.data.message) {
return rejectWithValue(error.response.data.message)
} else {
return rejectWithValue(error.message)
}
}
}
)
In the code block above, we've taken the values from the register form and made a POST request to the register route using Axios. In the event of an error, thunkAPI.rejectWithValue sends the custom error message from the backend as a payload to the reducer. You may notice that the register API is called without referencing the server's base URL. This is possible with the proxy configuration existing in frontend/package.json.
.{
"proxy": "http://127.0.0.1:5000",
}
extraReducers
で作成されたアクションは、createAsyncThunk
3つの可能なライフサイクルアクションタイプを生成します:pending
、、、fulfilled
およびrejected
。
extraReducers
のプロパティでこれらのアクションタイプを利用userSlice
して、状態に適切な変更を加えることができます。
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { registerUser } from './userActions'
const initialState = {
loading: false,
userInfo: null,
userToken: null,
error: null,
success: false,
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {
// register user
[registerUser.pending]: (state) => {
state.loading = true
state.error = null
},
[registerUser.fulfilled]: (state, { payload }) => {
state.loading = false
state.success = true // registration successful
},
[registerUser.rejected]: (state, { payload }) => {
state.loading = false
state.error = payload
},
},
})
export default userSlice.reducer
この場合、登録が成功したことを示すために、アクションが実行されたときにsuccess
値を設定します。true
useDispatch
とuseSelector
フックに反応する以前にインストールしたパッケージを使用useSelector
して、Reduxストアから状態を読み取り、コンポーネントからアクションをディスパッチすることができます。useDispatchreact-redux
// RegisterScreen.js
import { useForm } from 'react-hook-form'
import { useDispatch, useSelector } from 'react-redux'
import Error from '../components/Error'
import { registerUser } from '../features/user/userActions'
const RegisterScreen = () => {
const { loading, error } = useSelector(
(state) => state.user
)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const submitForm = (data) => {
// check if passwords match
if (data.password !== data.confirmPassword) {
alert('Password mismatch')
return
}
// transform email string to lowercase to avoid case sensitivity issues during login
data.email = data.email.toLowerCase()
dispatch(registerUser(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{/* render error message with Error component, if any */}
{error && <Error>{error}</Error>}
<div className='form-group'>
<label htmlFor='firstName'>First Name</label>
<input
type='text'
className='form-input'
{...register('firstName')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
type='email'
className='form-input'
{...register('email')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<input
type='password'
className='form-input'
{...register('password')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='email'>Confirm Password</label>
<input
type='password'
className='form-input'
{...register('confirmPassword')}
required
/>
</div>
<button type='submit' className='button' disabled={loading}>
Register
</button>
</form>
)
}
export default RegisterScreen
フォームが送信されると、フィールドpassword
とconfirmPassword
フィールドが一致するかどうかを確認することから始めます。その場合registerUser
、フォームデータを引数として、アクションがディスパッチされます。
フックは、Reduxストアのオブジェクトからと状態の値useSelector
を引き出すために使用されます。これらの値は、リクエストの進行中に送信ボタンを無効にしたり、エラーメッセージを表示したりするなど、特定のUIを変更するために使用されます。loadingerroruser
現在、ユーザーが登録を完了すると、ユーザーが行ったことが成功したことを示すものはありません。Reactルーターのフックとフックのsuccess
値を使用して、登録後にユーザーをログインページにリダイレクトできます。userSliceuseNavigateuseEffect
これがどのように見えるかです:
// RegisterScreen.js
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { useDispatch, useSelector } from 'react-redux'
import { useNavigate } from 'react-router-dom'
import Error from '../components/Error'
import { registerUser } from '../features/user/userActions'
const RegisterScreen = () => {
const { loading, userInfo, error, success } = useSelector(
(state) => state.user
)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const navigate = useNavigate()
useEffect(() => {
// redirect user to login page if registration was successful
if (success) navigate('/login')
// redirect authenticated user to profile screen
if (userInfo) navigate('/user-profile')
}, [navigate, userInfo, success])
const submitForm = (data) => {
// check if passwords match
if (data.password !== data.confirmPassword) {
alert('Password mismatch')
return
}
// transform email string to lowercase to avoid case sensitivity issues in login
data.email = data.email.toLowerCase()
dispatch(registerUser(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{/* form markup... */}
</form>
)
}
export default RegisterScreen
Reduxロジックを書いているときに気付くおなじみのパターンがあります。通常は次のようになります。
この順序である必要はありませんが、通常は繰り返し発生する手順です。ログインアクションでこれを繰り返しましょう。
ログインアクションは登録アクションと似ていますが、ここでは、結果をレデューサーに渡す前に、バックエンドから返されたJWTをローカルストレージに保存します。
// userActions.js
export const userLogin = createAsyncThunk(
'user/login',
async ({ email, password }, { rejectWithValue }) => {
try {
// configure header's Content-Type as JSON
const config = {
headers: {
'Content-Type': 'application/json',
},
}
const { data } = await axios.post(
'/api/user/login',
{ email, password },
config
)
// store user's token in local storage
localStorage.setItem('userToken', data.userToken)
return data
} catch (error) {
// return custom error message from API if any
if (error.response && error.response.data.message) {
return rejectWithValue(error.response.data.message)
} else {
return rejectWithValue(error.message)
}
}
}
)
これで、でライフサイクルアクションタイプを処理できますuserSlice.js
。
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { registerUser, userLogin } from './userActions'
// initialize userToken from local storage
const userToken = localStorage.getItem('userToken')
? localStorage.getItem('userToken')
: null
const initialState = {
loading: false,
userInfo: null,
userToken,
error: null,
success: false,
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {
// login user
[userLogin.pending]: (state) => {
state.loading = true
state.error = null
},
[userLogin.fulfilled]: (state, { payload }) => {
state.loading = false
state.userInfo = payload
state.userToken = payload.userToken
},
[userLogin.rejected]: (state, { payload }) => {
state.loading = false
state.error = payload
},
// register user reducer...
},
})
export default userSlice.reducer
の値userToken
はからのトークンの値に依存するため、localStorage
上記のように、最初にトークンを初期化することをお勧めします。
これで、フォームの送信時にこのアクションをディスパッチして、好みのUIを更新できます。
// LoginScreen.js
import { useForm } from 'react-hook-form'
import { useDispatch, useSelector } from 'react-redux'
import { userLogin } from '../features/user/userActions'
import Error from '../components/Error'
const LoginScreen = () => {
const { loading, error } = useSelector((state) => state.user)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const submitForm = (data) => {
dispatch(userLogin(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{error && <Error>{error}</Error>}
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
type='email'
className='form-input'
{...register('email')}
required
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<input
type='password'
className='form-input'
{...register('password')}
required
/>
</div>
<button type='submit' className='button' disabled={loading}>
Login
</button>
</form>
)
}
export default LoginScreen
また、以前に認証されたユーザーがこのページにアクセスできないようにする必要があります。userInfo
の値を使用して、認証されたユーザーをとでログインページにリダイレクトできます。useNavigateuseEffect
// LoginScreen.js
import { useForm } from 'react-hook-form'
import { useNavigate } from 'react-router-dom'
import { useDispatch, useSelector } from 'react-redux'
import { userLogin } from '../features/user/userActions'
import { useEffect } from 'react'
import Error from '../components/Error'
const LoginScreen = () => {
const { loading, userInfo, error } = useSelector((state) => state.user)
const dispatch = useDispatch()
const { register, handleSubmit } = useForm()
const navigate = useNavigate()
// redirect authenticated user to profile screen
useEffect(() => {
if (userInfo) {
navigate('/user-profile')
}
}, [navigate, userInfo])
const submitForm = (data) => {
dispatch(userLogin(data))
}
return (
<form onSubmit={handleSubmit(submitForm)}>
{/* form markup... */}
</form>
)
}
export default LoginScreen
これで登録プロセスは完了です。次に、有効なトークンがまだブラウザに保存されているユーザーを自動的に認証する方法を説明します。
getUserProfile
アクションこのアクションでは、クライアント要求に対応するためにいくつかの資格情報を必要とする承認ルート(エンドポイントとして機能)に連絡します。ここで、必要なクレデンシャルはローカルに保存されたJWTです。
ユーザー資格情報がフォームを介して渡される認証ルートとは異なり、承認ルートでは、次の構文を使用してHTTPAuthorization
ヘッダーAuthorization: <auth-scheme> <credentials>
を介して資格情報をより安全に渡す必要があります。
auth-scheme
使用する認証スキームを表します。Bearer
JWTは認証スキームをサポートするように設計されており、それを使用します。詳細については、 RFC6750ベアラトークンを参照してください。
Axiosを使用すると、JWTを送信するようにリクエストのヘッダーオブジェクトを構成できます。
const config = {
headers: {
Authorization: 'Bearer eyJhbGInR5cCI6IkpXVCJ9.eyJpZTI4MTI1MywiZXhwIjoxNjU1MzI0NDUzfQ.FWMexh',
},
}
ユーザーのトークンはReduxストアで開始されるため、その値を抽出してこのリクエストに含める必要があります。
createAsyncThunk
のコールバック関数の2番目のパラメーターであるは、Reduxストアの現在の値を読み取ることができるメソッドをthunkAPI
提供します。getState
// userActions.js
export const getUserDetails = createAsyncThunk(
'user/getUserDetails',
async (arg, { getState, rejectWithValue }) => {
try {
// get user data from store
const { user } = getState()
// configure authorization header with user's token
const config = {
headers: {
Authorization: `Bearer ${user.userToken}`,
},
}
const { data } = await axios.get(`/api/user/profile`, config)
return data
} catch (error) {
if (error.response && error.response.data.message) {
return rejectWithValue(error.response.data.message)
} else {
return rejectWithValue(error.message)
}
}
}
)
次に、ライフサイクルアクションを処理します。
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { getUserDetails, registerUser, userLogin } from './userActions'
const initialState = {
// state values...
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: {
// login user reducer ...
// register user reducer ...
[getUserDetails.pending]: (state) => {
state.loading = true
},
[getUserDetails.fulfilled]: (state, { payload }) => {
state.loading = false
state.userInfo = payload
},
[getUserDetails.rejected]: (state, { payload }) => {
state.loading = false
},
},
})
export default userSlice.reducer
このHeader
コンポーネントは、アプリケーション全体で表示され続ける唯一のコンポーネントであるため、このアクションをディスパッチするのに適した場所です。ここでは、アプリがuserToken
の値の変更に気付いたときにこのアクションをディスパッチする必要があります。useEffect
これはフックで実現できます。
// Header.js
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { NavLink } from 'react-router-dom'
import { getUserDetails } from '../features/user/userActions'
import '../styles/header.css'
const Header = () => {
const { userInfo, userToken } = useSelector((state) => state.user)
const dispatch = useDispatch()
// automatically authenticate user if token is found
useEffect(() => {
if (userToken) {
dispatch(getUserDetails())
}
}, [userToken, dispatch])
return (
<header>
<div className='header-status'>
<span>
{userInfo ? `Logged in as ${userInfo.email}` : "You're not logged in"}
</span>
<div className='cta'>
{userInfo ? (
<button className='button'>
Logout
</button>
) : (
<NavLink className='button' to='/login'>
Login
</NavLink>
)}
</div>
</div>
<nav className='container navigation'>
<NavLink to='/'>Home</NavLink>
<NavLink to='/login'>Login</NavLink>
<NavLink to='/register'>Register</NavLink>
<NavLink to='/user-profile'>Profile</NavLink>
</nav>
</header>
)
}
export default Header
userInfo
また、ユーザーの認証ステータスに関連する適切なメッセージと要素をナビゲーションバーにレンダリングするために使用したことに注意してください。これで、プロファイル画面にユーザーの詳細を表示することができます。
// ProfileScreen.js
import { useSelector } from 'react-redux'
import '../styles/profile.css'
const ProfileScreen = () => {
const { userInfo } = useSelector((state) => state.user)
return (
<div>
<figure>{userInfo?.firstName.charAt(0).toUpperCase()}</figure>
<span>
Welcome <strong>{userInfo?.firstName}!</strong> You can view this page
because you're logged in
</span>
</div>
)
}
export default ProfileScreen
現在、認証ステータスに関係なく、誰でもプロファイルページにアクセスできます。ページへのアクセスを許可する前にユーザーが存在するかどうかを確認することで、このルートを保護したいと考えています。このロジックは単一のProtectedRoute
コンポーネントに抽出できます。次にそれを作成します。
という名前のフォルダと。という名前のファイルrouting
を作成します。親ルート要素として使用することを目的としており、その子要素はこのコンポーネントにあるロジックによって保護されています。srcProtectedRoute.jsProtectedRoute
ここでは、userInfo
の値を使用して、ユーザーがログインしているかどうかを検出できます。ログインしていuserInfo
ない場合は、許可されていないテンプレートが返されます。それ以外の場合は、ReactRouterのOutlet
コンポーネントを使用して子ルートをレンダリングします。
// ProtectedRoute.js
import { useSelector } from 'react-redux'
import { NavLink, Outlet } from 'react-router-dom'
const ProtectedRoute = () => {
const { userInfo } = useSelector((state) => state.user)
// show unauthorized screen if no user is found in redux store
if (!userInfo) {
return (
<div className='unauthorized'>
<h1>Unauthorized :(</h1>
<span>
<NavLink to='/login'>Login</NavLink> to gain access
</span>
</div>
)
}
// returns child route elements
return <Outlet />
}
export default ProtectedRoute
ドキュメントによると、<Outlet>
子ルート要素をレンダリングするには、親ルート要素でを使用する必要があります。これは<Outlet>
、画面にマークアップをレンダリングしないが、子ルート要素に置き換えられることを意味します。
ProfileScreen
これで、保護されたルートを次のようにラップできます。
// App.js
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import Header from './components/Header'
import LoginScreen from './screens/LoginScreen'
import RegisterScreen from './screens/RegisterScreen'
import ProfileScreen from './screens/ProfileScreen'
import HomeScreen from './screens/HomeScreen'
import ProtectedRoute from './routing/ProtectedRoute'
import './App.css'
function App() {
return (
<Router>
<Header />
<main className='container content'>
<Routes>
<Route path='/' element={<HomeScreen />} />
<Route path='/login' element={<LoginScreen />} />
<Route path='/register' element={<RegisterScreen />} />
<Route element={<ProtectedRoute />}>
<Route path='/user-profile' element={<ProfileScreen />} />
</Route>
</Routes>
</main>
</Router>
)
}
export default App
そして、これでほとんどのアプリケーションが完成しました。次に、ユーザーをログアウトする方法を見てみましょう。
ユーザーをログアウトするには、Reduxストアを初期値にリセットし、ローカルストレージからトークンをクリアするアクションを作成します。userSlice
これは非同期タスクではないため、プロパティを使用して直接作成できますreducer
。
// userSlice.js
import { createSlice } from '@reduxjs/toolkit'
import { getUserDetails, registerUser, userLogin } from './userActions'
// initialize userToken from local storage
const userToken = localStorage.getItem('userToken')
? localStorage.getItem('userToken')
: null
const initialState = {
loading: false,
userInfo: null,
userToken,
error: null,
success: false,
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
logout: (state) => {
localStorage.removeItem('userToken') // deletes token from storage
state.loading = false
state.userInfo = null
state.userToken = null
state.error = null
},
},
extraReducers: {
// userLogin reducer ...
// registerUser reducer ...
// getUserDetails reducer ...
},
})
// export actions
export const { logout } = userSlice.actions
export default userSlice.reducer
そしてそれをHeader
コンポーネントにディスパッチします:
// Header.js
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { NavLink } from 'react-router-dom'
import { getUserDetails } from '../features/user/userActions'
import { logout } from '../features/user/userSlice'
import '../styles/header.css'
const Header = () => {
const { userInfo, userToken } = useSelector((state) => state.user)
const dispatch = useDispatch()
// automatically authenticate user if token is found
useEffect(() => {
if (userToken) {
dispatch(getUserDetails())
}
}, [userToken, dispatch])
return (
<header>
<div className='header-status'>
<span>
{userInfo ? `Logged in as ${userInfo.email}` : "You're not logged in"}
</span>
<div className='cta'>
{userInfo ? (
<button className='button' onClick={() => dispatch(logout())}>
Logout
</button>
) : (
<NavLink className='button' to='/login'>
Login
</NavLink>
)}
</div>
</div>
<nav className='container navigation'>
<NavLink to='/'>Home</NavLink>
<NavLink to='/login'>Login</NavLink>
<NavLink to='/register'>Register</NavLink>
<NavLink to='/user-profile'>Profile</NavLink>
</nav>
</header>
)
}
export default Header
これでアプリケーションは完成です。これで、ReduxToolkitで管理されるフロントエンド認証ワークフローを備えたMERNスタックアプリケーションができました。
私の意見では、Redux Toolkitは、特にRTKのリリース前にReduxがどれほど困難であったかと比較して、より優れた開発者エクスペリエンスを提供します。
状態管理を実装する必要があり、を使用して最初から作成したくない場合は、Toolkitをアプリケーションに簡単にプラグインできますReact.Context
。
トークンをWebStorage、つまりlocalStorageとsessionStorageに保存することも重要な議論です。トークンの寿命が短く、パスワードやカードの詳細などの個人情報が保存されていない場合、私は個人的にlocalStorageが安全だと感じています。
コメントで、フロントエンド認証を個人的にどのように処理するかを自由に共有してください。
ソース:https ://blog.logrocket.com/handling-user-authentication-redux-toolkit/
1657690080
User authentication can be handled in a myriad of ways. Because of how important this feature is, we’ve come to see more companies provide authentication solutions to ease the process — Firebase, Auth0, and NextAuth.js to name a few.
Regardless of how such services handle authentication and authorization on their end, the implementation process typically involves calling some API endpoints and receiving a private token (usually a JSON Web Token, or JWT) to be used in your frontend infrastructure.
In this article, we’ll learn how to use Redux Toolkit (RTK) to create a frontend authentication workflow in React. We’ll make use of essential Toolkit APIs, like createAsyncThunk
, to make asynchronous requests to an Express backend. We’ll also use createSlice
to handle state changes.
See more at: https://blog.logrocket.com/handling-user-authentication-redux-toolkit/
1656250800
Laravel 6 cung cấp gói trình soạn thảo tách biệt để tạo giàn giáo auth trong ứng dụng laravel 6. Bất cứ khi nào bạn yêu cầu tạo auth trong laravel 6 thì bạn phải cài đặt gói laravel / ui trong laravel 6.
Sử dụng laravel / ui, bạn có thể tạo chế độ xem đơn giản với auth giống như cách bạn đã làm trước đây. Nhưng bạn phải sử dụng vue js hoặc react js với auth view trong laravel 6. Nhưng họ không cung cấp như mặc định, bạn phải làm theo vài bước để thực hiện auth.
Bạn phải làm theo vài bước để tạo auth trong ứng dụng laravel 6 của mình.
Đầu tiên, bạn cần cài đặt gói laravel / ui như sau:
composer require laravel/ui
Sau đó, bạn có thể chạy lệnh sau và kiểm tra thông tin lệnh ui.
php artisan ui --help
Đầu ra:
Description:
Swap the front-end scaffolding for the application
Usage:
ui [options] [--]
Arguments:
type The preset type (bootstrap, vue, react)
Options:
--auth Install authentication UI scaffolding
--option[=OPTION] Pass an option to the preset command (multiple values allowed)
-h, --help Display this help message
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi Force ANSI output
--no-ansi Disable ANSI output
-n, --no-interaction Do not ask any interactive question
--env[=ENV] The environment the command should run under
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
Bạn có thể sử dụng các lệnh sau để tạo xác thực:
Sử dụng Vue:
php artisan ui vue --auth
Sử dụng React:
php artisan ui react --auth
Bây giờ bạn cần chạy lệnh npm, nếu không bạn không thể nhìn thấy bố cục tốt hơn của trang đăng nhập và đăng ký.
Cài đặt NPM:
npm install
Chạy NPM:
npm run dev
Bây giờ bạn có thể chạy và kiểm tra ứng dụng của mình.
Nó sẽ hoạt động tuyệt vời.
Nguồn: https://www.itsolutionstuff.com/post/laravel-6-authentication-tutorialexample.html
1656247080
O Laravel 6 fornece um pacote de compositor septado para criar o scaffold de autenticação no aplicativo laravel 6. Sempre que você precisar criar autenticação em laravel 6, deverá instalar o pacote laravel/ui em laravel 6.
Usando laravel/ui, você pode criar uma visualização simples com autenticação da mesma forma que fez antes. Mas você tem que usar vue js ou reagir js com auth view no laravel 6. Mas eles não fornecem como padrão você tem que seguir alguns passos para fazer auth.
Você tem que seguir alguns passos para fazer autenticação em seu aplicativo laravel 6.
Primeiro você precisa instalar o pacote laravel/ui como abaixo:
composer require laravel/ui
Depois disso, você pode executar o seguinte comando e verificar as informações dos comandos da interface do usuário.
php artisan ui --help
Resultado:
Description:
Swap the front-end scaffolding for the application
Usage:
ui [options] [--]
Arguments:
type The preset type (bootstrap, vue, react)
Options:
--auth Install authentication UI scaffolding
--option[=OPTION] Pass an option to the preset command (multiple values allowed)
-h, --help Display this help message
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi Force ANSI output
--no-ansi Disable ANSI output
-n, --no-interaction Do not ask any interactive question
--env[=ENV] The environment the command should run under
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
Você pode usar os seguintes comandos para criar autenticação:
Usando o Vue:
php artisan ui vue --auth
Usando Reagir:
php artisan ui react --auth
Agora você precisa executar o comando npm, caso contrário não poderá ver melhor layout de login e página de registro.
Instale o NPM:
npm install
Execute o NPM:
npm run dev
Agora você pode executar e verificar seu aplicativo.
Vai funcionar muito bem.
Fonte: https://www.itsolutionstuff.com/post/laravel-6-authentication-tutorialexample.html
1656246900
Laravel 6 fournit un package septate composer pour créer un échafaudage d'authentification dans l'application laravel 6. Chaque fois que vous avez besoin de créer auth dans laravel 6, vous devez installer le paquet laravel/ui dans laravel 6.
En utilisant laravel/ui, vous pouvez créer une vue simple avec auth comme vous l'avez fait avant. Mais vous devez utiliser vue js ou réagir js avec la vue auth dans laravel 6. Mais ils ne fournissent pas par défaut, vous devez suivre quelques étapes pour faire l'authentification.
Vous devez suivre quelques étapes pour effectuer l'authentification dans votre application laravel 6.
Vous devez d'abord installer le paquet laravel/ui comme ci-dessous :
composer require laravel/ui
Après cela, vous pouvez exécuter la commande suivante et vérifier les informations sur les commandes ui.
php artisan ui --help
Production:
Description:
Swap the front-end scaffolding for the application
Usage:
ui [options] [--]
Arguments:
type The preset type (bootstrap, vue, react)
Options:
--auth Install authentication UI scaffolding
--option[=OPTION] Pass an option to the preset command (multiple values allowed)
-h, --help Display this help message
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi Force ANSI output
--no-ansi Disable ANSI output
-n, --no-interaction Do not ask any interactive question
--env[=ENV] The environment the command should run under
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
Vous pouvez utiliser les commandes suivantes pour créer auth :
Utilisation de Vue :
php artisan ui vue --auth
Utiliser React :
php artisan ui react --auth
Maintenant, vous devez exécuter la commande npm, sinon vous ne pouvez pas voir une meilleure disposition de la page de connexion et d'enregistrement.
Installez NPM :
npm install
Exécutez NPM :
npm run dev
Vous pouvez maintenant exécuter et vérifier votre application.
Cela fonctionnera très bien.
Source : https://www.itsolutionstuff.com/post/laravel-6-authentication-tutorialexample.html
1656243180
Laravel 6は、laravel6アプリケーションで認証スキャフォールドを作成するためのセプタムコンポーザーパッケージを提供します。laravel 6で認証を作成する必要がある場合は常に、laravel6にlaravel/uiパッケージをインストールする必要があります。
laravel / uiを使用すると、以前と同じようにauthを使用して単純なビューを作成できます。ただし、Vue jsを使用するか、laravel 6の認証ビューでjsを反応させる必要があります。ただし、デフォルトでは、認証を行うためにいくつかの手順を実行する必要があります。
Laravel 6アプリケーションで認証を行うには、いくつかの手順に従う必要があります。
まず、次のようにlaravel/uiパッケージをインストールする必要があります。
composer require laravel/ui
その後、次のコマンドを実行して、UIコマンド情報を確認できます。
php artisan ui --help
出力:
Description:
Swap the front-end scaffolding for the application
Usage:
ui [options] [--]
Arguments:
type The preset type (bootstrap, vue, react)
Options:
--auth Install authentication UI scaffolding
--option[=OPTION] Pass an option to the preset command (multiple values allowed)
-h, --help Display this help message
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi Force ANSI output
--no-ansi Disable ANSI output
-n, --no-interaction Do not ask any interactive question
--env[=ENV] The environment the command should run under
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
次のコマンドを使用して、認証を作成できます。
Vueの使用:
php artisan ui vue --auth
Reactの使用:
php artisan ui react --auth
ここでnpmコマンドを実行する必要があります。そうしないと、ログインと登録ページのより良いレイアウトを見ることができません。
NPMをインストールします。
npm install
NPMを実行します。
npm run dev
これで、アプリを実行して確認できます。
それはうまくいくでしょう。
ソース:https ://www.itsolutionstuff.com/post/laravel-6-authentication-tutorialexample.html
1656242940
Laravel 6 提供了 septate composer 包,用于在 laravel 6 应用程序中创建 auth 脚手架。每当您需要在 laravel 6 中创建身份验证时,您都必须在 laravel 6 中安装 laravel/ui 包。
使用 laravel/ui,您可以像之前一样使用 auth 创建简单的视图。但是您必须在 laravel 6 中使用 vue js 或使用 auth 视图对 js 做出反应。但它们没有默认提供您必须遵循几个步骤来进行身份验证。
您必须按照几个步骤在您的 laravel 6 应用程序中进行身份验证。
首先你需要安装 laravel/ui 包,如下所示:
composer require laravel/ui
之后,您可以运行以下命令并检查 ui 命令信息。
php artisan ui --help
输出:
Description:
Swap the front-end scaffolding for the application
Usage:
ui [options] [--]
Arguments:
type The preset type (bootstrap, vue, react)
Options:
--auth Install authentication UI scaffolding
--option[=OPTION] Pass an option to the preset command (multiple values allowed)
-h, --help Display this help message
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi Force ANSI output
--no-ansi Disable ANSI output
-n, --no-interaction Do not ask any interactive question
--env[=ENV] The environment the command should run under
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
您可以使用以下命令来创建身份验证:
使用 Vue:
php artisan ui vue --auth
使用反应:
php artisan ui react --auth
现在您需要运行 npm 命令,否则您将看不到更好的登录和注册页面布局。
安装 NPM:
npm install
运行 NPM:
npm run dev
现在您可以运行并检查您的应用程序。
它会很好用。
来源:https ://www.itsolutionstuff.com/post/laravel-6-authentication-tutorialexample.html
1656242820
Laravel 6 proporciona un paquete septate composer para crear un andamio de autenticación en la aplicación laravel 6. Siempre que necesite crear autenticación en laravel 6, debe instalar el paquete laravel/ui en laravel 6.
Usando laravel/ui puede crear una vista simple con autenticación como lo hizo antes. Pero debe usar vue js o reaccionar js con la vista de autenticación en laravel 6. Pero no proporcionan de forma predeterminada, debe seguir algunos pasos para realizar la autenticación.
Debe seguir algunos pasos para realizar la autenticación en su aplicación laravel 6.
Primero necesita instalar el paquete laravel/ui como se muestra a continuación:
composer require laravel/ui
Después de eso, puede ejecutar el siguiente comando y verificar la información de los comandos de ui.
php artisan ui --help
Producción:
Description:
Swap the front-end scaffolding for the application
Usage:
ui [options] [--]
Arguments:
type The preset type (bootstrap, vue, react)
Options:
--auth Install authentication UI scaffolding
--option[=OPTION] Pass an option to the preset command (multiple values allowed)
-h, --help Display this help message
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi Force ANSI output
--no-ansi Disable ANSI output
-n, --no-interaction Do not ask any interactive question
--env[=ENV] The environment the command should run under
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
Puede usar los siguientes comandos para crear autenticación:
Usando Vue:
php artisan ui vue --auth
Usando reaccionar:
php artisan ui react --auth
Ahora necesita ejecutar el comando npm; de lo contrario, no podrá ver un mejor diseño de la página de inicio de sesión y registro.
Instalar NPM:
npm install
Ejecutar NPM:
npm run dev
Ahora puede ejecutar y verificar su aplicación.
Funcionará muy bien.
Fuente: https://www.itsolutionstuff.com/post/laravel-6-authentication-tutorialexample.html
1655723100
Azure AD Appのみの認証は、M365サービスへの認証と、データの読み取り、データのアップロード、自動化スクリプトを介したバックエンドジョブの実行などの操作に使用されています。Microsoftは、Azure ADに登録されているアプリケーションに証明書ベースの認証を使用して、M365または任意のクラウドサービスに対して認証することをお勧めします。CBAは、ユーザーのIDを検証するための非常に堅牢で安全なメカニズムです。
この記事では、私が最近遭遇したユースケースについて説明したいと思います。以前は、SharePoint App Only認証を使用しています。これはACS(Azure Controlサービス)の概念であり、サイトコレクションの管理者はサイトコレクションに/_layouts/appregnew.aspxを追加することで、クライアントIDとクライアントシークレットを作成できます。アプリケーションで。ただし、このACSアプリのみのアクセストークン方式を使用する場合の問題はほとんどありません。
ACSトークンベースの認証の詳細については、参照セクションを参照してください。
幸いなことに、Azure ADアプリでは、SharePointのAPIアクセス許可に「Sites.Selected」という新しいアクセス許可が追加されました。これにより、AzureADアプリは単一のクライアントと証明書の詳細を使用して複数のサイトコレクションに対して認証できます。
証明書を使用したこのAzureADアプリのみの認証に進む前に、Azure ADの証明書ベースの認証(別名CBA)とは何かを理解しようとします。AzureADには2種類のCBAがあります。
以前は、CBAを実装するために、ADFSサービスをユーザーとAzureADの間にデプロイする必要がありました。ADFSを使用するCBAは、X.509証明書を使用してAzureADに対して認証します。
Azure AD CBAである最新バージョンでは、ADFSの構成と展開は必要ありません。ユーザーはAzureADと直接対話し、アプリケーションに対して認証できます。
ADFSおよびAzureADCBAを使用したCBAの詳細については、参照セクションに記載されている記事を参照してください。
次に、APIアクセス許可「サイト」を使用してAzureADアプリを作成する手順を実行します。タイプ「アプリケーション」の「選択済み」。次に、このAzure ADアプリを使用して、複数のサイトコレクションに対して認証します。記事を正しく実行するには、最新のPnPPowershellバージョンがインストールされている必要があります。
ステップ1
管理者としてPowerShellISEまたはコマンドウィンドウを開きます。
ステップ2
以下のPSコマンドを実行してアプリケーションを登録します。以下のコマンドを実行しているアカウントに「グローバル管理者」権限があることを確認してください。アカウントでMFA(Multi-Factor Authentication Enabled)がある場合は、プロンプトに従います
Register-PnPAzureADApp -ApplicationName SPSitesSelected -Tenant contosodev.onmicrosoft.com -Store CurrentUser -SharePointApplicationPermissions "Sites.Selected" -Interactive
ステップ3
認証が成功すると、必要なアーティファクトをチェックして同意フローを開始するために60秒待つことを示す以下のメッセージが表示されます。
ステップ4
アプリを登録してから、証明書と指紋を作成するために、もう一度認証するように求められます。プロンプトに従います
ステップ5
これで、以下のように認証が成功すると同意がポップアップ表示されます。アプリ名(この場合はSPSites Selected)と、承認およびキャンセルするオプションが表示されます。
[アプリ情報]をクリックして、アプリの詳細を確認することもできます。
ステップ6
[同意する]をクリックして同意することに同意すると、コマンド出力ウィンドウから次の情報が表示されます。
次の値があります、
クライアントID、指紋、およびPfxファイルとCerファイルの場所のみをメモする必要があります。
上記の手順は、AzureADアプリケーションが「Sites.Selected」である必要なアクセス許可で作成されていることを確認します。これは、特定のサイトに対してのみ認証するようにAzureADアプリを構成できるようになったことを意味します。
ここで、Azure ADアプリへのアクセスを許可するには、次の一連のコマンドを実行します。
ステップ1
グローバル管理者の資格情報を持つPnPPowerShellモジュールを使用して、テナントのSharePoint管理者URLにログインします。
Connect-PnPOnline -Url "https://contoso-admin.sharepoint.com" -Interactive
ステップ2
認証時に、PnP管理シェルが実行できる権限に関する次の情報を取得します。
ここでは、組織に代わって同意するか、チェックを外したままにすることができます。[組織を代表して同意する]をオンにした場合、他のユーザーは同意を求められません。
ステップ3
次のコマンドを実行して、アプリに権限を付与します。アプリに付与できる権限は、「読み取り」または「書き込み」の2セットのみであることに注意してください。
Grant-PnPAzureADAppSitePermission -AppId 'YOUR APP ID HERE' -DisplayName 'APP DISPLAY NAME HERE' -Site 'https://contosodev.sharepoint.com/sites/CBADemo1' -Permissions Write
ステップ1
権限が付与されているサイトに接続して、アプリへのアクセスを検証します。問題なくコンテンツが表示されるはずです。この場合、以前の接続が存在する場合は、以前のPnP接続から切断します。
Disconnect-PnPOnline
ステップ2
以下のコマンドを入力して、他にPnP接続が存在しないことを確認します。
Get-PnPConnection
「現在の接続にはSharePointコンテキストがありません」というエラーが表示されるはずです。
ステップ3
次に、AzureADアプリのクレデンシャルを使用してSharePointサイトに接続します。
Connect-PnPOnline -Url "https://contosodev.sharepoint.com/sites/CBADemo2" -ClientId "AZURE AD APP ID" -Thumbprint "CERT THUMP PRINT" -Tenant "YOUR TENANT DOMAIN"
アプリID(クライアントID)とフィンガープリントの値は、[AzureADアプリの作成]セクションの手順6で生成されることに注意してください。Azure ADポータルにログインし、[エンタープライズアプリケーション]でアプリを確認することで、AzureADから詳細を取得することもできます。
同様に、テナントドメインは、クイック起動から[Azure Active Directory]をクリックして、[プライマリドメイン]の値を探すことで取得できます。
ステップ4
次に、以下のコマンドを実行して、アプリが接続されているサイトを確認します。
Get-PnPSite
ステップ5
次に、以下のコマンドを実行して、このサイトコレクション内のすべてのリストのリストを取得します。
Get-PnPList
AzureADアプリがアクセスする必要のある他のサイトコレクションに対しても同じコマンドを実行できます。
ステップ6
アクセスが許可されていないサイトに接続して、アプリへのアクセスを検証します。403forbiddenエラーが表示されるはずです。
Connect-PnPOnline -Url "https://contosodev.sharepoint.com/sites/M365POC" -ClientId "YOUR CLIENT ID" -Thumbprint "CERT THUMP PRINT" -Tenant "contosodev.onmicrosoft.com"
クライアントIDと証明書のサンププリントを使用してサイトに接続しているときにエラーがスローされないことに気付いたかもしれませんが、サイトの詳細またはリストのコンテンツを取得するときにエラーがスローされます。
完全なスクリプト
#Creating Azure AD App with Certificate Thumbprint.
Register-PnPAzureADApp -ApplicationName SPSitesSelected -Tenant contosodev.onmicrosoft.com -Store CurrentUser -SharePointApplicationPermissions "Sites.Selected" -Interactive
#Connecting to SharePoint online Admin center using Global Admin Credentials
Connect-PnPOnline -Url "https://contosodev-admin.sharepoint.com" -Interactive
#Granting Access to Azure AD App for specific sites
Grant-PnPAzureADAppSitePermission -AppId 'bf8f7d56-c37f-44d6-abcb-670832e49b9c' -DisplayName 'SPSitesSelected' -Site 'https://contosodev.sharepoint.com/sites/CBADemo1' -Permissions Write
Grant-PnPAzureADAppSitePermission -AppId 'bf8f7d56-c37f-44d6-abcb-670832e49b9c' -DisplayName 'SPSitesSelected' -Site 'https://contosodev.sharepoint.com/sites/CBADemo2' -Permissions Write
#Disconnecting the previous connections
Disconnect-PnPOnline
#Validating the connection
Get-PnPConnection
#Connecting to SPO site using Azure AD App
Connect-PnPOnline -Url "https://contosodev.sharepoint.com/sites/CBADemo1" -ClientId "bf8f7d56-c37f-44d6-abcb-670832e49b9c" -Thumbprint "6A506565EABCD759C204C8517955301420A0C02D" -Tenant "contosodev.onmicrosoft.com"
#Gettting site details
Get-PnPSite
#Getting the list content
Get-PnPList
#Disconnecting from the Azure AD App connection
Disconnect-PnPOnline
#Connecting to SPO site using Azure Ad App with other site where access is not being granted.
Connect-PnPOnline -Url "https://contosodev.sharepoint.com/sites/M365POC" -ClientId "bf8f7d56-c37f-44d6-abcb-670832e49b9c" -Thumbprint "6A506565EABCD759C204C8517955301420A0C02D" -Tenant "contosodev.onmicrosoft.com"
#Get the site details
Get-PnPSite
#Get list content for site
Get-PnPList
したがって、この記事では、
参考文献
1655722800
La autenticación solo de aplicaciones de Azure AD se usa para autenticarse en los servicios de M365 y realizar algunas operaciones, como leer los datos, cargar los datos o realizar algunos trabajos de back-end a través de scripts de automatización. Microsoft recomienda utilizar la autenticación basada en certificados para sus aplicaciones registradas en Azure AD para autenticarse en el M365 o en cualquier servicio en la nube. CBA es un mecanismo extremadamente robusto y seguro para validar la identidad del usuario.
En este artículo, quiero hablar sobre el caso de uso que encontré recientemente. Anteriormente, estaba usando la autenticación de solo aplicación de SharePoint, que es el concepto de ACS (servicios de control de Azure), donde el administrador de la colección de sitios puede crear un ID de cliente y un secreto de cliente agregando /_layouts/appregnew.aspx en la colección de sitios y usando las credenciales del cliente. en aplicación. Sin embargo, hay algunos problemas al usar este método de token de acceso de solo aplicación de ACS.
Puede consultar más información sobre la autenticación basada en token de ACS en la sección de referencias.
La buena noticia es que en la aplicación Azure AD, los permisos de API para SharePoint vienen con nuevos permisos llamados "Sitios.Seleccionados", que permitirán que su aplicación Azure AD se autentique en varias colecciones de sitios utilizando un solo cliente y detalles del certificado.
Antes de pasar a esta autenticación solo de aplicaciones de Azure AD mediante certificados, intentaremos comprender qué es la autenticación basada en certificados (también conocida como CBA) en Azure AD. Hay 2 tipos de CBA en Azure AD.
Anteriormente, para implementar el CBA, los servicios de ADFS deben implementarse entre los usuarios y Azure AD. CBA con ADFS usa certificados X.509 para autenticarse en Azure AD.
La última versión, que es Azure AD CBA, no necesita configuración ni implementación de AD FS. Los usuarios pueden interactuar directamente con Azure AD y autenticarse en las aplicaciones.
Para obtener más detalles sobre CBA con AD FS y Azure AD CBA, puede consultar los artículos mencionados en la sección de referencias.
Ahora seguiremos los pasos para crear la aplicación Azure AD, con permisos de API "Sitios. Seleccionado” de tipo “Aplicación”. Luego use esta aplicación de Azure AD para autenticarse en varias colecciones de sitios. Para poder seguir correctamente el artículo, es necesario tener instalada la última versión de PnP Powershell.
Paso 1
Abra PowerShell ISE o las ventanas de comandos como administrador.
Paso 2
Registre la aplicación ejecutando el siguiente comando PS. Asegúrese de que la cuenta que ejecuta los siguientes comandos tenga derechos de 'Administrador global'. Siga las indicaciones si la cuenta tiene MFA (autenticación multifactor habilitada)
Register-PnPAzureADApp -ApplicationName SPSitesSelected -Tenant contosodev.onmicrosoft.com -Store CurrentUser -SharePointApplicationPermissions "Sites.Selected" -Interactive
Paso 3
En una autenticación exitosa, recibirá el siguiente mensaje que dice que debe esperar 60 segundos para verificar los artefactos requeridos e iniciar el flujo de consentimiento.
Paso 4
Se le pedirá que se autentique una vez más para registrar la aplicación y luego para crear un certificado y una huella digital. Siga las instrucciones de nuevo
Paso 5
Ahora tendrá una ventana emergente de consentimiento en una autenticación exitosa similar a la siguiente. Muestra el nombre de la aplicación (en este caso, SPSites seleccionado) y opciones para Aceptar y cancelar.
También puede verificar los detalles de la aplicación haciendo clic en 'Información de la aplicación'.
Paso 6
Después de aceptar el consentimiento haciendo clic en 'Aceptar', debería ver la siguiente información en la ventana de salida del comando.
Tendrás los siguientes valores,
Debe anotar solo el ID del cliente, la huella digital y la ubicación de los archivos Pfx y Cer.
Los pasos anteriores confirman que la aplicación de Azure AD se crea con los permisos necesarios, que es "Sitios.Seleccionados". Esto significa que la aplicación de Azure AD ahora se puede configurar para autenticarse solo en sitios específicos.
Ahora, para otorgar acceso a la aplicación Azure AD, ejecute el siguiente conjunto de comandos.
Paso 1
Inicie sesión en la URL de administración de SharePoint para su arrendatario mediante el módulo PnP PowerShell con credenciales de administrador global.
Connect-PnPOnline -Url "https://contoso-admin.sharepoint.com" -Interactive
Paso 2
En la autenticación, obtendrá la siguiente información, sobre los permisos sobre lo que podría hacer el shell de administración de PnP.
Aquí puede dar su consentimiento en nombre de la organización o dejarlo sin marcar. Si marcó 'Consentimiento en nombre de su organización', no se solicitará el consentimiento de ningún otro usuario.
Paso 3
Otorgue el permiso a la aplicación ejecutando el siguiente comando. Tenga en cuenta que solo hay 2 conjuntos de permisos que puede otorgar a la aplicación, que es 'Lectura' o 'Escritura'.
Grant-PnPAzureADAppSitePermission -AppId 'YOUR APP ID HERE' -DisplayName 'APP DISPLAY NAME HERE' -Site 'https://contosodev.sharepoint.com/sites/CBADemo1' -Permissions Write
Paso 1
Valide el acceso a la aplicación conectándose a sitios que tengan permisos. Debería ver el contenido sin ningún problema. En este caso, desconéctese de las conexiones PnP anteriores si existen conexiones anteriores.
Disconnect-PnPOnline
Paso 2
Valide que no exista otra conexión PnP escribiendo el siguiente comando.
Get-PnPConnection
Debería ver el error que dice "La conexión actual no tiene contexto de SharePoint".
Paso 3
Ahora conéctese al sitio de SharePoint usando las credenciales de la aplicación Azure AD.
Connect-PnPOnline -Url "https://contosodev.sharepoint.com/sites/CBADemo2" -ClientId "AZURE AD APP ID" -Thumbprint "CERT THUMP PRINT" -Tenant "YOUR TENANT DOMAIN"
Tenga en cuenta que los valores de ID de aplicación (ID de cliente) y Huella digital se generan en el Paso 6 en la sección "Crear aplicación de Azure AD". También puede obtener los detalles de su Azure AD iniciando sesión en Azure AD Portal y revisando su aplicación en 'Aplicaciones empresariales'.
De manera similar, el dominio del arrendatario se puede obtener haciendo clic en 'Azure Active Directory' desde el inicio rápido y buscando el valor 'Dominio principal'.
Paso 4
Ahora verifique a qué sitio está conectada la aplicación ejecutando el siguiente comando.
Get-PnPSite
Paso 5
Ahora obtenga la lista de todas las listas en esta colección de sitios ejecutando el siguiente comando.
Get-PnPList
Puede ejecutar los mismos comandos para cualquier otra colección de sitios a la que necesite acceder la aplicación Azure AD.
Paso 6
Valide el acceso a la aplicación conectándose a sitios a los que no se les otorga acceso. Debería ver el error 403 prohibido.
Connect-PnPOnline -Url "https://contosodev.sharepoint.com/sites/M365POC" -ClientId "YOUR CLIENT ID" -Thumbprint "CERT THUMP PRINT" -Tenant "contosodev.onmicrosoft.com"
Es posible que haya notado que no arroja ningún error al conectarse al sitio utilizando la ID del cliente y la impresión del certificado, sin embargo, arroja un error al obtener los detalles del sitio o el contenido de la lista.
Guión completo
#Creating Azure AD App with Certificate Thumbprint.
Register-PnPAzureADApp -ApplicationName SPSitesSelected -Tenant contosodev.onmicrosoft.com -Store CurrentUser -SharePointApplicationPermissions "Sites.Selected" -Interactive
#Connecting to SharePoint online Admin center using Global Admin Credentials
Connect-PnPOnline -Url "https://contosodev-admin.sharepoint.com" -Interactive
#Granting Access to Azure AD App for specific sites
Grant-PnPAzureADAppSitePermission -AppId 'bf8f7d56-c37f-44d6-abcb-670832e49b9c' -DisplayName 'SPSitesSelected' -Site 'https://contosodev.sharepoint.com/sites/CBADemo1' -Permissions Write
Grant-PnPAzureADAppSitePermission -AppId 'bf8f7d56-c37f-44d6-abcb-670832e49b9c' -DisplayName 'SPSitesSelected' -Site 'https://contosodev.sharepoint.com/sites/CBADemo2' -Permissions Write
#Disconnecting the previous connections
Disconnect-PnPOnline
#Validating the connection
Get-PnPConnection
#Connecting to SPO site using Azure AD App
Connect-PnPOnline -Url "https://contosodev.sharepoint.com/sites/CBADemo1" -ClientId "bf8f7d56-c37f-44d6-abcb-670832e49b9c" -Thumbprint "6A506565EABCD759C204C8517955301420A0C02D" -Tenant "contosodev.onmicrosoft.com"
#Gettting site details
Get-PnPSite
#Getting the list content
Get-PnPList
#Disconnecting from the Azure AD App connection
Disconnect-PnPOnline
#Connecting to SPO site using Azure Ad App with other site where access is not being granted.
Connect-PnPOnline -Url "https://contosodev.sharepoint.com/sites/M365POC" -ClientId "bf8f7d56-c37f-44d6-abcb-670832e49b9c" -Thumbprint "6A506565EABCD759C204C8517955301420A0C02D" -Tenant "contosodev.onmicrosoft.com"
#Get the site details
Get-PnPSite
#Get list content for site
Get-PnPList
Por lo tanto, en este artículo, hemos aprendido sobre
Referencias
Esta historia se publicó originalmente en https://www.c-sharpcorner.com/article/certificate-based-authentication-to-connect-to-sharepoint-online-sites/