React+TS使用鉴权进行登录访问控制(移动端)

前言

在PC端中经常遇见的需求:

  1. 没有登录也能访问的页面,比如登录页,404页面
  2. 只有登录,有了令牌才能访问的页面,比如个人信息(只有登录了.你才能访问的页面)

因此,我们需要在项目中使用鉴权进行登录访问控制,获得令牌之后才能访问,如果没有登录就访问,让页面跳转到登录页进行登录

在vue当中使用路由守卫就可以实现,但是在React中没有导航守卫,这就需要我们自己封装了

那么我们如何实现?

首先分析一下,不管是哪个页面他们都是使用路由进行访问的,所以我们可以从路由下手,封装组件判断是否登录

登录就显示要访问的页面

没有登录就跳转到登录页面,让用户进行登录

react-router-dom提供了鉴权示例

image.png 那么如何在React+TS封装自己的鉴权组件?

React+TS封装自己的鉴权组件

实现步骤:

  1. 定义私有路由组件。在 components 目录中创建 PrivateRoute路由组件:实现路由的登录访问控制逻辑

    • 有token,正常访问
    • 没有token,重定向到登录页面,并传递要访问的路由地址
  2. 使用路由组件

    1. 将需要权限才能访问的页面,使用私有路由组件

分析PrivateRoute鉴权路由组件

  • 场景:限制某个页面只能在登录的情况下访问。

  • 说明:在 React 路由中并没有直接提供该组件,需要手动封装,来实现登录访问控制(类似于 Vue 路由的导航守卫)。

  • 如何封装?参考 react-router-dom 文档中提供的鉴权示例 。

  • 如何使用?使用PrivateRoute 组件代替默认的 Route 组件,来配置路由规则。

  • PrivateRoute 组件实际上就是对原来的 Route 组件做了一次包装,来实现了一些额外的功能。

  • <Route path component render> render 方法,指定该路由要渲染的组件内容(类似于 component 属性)。

  • Redirect 组件:重定向组件,通过 to 属性,指定要跳转到的路由信息。

  • state 属性:表示给路由附加一些额外信息,此处,用于指定登录成功后要进入的页面地址。

使用方式:

   <PrivateRoute path="/xxx/xxx">
            <ProfileEdit />  // 登录之后才能访问的页面
   </PrivateRoute>
复制代码

实现步骤:

处理Token

    // 用来对{token: string, refresh_token: string }做本地持久化

    import { Token } from '@/types/data'

    const TOKEN_KEY = 'geek-app'

    // 获取 token
    export function getToken (): Token {
      // 字符串转对象
      return JSON.parse(localStorage.getItem(TOKEN_KEY) || '{}')
    }

    // 设置 token
    export function setToken (data: Token): void {
      // 对象转字符串
      localStorage.setItem(TOKEN_KEY, JSON.stringify(data))
    }

    // 移除 token
    export function removeToken (): void {
      localStorage.removeItem(TOKEN_KEY)
    }

    // 判断是否登录(授权)
    export function hasToken (): boolean {
      return !!getToken().token
    }
复制代码

权限判断(封装PrivateRoute)

import { hasToken } from '@/utils/storage'
import { Route, Redirect, RouteProps } from 'react-router-dom'
// RouteProps 特有的类型
export const PrivateRoute = ({ children, ...rest }: RouteProps) => {
  return (
    <Route
      {...rest}
      render={props => {
        if (hasToken()) {
          return children
        }

        return (
          <Redirect
            to={{
              pathname: '/login',
              state: {
                from: props.location.pathname // 回跳地址
              }
            }}
          />
        )
      }}
    />
  )
}
复制代码

使用PrivateRoute

在App.tsx

      <PrivateRoute path="/profile/edit">
        <ProfileEdit />
      </PrivateRoute>
复制代码

登录成功处理

import { useHistory, useLocation } from 'react-router-dom'
import { useDispatch } from 'react-redux'
import { Button, NavBar, Form, Input, List, Toast } from 'antd-mobile'

const dispatch = useDispatch()
//  登录收集信息
  const onFinish = async (values:loginForm) => {
    // console.log(values)
    try {
      await dispatch(getToken(values))
      // 轻提示
      Toast.show({
        icon: 'success',
        content: '登录成功',
        // 在关闭之后调用
        afterClose: () => {
        // location.state?.from 回跳
          const path = location.state?.from || '/'
          history.replace(path)
        }
      })
    } catch (e) {
      const err = e as AxiosError<{message:string}>
      const content = err.response?.data.message || '登录失败'
      Toast.show({
        icon: 'fail',
        content
      })
    }
  }
复制代码

封装history

import { createBrowserHistory } from 'history'

const history = createBrowserHistory()

export default history
复制代码

App.tsx 修改

import history from '@/utils/history'

<Router history={history}><Router>
复制代码

修改响应拦截

对401状态的处理

  // 添加响应拦截器
instance.interceptors.response.use(
  function (response) {
    // 对响应数据做点什么
    return response
  },
  async function (error) {
    // 对响应错误做点什么
    const er = error as AxiosError
    if (!er.response) {
      Toast.show({ content: '网络异常' })
      return Promise.reject(error)
    }
    if (er.response?.status === 401) {
      console.log(401)
      const { refresh_token } = getToken()
      // 没有 refresh_token 的情况
      if (!refresh_token) {
        Toast.show({ content: '请重新登录' })
        // 跳到登录页
        history.push({
          pathname: '/login',
          state: { from: history.location.pathname }
        })
        return Promise.reject(error)
      }
      try {
        const res = await axios.put(baseURL + 'authorizations', null, {
          headers: {
            Authorization: `Bearer ${refresh_token}`
          }
        })
        console.log(res)
        const newToken = { token: res.data.data.token, refresh_token }
        setToken(newToken)
        store.dispatch({
          type: 'login/token',
          payload: newToken
        })
        return instance(er.config)
      } catch (error) {
        Toast.show({ content: '请重新登录' })
        // 跳到登录页
        history.push({
          pathname: '/login',
          state: { from: history.location.pathname }
        })
        return Promise.reject(error)
      }
    }

    return Promise.reject(error)
  }
)
复制代码

猜你喜欢

转载自juejin.im/post/7083481223384793096