许 志强

许 志强

1657710840

使用 Redux Toolkit 处理用户身份验证

可以通过多种方式处理用户身份验证。由于此功能的重要性,我们已经看到越来越多的公司提供身份验证解决方案来简化流程 - Firebase、Auth0 和 NextAuth.js 等等。

无论此类服务如何处理最终的身份验证和授权,实现过程通常涉及调用一些 API 端点并接收要在前端基础架构中使用的私有令牌(通常是JSON Web 令牌或 JWT)。

在本文中,我们将学习如何使用 Redux Toolkit (RTK) 在 React 中创建前端身份验证工作流。我们将使用基本的 Toolkit API,例如createAsyncThunk向 Express 后端发出异步请求。我们还将用于createSlice处理状态更改。

简介 App Demo

该项目的后端是使用 Express 和 MongoDB 数据库构建的,但前端工作流程仍应适用于您使用的任何提供令牌的身份验证服务。您可以下载项目存储库中的源文件,其中包含有关如何设置数据库并可以在本地运行应用程序的说明。在此处查看现场演示

要继续学习,您需要:

  • 熟悉 React 概念,例如钩子和状态
  • 熟悉 Redux Toolkit,尤其是createSlicecreateAsyncThunk API
  • 了解 ES6 语法和 JavaScript Promises。

现在,让我们开始认证吧!

从 GitHub 克隆起始文件

存储库包含一个[starter-files] 分支,其中包含引导此应用程序所需的文件。前端文件夹还包括演示中看到的各种用户界面,例如HomeLoginRegisterProfile屏幕以及各自的路由//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 存储

Redux Toolkit 引入了一种创建 store 的新方法。它将存储的各个部分分成不同的文件,称为切片。

切片代表 Redux 状态的单个单元。它是应用程序中单个功能的 reducer 逻辑和操作的集合,通常在单个文件中一起定义。对我们来说,这个文件是features/user.

通过使用 RTK 的createSliceAPI,我们可以像这样创建一个 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 DevTools 扩展的屏幕截图

组织 Redux 切片和操作

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 操作,从注册操作开始。

Redux 身份验证:注册操作

userAction.js中,您将使用createAsyncThunk在将处理后的结果发送到减速器之前执行延迟的异步逻辑。

createAsyncThunk接受三个参数:字符串操作类型、回调函数和可选options对象。

回调函数是一个重要参数,因为它有两个您应该考虑的关键参数:

  • argdispatch:这是调用动作时传递给方法的单个值。如果需要传递多个值,可以传入一个对象
  • thunkAPI: 包含通常传递给 Redux thunk 函数的参数的对象。参数包括getStatedispatchrejectWithValue
// 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生成三种可能的生命周期操作类型:pendingfulfilledrejected

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为操作完成以表示成功注册。

React ReduxuseDispatchuseSelector钩子

通过使用useSelectoruseDispatchreact-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

提交表单后,我们首先验证passwordandconfirmPassword字段是否匹配。如果他们这样做了,registerUser则分派该操作,并将表单数据作为其参数。

useSelector钩子用于从Redux 存储中的对象中提取loadingerror状态值。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 逻辑时,您会注意到一个熟悉的模式。它通常是这样的:

  • 在动作文件中创建 Redux 动作
  • 编写 reducer 逻辑来处理上述动作的状态变化
  • 组件内的调度操作
  • 进行必要的 UI 更新/由于分派的操作而发生的副作用

它不必按那个顺序,但它们通常是重复发生的步骤。让我们通过登录操作重复此操作。

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组件中,接下来我们将创建它。

使用 React Router 保护路由

创建一个名为routinginsrc的文件夹和一个名为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 身份验证:注销操作

要注销用户,我们将创建一个操作,将 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

Chuck 注销演示

这样就完成了我们的应用程序!您现在拥有一个 MERN 堆栈应用程序,其中包含使用 Redux Toolkit 管理的前端身份验证工作流。

结论

在我看来,Redux Toolkit 提供了更好的开发者体验,特别是与 RTK 发布之前 Redux 的难度相比。

当我需要实现状态管理并且不想使用React.Context.

在 WebStorage 中存储令牌,即 localStorage 和 sessionStorage,也是一个重要的讨论。当令牌的寿命很短并且不存储密码、卡详细信息等私人详细信息时,我个人认为 localStorage 是安全的。

请随时在评论中分享您个人如何处理前端身份验证!

来源:https ://blog.logrocket.com/handling-user-authentication-redux-toolkit/

#redux #authentic #react 

使用 Redux Toolkit 处理用户身份验证
Léon  Peltier

Léon Peltier

1657707840

