前言
在PC端中经常遇见的需求:
- 没有登录也能访问的页面,比如登录页,404页面
- 只有登录,有了令牌才能访问的页面,比如个人信息(只有登录了.你才能访问的页面)
因此,我们需要在项目中使用鉴权进行登录访问控制,获得令牌之后才能访问,如果没有登录就访问,让页面跳转到登录页进行登录
在vue当中使用路由守卫就可以实现,但是在React中没有导航守卫,这就需要我们自己封装了
那么我们如何实现?
首先分析一下,不管是哪个页面他们都是使用路由进行访问的,所以我们可以从路由下手,封装组件判断是否登录
登录就显示要访问的页面
没有登录就跳转到登录页面,让用户进行登录
在react-router-dom提供了鉴权示例
那么如何在React+TS封装自己的鉴权组件?
React+TS封装自己的鉴权组件
实现步骤:
-
定义私有路由组件。在 components 目录中创建 PrivateRoute路由组件:实现路由的登录访问控制逻辑
- 有token,正常访问
- 没有token,重定向到登录页面,并传递要访问的路由地址
-
使用路由组件
- 将需要权限才能访问的页面,使用私有路由组件
分析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)
}
)
复制代码