Gestion De L'authentification Des Utilisateurs Avec Redux Toolkit

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 createSlicepour gérer les changements d'état.

Démo de l'application

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 :

  • Familiarité avec les concepts de React, tels que les crochets et l'état
  • Familiarité avec Redux Toolkit, en particulier les API createSlice et createAsyncThunk
  • Connaissance de la syntaxe ES6 et des promesses JavaScript.

Maintenant, commençons à nous authentifier !

Cloner des fichiers de démarrage à partir de GitHub

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, /registeret /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 handleSubmitfonction.

 

// 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-filesbranche 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

Configuration du magasin Redux

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 reducerpropriété userSlicedans 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

Capture d'écran de l'extension Redux DevTools

Organisation des tranches et des actions Redux

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.

Architecture dorsale

Notre serveur Express, hébergé sur localhost:5000, propose actuellement trois routes :

  • api/user/login: la route de connexion. Il accepte POSTles 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 heures
  • api/user/register: la route du registre. Il accepte POSTles demandes et nécessite le prénom, l'e-mail et le mot de passe de l'utilisateur
  • api/user/profile: une route d'autorisation. Il accepte GETles 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.

Authentification Redux : Enregistrer l'action

Dans userAction.js, vous utiliserez createAsyncThunkpour exécuter une logique asynchrone retardée avant d'envoyer les résultats traités aux réducteurs.

createAsyncThunkaccepte trois paramètres : un type d'action de chaîne, une fonction de rappel et un optionsobjet 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 dispatchméthode lorsque l'action est appelée. Si vous devez transmettre plusieurs valeurs, vous pouvez transmettre un objet
  • thunkAPI: 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",
}

Gestion des fonctions asynchrones dansextraReducers

Les actions créées avec createAsyncThunkgénèrent trois types d'action de cycle de vie possibles : pending, fulfilledet rejected.

Vous pouvez utiliser ces types d'action dans la extraReducerspropriété de userSlicepour 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 successvaleur truelorsque l'action est accomplie pour signifier un enregistrement réussi.

React Redux useDispatchet useSelectorcrochets

En utilisant useSelectoret à useDispatchpartir du react-reduxpackage 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 passwordet correspondent. confirmPasswordSi c'est le cas, l' registerUseraction est envoyée en prenant les données du formulaire comme argument.

Le useSelectorcrochet est utilisé pour extraire les valeurs d'état loadinget 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 successvaleur à userSlicecôté du crochet du routeur React useNavigateet du useEffectcrochet, 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 :

  • Créer une action Redux dans le fichier d'action
  • Écrire une logique de réduction pour gérer les changements d'état pour l'action ci-dessus
  • Répartir l'action dans un composant
  • Effectuez les mises à jour/effets secondaires nécessaires de l'interface utilisateur qui se produisent à la suite de l'action envoyée

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.

Authentification Redux : 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

Initialiser au début

Étant donné que la valeur de userTokendé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. userInfoLa valeur de peut être utilisée pour rediriger un utilisateur authentifié vers la page de connexion avec useNavigateet 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.

getUserProfileaction

Pour 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-schemereprésente le schéma d'authentification que vous souhaitez utiliser. Les JWT sont conçus pour prendre en charge le Bearersché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 createAsyncThunkla fonction de rappel de thunkAPI, fournit une getStatemé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 Headercomposant 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 userTokenla valeur de . Ceci peut être réalisé avec le useEffectcrochet.

// 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 userInfod'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 ProtectedRoutecomposant, et nous allons le créer ensuite.

Routes protégées avec React Router

Créez un dossier appelé routingdans srcet un fichier nommé ProtectedRoute.js. ProtectedRouteest 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 userInfola valeur de pour détecter si un utilisateur est connecté. Si userInfoest absent, un modèle non autorisé est renvoyé. Sinon, nous utilisons le composant de React Router Outletpour 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 ProfileScreenla 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.

Authentification Redux : action de déconnexion

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 userSliceavec la reducerproprié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 Headercomposant :

// 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

Démo de déconnexion de Chuck

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.

Conclusion

À 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/

#redux #authentic #react 

Gestion De L'authentification Des Utilisateurs Avec Redux Toolkit

Manipulando A Autenticação Do Usuário Com O Redux Toolkit

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 createSlicepara lidar com mudanças de estado.

Introdução Demonstração do aplicativo

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:

  • Familiaridade com conceitos de React, como hooks e state
  • Familiaridade com o Redux Toolkit, particularmente as APIs createSlice e createAsyncThunk
  • Conhecimento de sintaxe ES6 e promessas de JavaScript.

Agora, vamos começar a autenticar!

Clonar arquivos iniciais do GitHub

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, /registere /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 handleSubmitfunçã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-filesbranch 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

Configurando a loja Redux

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 reducerpropriedade da userSliceloja 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

Captura de tela da extensão Redux DevTools

Organizando fatias e ações do Redux

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.

Arquitetura de back-end

Nosso servidor Express, hospedado em localhost:5000, atualmente possui três rotas:

  • api/user/login: a rota de login. Ele aceita POSTsolicitaçõ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 horas
  • api/user/register: a rota de registro. Ele aceita POSTsolicitações e exige o nome, e-mail e senha do usuário
  • api/user/profile: uma rota de autorização. Ele aceita GETsolicitaçõ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.

Autenticação do Redux: ação de registro

Em userAction.js, você usará createAsyncThunkpara executar lógica assíncrona atrasada antes de enviar os resultados processados ​​para os redutores.

createAsyncThunkaceita três parâmetros: um tipo de ação de string, uma função de retorno de chamada e um optionsobjeto 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 dispatchmétodo quando a ação é chamada. Se você precisar passar vários valores, poderá passar um objeto
  • thunkAPI: um objeto contendo parâmetros normalmente passados ​​para uma função de conversão do Redux. Os parâmetros incluem getState, dispatch, rejectWithValuee 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",
}

Manipulando funções assíncronas emextraReducers

As ações criadas com createAsyncThunkgeram três tipos de ação de ciclo de vida possíveis: pending, fulfillede rejected.

Você pode utilizar esses tipos de ação na extraReducerspropriedade de userSlicepara 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 successvalor para truequando a ação for cumprida para significar um registro bem-sucedido.

Reagir Redux useDispatche useSelectorganchos

Ao usar useSelectore useDispatchdo react-reduxpacote 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 passworde correspondem. confirmPasswordSe o fizerem, a registerUseração é despachada, tomando os dados do formulário como seu argumento.

O useSelectorgancho é usado para extrair os valores de estado loadinge 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 successvalor ao lado do gancho e do gancho userSlicedo 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:

  • Criar ação Redux no arquivo de ação
  • Escreva a lógica do redutor para lidar com as alterações de estado para a ação acima
  • Ação de despacho em um componente
  • Faça as atualizações/efeitos colaterais necessários da interface do usuário que ocorrem como resultado da ação despachada

Não precisa estar nessa ordem, mas geralmente são etapas recorrentes. Vamos repetir isso com a ação de login.

Autenticação do Redux: 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

Inicialize no início

Como o valor de userTokendepende 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. userInfoO valor de pode ser usado para redirecionar um usuário autenticado para a página de login com useNavigatee 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.

getUserProfileação

Para 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-schemerepresenta qual esquema de autenticação você deseja usar. Os JWTs são projetados para suportar o Beareresquema 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 createAsyncThunkfunção de retorno de chamada do , thunkAPI, fornece um getStatemé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 Headercomponente é 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 userTokenvalor de . Isto pode ser conseguido com o useEffectgancho.

// 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 userInforenderizar 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 ProtectedRoutecomponente, e vamos criar isso a seguir.

Rotas protegidas com React Router

Crie uma pasta chamada routinge srcum arquivo chamado ProtectedRoute.js. ProtectedRoutedestina-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 userInfoo valor de 's para detectar se um usuário está logado. Se userInfoestiver ausente, um modelo não autorizado é retornado. Caso contrário, usamos o Outletcomponente 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 ProfileScreencom 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.

Autenticação do Redux: ação de logout

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 userSlicecom a reducerpropriedade.

// 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 Headercomponente:

// 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

Demonstração de saída do Chuck

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.

Conclusão

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/

#redux #authentic #react 

Manipulando A Autenticação Do Usuário Com O Redux Toolkit

Manejo De La Autenticación De Usuario Con Redux Toolkit

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 createSlicepara manejar los cambios de estado.

Introducción Demostración de la aplicación

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:

  • Familiaridad con los conceptos de React, como ganchos y estado.
  • Familiaridad con Redux Toolkit, particularmente las API createSlice y createAsyncThunk
  • Conocimiento de la sintaxis de ES6 y JavaScript Promises.

Ahora, ¡comencemos a autenticar!

Clonación de archivos de inicio de GitHub

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, /registery /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 handleSubmitfunció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-filesrama 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

Configurando la tienda Redux

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 reducerpropiedad desde userSlicela 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

Captura de pantalla de la extensión Redux DevTools

Organizar rebanadas y acciones de Redux

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.

Arquitectura de fondo

Nuestro servidor Express, alojado en localhost:5000, actualmente tiene tres rutas:

  • api/user/login: la ruta de inicio de sesión. Acepta POSTsolicitudes 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 POSTsolicitudes y requiere el nombre, correo electrónico y contraseña del usuario.
  • api/user/profile: una ruta de autorización. Acepta GETsolicitudes 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.

Autenticación Redux: acción de registro

En userAction.js, utilizará createAsyncThunkpara realizar una lógica asincrónica retrasada antes de enviar los resultados procesados ​​a los reductores.

createAsyncThunkacepta tres parámetros: un tipo de acción de cadena, una función de devolución de llamada y un optionsobjeto 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 dispatchmétodo cuando se llama a la acción. Si necesita pasar varios valores, puede pasar un objeto
  • thunkAPI: un objeto que contiene parámetros que normalmente se pasan a una función thunk de Redux. Los parámetros incluyen getState, dispatch, rejectWithValuey 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",
}

Manejo de funciones asíncronas enextraReducers

Las acciones creadas con createAsyncThunkgeneran tres posibles tipos de acciones de ciclo de vida: pending, fulfilledy rejected.

Puede utilizar estos tipos de acción en la extraReducerspropiedad de userSlicepara 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 successvalor truecuando se cumple la acción para indicar un registro exitoso.

React Redux useDispatchy useSelectorganchos

Al usar useSelectory useDispatchdesde el react-reduxpaquete 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 passwordy coinciden. confirmPasswordSi lo hacen, la registerUseracción se despacha, tomando los datos del formulario como argumento.

El useSelectorgancho se usa para extraer los valores de estado loadingy 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 successvalor userSlicejunto al gancho del enrutador React useNavigatey el useEffectgancho, 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í:

  • Crear acción Redux en el archivo de acción
  • Escriba la lógica del reductor para manejar los cambios de estado para la acción anterior
  • Despachar acción dentro de un componente
  • Realice las actualizaciones necesarias de la interfaz de usuario o los efectos secundarios que se produzcan como resultado de la acción enviada

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.

Autenticación Redux: 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

Inicializar al principio

Debido a que el valor de userTokendepende 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. userInfoEl valor de se puede utilizar para redirigir a un usuario autenticado a la página de inicio de sesión con useNavigatey 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.

getUserProfileacción

Para 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-schemerepresenta qué esquema de autenticación desea utilizar. Los JWT están diseñados para admitir el Beareresquema 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 createAsyncThunkla función de devolución de llamada thunkAPI, proporciona un getStatemé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 Headercomponente 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 userTokenel valor de . Esto se puede lograr con el useEffectgancho.

// 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 userInfomensajes 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 ProtectedRoutecomponente, y lo vamos a crear a continuación.

Rutas protegidas con React Router

Cree una carpeta llamada routingy srcun archivo llamado ProtectedRoute.js. ProtectedRouteestá 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 userInfoel valor de para detectar si un usuario ha iniciado sesión. Si userInfoestá ausente, se devuelve una plantilla no autorizada. De lo contrario, usamos el Outletcomponente 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 ProfileScreencon 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.

Autenticación de Redux: acción de cierre de sesión

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 userSlicecon la reducerpropiedad.

// 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 Headercomponente:

// 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

Demostración de cierre de sesión de Chuck

¡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.

Conclusión

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/

 #redux #authentic #react 

Manejo De La Autenticación De Usuario Con Redux Toolkit
Hoang  Ha

Hoang Ha

1657699802

Xử Lý Xác Thực Người Dùng Với Bộ Công Cụ Redux

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.

Giới thiệu ứng dụng Demo

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:

  • Quen thuộc với các khái niệm React, chẳng hạn như hook và state
  • Quen thuộc với Bộ công cụ Redux, đặc biệt là API createSlicecreateAsyncThunk
  • Kiến thức về cú pháp ES6 và JavaScript Promises.

Bây giờ, chúng ta hãy bắt đầu xác thực!

Sao chép các tệp bắt đầu từ GitHub

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ý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 handleSubmithà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-filesnhá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

Định cấu hình cửa hàng Redux

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 reducertính từ userSlicevà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

Ảnh chụp màn hình từ Tiện ích mở rộng Redux DevTools

Tổ chức các lát và hành động của Redux

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ó.

Kiến trúc phụ trợ

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 POSTcá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 POSTcác yêu cầu và yêu cầu tên, email và mật khẩu của người dùng
  • api/user/profile: một tuyến đường ủy quyền. Nó chấp nhận GETcá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ý.

Xác thực Redux: Đăng ký hành động

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.

createAsyncThunkchấ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 dispatchphươ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ượng
  • thunkAPI: 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 , getStatevà 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",
}

Xử lý các chức năng không đồng bộ trongextraReducers

Các hành động được tạo bằng createAsyncThunkcách tạo ra ba loại hành động vòng đời có thể có pending:, fulfilledrejected.

Bạn có thể sử dụng các loại hành động này trong thuộc extraReducerstí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 successgiá trị thành truekhi hành động được hoàn thành để báo hiệu đăng ký thành công.

React Redux's useDispatchuseSelectorhooks

Bằng cách sử dụng useSelectoruseDispatchtừ react-reduxgó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 passwordvà có confirmPasswordkhớp nhau không. Nếu họ làm vậy, registerUserhành động sẽ được gửi đi, lấy dữ liệu biểu mẫu làm đối số của nó.

useSelectorHook được sử dụng để lấy ra các loadinggiá trị và trạng errorthá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 successgiá trị từ cùng với hook và hook userSlicecủ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:

  • Tạo hành động Redux trong tệp hành động
  • Viết logic giảm thiểu để xử lý các thay đổi trạng thái cho hành động trên
  • Cử hành động trong một thành phần
  • Thực hiện các bản cập nhật giao diện người dùng cần thiết / các tác dụng phụ xảy ra do hành động đã thực hiện

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.

Xác thực Redux: 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

Khởi tạo ngay từ đầu

Bởi vì giá trị của userTokenphụ 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. userInfoGiá 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 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

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.

getUserProfilehoạ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 đề HTTPAuthorizationAuthorization: <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ợ Bearerlượ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 createAsyncThunkhàm gọi lại của thunkAPI, cung cấp một getStatephươ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 Headerphầ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 userTokengiá trị của. Điều này có thể đạt được với useEffecthook.

// 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 ProtectedRoutethành phần duy nhất và chúng ta sẽ tạo nó tiếp theo.

Các tuyến đường được bảo vệ với Bộ định tuyến React

Tạo một thư mục được gọi routingtrong srcvà 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 userInfogiá trị của để phát hiện xem người dùng đã đăng nhập hay chưa. Nếu userInfovắ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 ProfileScreenvớ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.

Xác thực Redux: Hành động đăng xuất

Để đă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 userSlicebằng thuộc reducertí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 Headerthà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

Chuck Đăng xuất Demo

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.

Sự kết luận

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/

#redux #authentic #react 

Xử Lý Xác Thực Người Dùng Với Bộ Công Cụ Redux
坂本  篤司

坂本 篤司

1657699589

ReduxToolkitによるユーザー認証の処理

ユーザー認証は、無数の方法で処理できます。この機能がいかに重要であるかにより、プロセスを容易にする認証ソリューションを提供する企業が増えています。たとえば、Firebase、Auth0、NextAuth.jsなどです。

このようなサービスが認証と承認を処理する方法に関係なく、実装プロセスでは通常、一部のAPIエンドポイントを呼び出し、フロントエンドインフラストラクチャで使用されるプライベートトークン(通常はJSON Web Token、またはJWT)を受信します。

この記事では、Redux Toolkit(RTK)を使用してReactでフロントエンド認証ワークフローを作成する方法を学習します。createAsyncThunkExpressバックエンドへの非同期リクエストを行うために、などの重要なToolkitAPIを利用します。createSlice状態の変化を処理するためにも使用します。

はじめにアプリのデモ

このプロジェクトのバックエンドは、MongoDBデータベースでExpressを使用して構築されていますが、フロントエンドワークフローは、トークンを提供する使用するすべての認証サービスに引き続き適用する必要があります。プロジェクトのリポジトリにあるソースファイルをダウンロードして、データベースを設定する方法を説明し、アプリケーションをローカルで実行することができます。ここでライブデモをご覧ください

フォローするには、次のものが必要です。

それでは、認証を始めましょう!

GitHubからのスターターファイルのクローン作成

リポジトリには、このアプリケーションをブートストラップするために必要なファイルを含む[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ストアの設定

Redux Toolkitは、ストアを作成する新しい方法を導入します。ストアの一部をスライスと呼ばれるさまざまなファイルに分割します。

スライスは、Redux状態の単一ユニットを表します。これは、アプリの単一機能のレデューサーロジックとアクションのコレクションであり、通常は1つのファイルにまとめて定義されます。私たちにとって、このファイルはfeatures/userです。

RTKのcreateSliceAPIを使用すると、次のように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

ReduxDevTools拡張機能のスクリーンショット

Reduxスライスとアクションの整理

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アクションの記述に進むことができます。

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

で作成されたアクションは、createAsyncThunk3つの可能なライフサイクルアクションタイプを生成します: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

ReduxuseDispatchuseSelectorフックに反応する

以前にインストールしたパッケージを使用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

フォームが送信されると、フィールドpasswordconfirmPasswordフィールドが一致するかどうかを確認することから始めます。その場合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ロジックを書いているときに気付くおなじみのパターンがあります。通常は次のようになります。

  • アクションファイルにReduxアクションを作成します
  • 上記のアクションの状態変化を処理するためのレデューサーロジックを記述します
  • コンポーネント内のディスパッチアクション
  • ディスパッチされたアクションの結果として発生する必要なUIの更新/副作用を作成します

この順序である必要はありませんが、通常は繰り返し発生する手順です。ログインアクションでこれを繰り返しましょう。

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使用する認証スキームを表します。BearerJWTは認証スキームをサポートするように設計されており、それを使用します。詳細については、 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コンポーネントに抽出できます。次にそれを作成します。

Reactルーターで保護されたルート

という名前のフォルダと。という名前のファイル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認証:ログアウトアクション

ユーザーをログアウトするには、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/

   #redux #authentic #react 

ReduxToolkitによるユーザー認証の処理

Handling User Authentication with Redux Toolkit

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/

#redux #authentic 

Handling User Authentication with Redux Toolkit
Hoang  Kim

Hoang Kim

1656250800

Cách Tạo Đăng Nhập Và Đăng Ký Bằng Auth Trong Laravel 6

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

#laravel #authentic 

Cách Tạo Đăng Nhập Và Đăng Ký Bằng Auth Trong Laravel 6
Iara  Simões

Iara Simões

1656247080

Como Criar Login E Registro Usando Auth Em Laravel 6

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

#laravel #authentic 

Como Criar Login E Registro Usando Auth Em Laravel 6
Anne  de Morel

Anne de Morel

1656246900

Créer Une Connexion Et Un Enregistrement En Utilisant Auth Laravel 6

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

#laravel #authentic 

Créer Une Connexion Et Un Enregistrement En Utilisant Auth Laravel 6

laravel6でauthを使用してログインと登録を作成する方法

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

#laravel #authentic 

laravel6でauthを使用してログインと登録を作成する方法
许 志强

许 志强

1656242940

如何在 laravel 6 中使用 auth 创建登录和注册

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

#laravel #authentic 

如何在 laravel 6 中使用 auth 创建登录和注册
Saul  Alaniz

Saul Alaniz

1656242820

Cómo Crear inicio De Sesión Y Registro Usando Auth En Laravel 6

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

#laravel #authentic 

Cómo Crear inicio De Sesión Y Registro Usando Auth En Laravel 6

PnPPowerShellを使用したSharePointOnlineでのアプリのみの認証

Azure AD Appのみの認証は、M365サービスへの認証と、データの読み取り、データのアップロード、自動化スクリプトを介したバックエンドジョブの実行などの操作に使用されています。Microsoftは、Azure ADに登録されているアプリケーションに証明書ベースの認証を使用して、M365または任意のクラウドサービスに対して認証することをお勧めします。CBAは、ユーザーのIDを検証するための非常に堅牢で安全なメカニズムです。 

この記事では、私が最近遭遇したユースケースについて説明したいと思います。以前は、SharePoint App Only認証を使用しています。これはACS(Azure Controlサービス)の概念であり、サイトコレクションの管理者はサイトコレクションに/_layouts/appregnew.aspxを追加することで、クライアントIDとクライアントシークレットを作成できます。アプリケーションで。ただし、このACSアプリのみのアクセストークン方式を使用する場合の問題はほとんどありません。

  • 認証は安全ではありません。MSFTは、AzureADアプリのみの認証に切り替えることをお勧めします。
  • アプリケーションが複数のサイトコレクションと通信するようにする場合は、複数のクライアントIDとシークレットを作成する必要があり、処理が面倒になります。

ACSトークンベースの認証の詳細については、参照セクションを参照してください。

幸いなことに、Azure ADアプリでは、SharePointのAPIアクセス許可に「Sites.Selected」という新しいアクセス許可が追加されました。これにより、AzureADアプリは単一のクライアントと証明書の詳細を使用して複数のサイトコレクションに対して認証できます。 

証明書を使用したこのAzureADアプリのみの認証に進む前に、Azure ADの証明書ベースの認証(別名CBA)とは何かを理解しようとします。AzureADには2種類のCBAがあります。 

  1. フェデレーションADFSを使用した証明書ベースの認証
  2. AzureAD証明書ベースの認証

フェデレーションADFSを使用した証明書ベースの認証

以前は、CBAを実装するために、ADFSサービスをユーザーとAzureADの間にデプロイする必要がありました。ADFSを使用するCBAは、X.509証明書を使用してAzureADに対して認証します。

  • ここで、ユーザーは自分の資格情報とデバイスにインストールされた証明書を使用してアプリケーションに署名します。
  • ADFSはユーザーの資格情報と証明書を検証し、成功するとアクセストークンをユーザーに渡してアプリケーションにアクセスします。

AzureAD証明書ベースの認証

Azure AD CBAである最新バージョンでは、ADFSの構成と展開は必要ありません。ユーザーはAzureADと直接対話し、アプリケーションに対して認証できます。

ADFSおよびAzureADCBAを使用したCBAの詳細については、参照セクションに記載されている記事を参照してください。

前提条件

  • PnP.Powershellバージョン1.10.0。このバージョンでは、CBAを使用した認証が更新されていることに注意してください。

  • PowerShellバージョン5.1以降
  • PowerShellコマンドの実行に使用されるアカウントには、「グローバル管理者」権限が必要です。

AzureADアプリを作成する

次に、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

SharePointOnlineサイトに接続するための証明書ベースの認証

SharePointOnlineサイトに接続するための証明書ベースの認証

ステップ3

認証が成功すると、必要なアーティファクトをチェックして同意フローを開始するために60秒待つことを示す以下のメッセージが表示されます。

SharePointOnlineサイトに接続するための証明書ベースの認証

ステップ4

アプリを登録してから、証明書と指紋を作成するために、もう一度認証するように求められます。プロンプトに従います

SharePointOnlineサイトに接続するための証明書ベースの認証

ステップ5

これで、以下のように認証が成功すると同意がポップアップ表示されます。アプリ名(この場合はSPSites Selected)と、承認およびキャンセルするオプションが表示されます。

SharePointOnlineサイトに接続するための証明書ベースの認証

[アプリ情報]をクリックして、アプリの詳細を確認することもできます。

ステップ6

[同意する]をクリックして同意することに同意すると、コマンド出力ウィンドウから次の情報が表示されます。

SharePointOnlineサイトに接続するための証明書ベースの認証

次の値があります、

  • Pfxファイル:証明書に関連付けられた公開鍵と秘密鍵の両方の情報が含まれています。これは組織外で共有しないでください。
  • Cerファイル:公開鍵とデバイス(この場合はサーバー)に関する情報が含まれています。これは通常、パートナーと交換されます。
  • 指紋:アプリケーションへの認証に使用される証明書に関連付けられた安全なキー。
  • Base64Encoded:これはASCII文字列形式の証明書情報です。

クライアントID、指紋、およびPfxファイルとCerファイルの場所のみをメモする必要があります。

上記の手順は、AzureADアプリケーションが「Sites.Selected」である必要なアクセス許可で作成されていることを確認します。これは、特定のサイトに対してのみ認証するようにAzureADアプリを構成できるようになったことを意味します。

AzureADアプリへのアクセスを許可する

ここで、Azure ADアプリへのアクセスを許可するには、次の一連のコマンドを実行します。

ステップ1

グローバル管理者の資格情報を持つPnPPowerShellモジュールを使用して、テナントのSharePoint管理者URLにログインします。

Connect-PnPOnline -Url "https://contoso-admin.sharepoint.com" -Interactive

SharePointOnlineサイトに接続するための証明書ベースの認証

ステップ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

SharePointOnlineサイトに接続するための証明書ベースの認証

検証

ステップ1

権限が付与されているサイトに接続して、アプリへのアクセスを検証します。問題なくコンテンツが表示されるはずです。この場合、以前の接続が存在する場合は、以前のPnP接続から切断します。

Disconnect-PnPOnline

ステップ2

以下のコマンドを入力して、他にPnP接続が存在しないことを確認します。

Get-PnPConnection

「現在の接続にはSharePointコンテキストがありません」というエラーが表示されるはずです。

SharePointOnlineサイトに接続するための証明書ベースの認証

ステップ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から詳細を取得することもできます。

SharePointOnlineサイトに接続するための証明書ベースの認証

同様に、テナントドメインは、クイック起動から[Azure Active Directory]をクリックして、[プライマリドメイン]の値を探すことで取得できます。

SharePointOnlineサイトに接続するための証明書ベースの認証

ステップ4

次に、以下のコマンドを実行して、アプリが接続されているサイトを確認します。

Get-PnPSite

ステップ5

次に、以下のコマンドを実行して、このサイトコレクション内のすべてのリストのリストを取得します。

Get-PnPList

SharePointOnlineサイトに接続するための証明書ベースの認証

AzureADアプリがアクセスする必要のある他のサイトコレクションに対しても同じコマンドを実行できます。

ステップ6

アクセスが許可されていないサイトに接続して、アプリへのアクセスを検証します。403forbiddenエラーが表示されるはずです。

Connect-PnPOnline -Url "https://contosodev.sharepoint.com/sites/M365POC" -ClientId "YOUR CLIENT ID" -Thumbprint "CERT THUMP PRINT" -Tenant "contosodev.onmicrosoft.com"

SharePointOnlineサイトに接続するための証明書ベースの認証

クライアント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

結論

したがって、この記事では、

  • AzureAD証明書ベースの認証とは何ですか
  • さまざまな種類の認証
  • PnPモジュールを使用して、「Sites.Selected」APIアクセス許可を持つAzureADアプリを生成します。
  • Azure ADアプリへのアクセスを許可してから、アクセスを検証します。

参考文献

 このストーリーは、もともとhttps://www.c-sharpcorner.com/article/certificate-based-authentication-to-connect-to-sharepoint-online-sites/で公開されました。

#authentic #sharepoint #azure 

PnPPowerShellを使用したSharePointOnlineでのアプリのみの認証
Saul  Alaniz

Saul Alaniz

1655722800

Autenticación Solo De Aplicaciones En SharePoint online Mediante PnP

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.

  • La autenticación no es segura. MSFT recomienda cambiar a la autenticación de solo aplicación de Azure AD.
  • Si desea que su aplicación se comunique con múltiples colecciones de sitios, es necesario crear múltiples ID de cliente y secretos, lo que se vuelve engorroso de manejar.

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. 

  1. Autenticación basada en certificados con AD FS federado
  2. Autenticación basada en certificados de Azure AD

Autenticación basada en certificados con AD FS federado

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.

  • Aquí el usuario inicia sesión en la aplicación con sus credenciales y también con el certificado instalado en sus dispositivos.
  • ADFS valida las credenciales y el certificado del usuario y, en caso de éxito, pasa tokens de acceso al usuario para acceder a las aplicaciones.

Autenticación basada en certificados de 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.

requisitos previos

  • PnP.Powershell versión 1.10.0. Tenga en cuenta que la autenticación mediante CBA se actualiza en esta versión.

  • PowerShell versión 5.1 o posterior
  • La cuenta utilizada para ejecutar los comandos de PowerShell debe tener derechos de "Administrador global".

Crear una aplicación de Azure AD

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

Autenticación basada en certificados para conectarse a sitios de SharePoint Online

Autenticación basada en certificados para conectarse a sitios de SharePoint Online

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.

Autenticación basada en certificados para conectarse a sitios de SharePoint Online

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

Autenticación basada en certificados para conectarse a sitios de SharePoint Online

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.

Autenticación basada en certificados para conectarse a sitios de SharePoint Online

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.

Autenticación basada en certificados para conectarse a sitios de SharePoint Online

Tendrás los siguientes valores,

  • Archivo Pfx: incluye información de clave tanto pública como privada asociada al certificado. Esto no debe compartirse fuera de su organización.
  • Archivo cer: tiene clave pública y alguna información sobre el dispositivo (en este caso el servidor). Esto normalmente se intercambia con los socios.
  • Huella digital: una clave segura asociada con el certificado que se utiliza para autenticarse en la aplicación.
  • Base64Encoded: esta es la información del certificado en formato de cadena ASCII.

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.

Concesión de acceso a la aplicación Azure AD

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

Autenticación basada en certificados para conectarse a sitios de SharePoint Online

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

Autenticación basada en certificados para conectarse a sitios de SharePoint Online

Validación

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".

Autenticación basada en certificados para conectarse a sitios de SharePoint Online

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'.

Autenticación basada en certificados para conectarse a sitios de SharePoint Online

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'.

Autenticación basada en certificados para conectarse a sitios de SharePoint Online

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

Autenticación basada en certificados para conectarse a sitios de SharePoint Online

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"

Autenticación basada en certificados para conectarse a sitios de SharePoint Online

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

Conclusión

Por lo tanto, en este artículo, hemos aprendido sobre

  • ¿Qué es la autenticación basada en certificados de Azure AD y
  • los diferentes tipos de autenticación
  • utilizando el módulo PnP para generar la aplicación Azure AD con los permisos de API 'Sitios.Seleccionados'.
  • Otorgar acceso a la aplicación Azure AD y luego validar el acceso.

Referencias

 Esta historia se publicó originalmente en https://www.c-sharpcorner.com/article/certificate-based-authentication-to-connect-to-sharepoint-online-sites/

#authentic #sharepoint #azure 

Autenticación Solo De Aplicaciones En SharePoint online Mediante PnP