持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情
token类型设置
src\types\data.d.ts
//token类型
export type Token={
token:string,
refresh_token:string
}
//表单传递信息类型
export type LoginForm={
mobile:string
code:string
}
//异步action接收数据类型
export interface ApiResponse<T>{
message:string,
data:T
}
token持久化存储工具
src\utils\storage.ts
import { Token } from "@/types/data";
const TOKEN_KEY='app'
export function getToken():Token{
if (localStorage.getItem(TOKEN_KEY)) {
return JSON.parse(localStorage.getItem(TOKEN_KEY) || '{}')
} else {
return {
token: '',
refresh_token: ''
}
}
}
export function setToken(data:Token):void{
localStorage.setItem(TOKEN_KEY,JSON.stringify(data))
}
export function removeToken():void{
localStorage.removeItem(TOKEN_KEY)
}
export function hasToken():boolean{
return !!getToken().token
}
请求中携带token
import { getToken } from './storage'
instance.interceptors.request.use(
function(config){
const token=getToken().token
if(token){
config.headers!.Authorization = 'Bearer ' + token
}
return config
},
function (error){
return Promise.reject(error)
}
)
token相关请求类型
src\types\store.d.ts
import {Token} from './data'
export type LoginAction = {
type: 'login/token'
payload: Token
}|{ type: 'login/logout'}
type RootAction = LoginAction | ProfileAction
export type RootThunkAction = ThunkAction<
void,
RootState,
unknown,
RootAction
>
注:RootThunkAction的使用及其参数
-
使用 thunk 中间件后的 Redux dispatch 类型 -
ReturnType:thunk action 的返回类型,项目中几乎都是返回 Promise -
State: Redux 的状态 RootState -
ExtraThunkArg: 额外的参数,没有用到,可以指定为 unknown -
BasicAction: 非 thunk action,即对象形式的 action
token相关异步action
src\store\actions\login.ts
import { LoginAction, RootThunkAction } from "@/types/store";
import { ApiResponse, LoginForm, Token } from "@/types/data";
import request from "@/utils/request";
import { removeToken, setToken } from "@/utils/storage";
export const login=(values:LoginForm):RootThunkAction=>{
return async()=>{
const res=await request.post<ApiResponse<Token>>('/authorizations',values)
// console.log(res);
setToken(res.data.data)
return({
type:'login/token',
payload:res.data.data
})
}
}
export const getCode=(mobile:string):RootThunkAction=>{
return async()=>{
await request.get(`/sms/codes/${mobile}`)
}
}
export function logout():LoginAction{
removeToken()
return {
type:'login/logout'
}
}
token相关reducer
import { Token } from '@/types/data'
import { LoginAction } from '@/types/store'
import { getToken } from '@/utils/storage'
const initState:Token=getToken()||{
token:'',
refresh_token:''
}
const login=(state=initState,action:LoginAction):Token=>{
if(action.type==='login/token'){
return action.payload
}else if(action.type==='login/logout'){
return {
token:'',
refresh_token:''
}
} else {
return state
}
}
export default login
登录功能
设置异步登录action类型
export type LoginAction = {
type: 'login/token'
payload: Token
}
设置登录请求
export const login=(values:LoginForm):RootThunkAction=>{
return async()=>{
const res=await request.post<ApiResponse<Token>>('/authorizations',values)
// console.log(res);
setToken(res.data.data)
return({
type:'login/token',
payload:res.data.data
})
}
}
reducer相应设置
...
const login=(state=initState,action:LoginAction):Token=>{
if(action.type==='login/token'){
return action.payload
} else {
return state
}
}
发送请求
背景说明:登录表单中点击登录按钮触发表单的onFinish事件
const onFinish = async (values: LoginForm) => {
try{
await dispatch(login(values));
Toast.show({
content:'登录成功',
duration:500,
afterClose:()=>{
history.replace('/')//不可回退,可使用push回退但是不推荐
}
})
}catch(e){
// console.log(e);
const error = e as AxiosError<{ message: string }>
Toast.show({
content: error.response?.data.message,
duration: 1000,
})
}
}
注:try-catch是为了方便处理登录时的异常
说明:catch捕获事件e,设置error为AxiosError类型的e中的message错误提示信息并使用Toast.show显示异常信息
请求头携带token
import axios from 'axios'
import { getToken } from './storage'
const instance =axios.create({
baseURL:'***',
timeout:5000
})
instance.interceptors.request.use(
function(config){
const token=getToken().token
if(token){
config.headers!.Authorization = 'Bearer ' + token
}
return config
},
function (error){
return Promise.reject(error)
}
)
export default instance
退出功能
设置退出异步action类型
export type LoginAction = {
。。。
|{ type: 'login/logout'}
退出异步action
export function logout():LoginAction{
removeToken()
return {
type:'login/logout'
}
}
reducers处理
src\store\reducers\login.ts
const login=(state=initState,action:LoginAction)=>{
。。。
else if(action.type==='login/logout'){
return { }
}
。。。
}
使用
可使用Dialog.onConfirm直接设置确定事件,也可使用 Dialog.show自定义设置
src\pages\Profile\Edit\index.tsx
const onLogout = () => {
const handler = Dialog.show({
title: '温馨提示',
content: '亲,你确定退出吗?',
closeOnAction: true,
actions: [
[
{
key: 'cancel',
text: '取消',
onClick: () => {
handler.close()
}
},
{
key: 'confirm',
text: '退出',
style: {
color: 'var(--adm-color-weak)'//此为自定义全局CSS变量
},
onClick: () => {
dispatch(logout())
history.replace('/login')
Toast.show({content:'退出成功'})
}
]
]
})
}
return (
// ...
<Button onClick={onLogout}>
退出登录
</Button>
)
封装鉴权路由组件
实现根据token控制访问权限的功能
思路梳理:
-
定义私有路由组件。在 components 目录中创建 PrivateRoute路由组件:实现路由的登录访问控制逻辑
- 有token,正常访问
- 没有token,重定向到登录页面,并传递要访问的路由地址
-
使用路由组件
- 将需要权限才能访问的页面,使用私有路由组件
具体请跳转这里这是一个审核未通过的链接QAQ
401处理
场景:
401是http的状态码,表示本次请求没有权限;
有如下两种情况会出现401错误:
- 未登陆用户做一些需要权限才能做的操作,network中会提示本次请求是401错误
- 登录用户的token过期了
token过期:
登陆成功之后,接口会返回一个token值,这个值在后续请求时通过请求头时带上(就像是进入小区门的开门钥匙)。但是,这个值一般会有有效期(具体是多长,是由后端决定)
如果你上午8点登陆成功,到了10:01分,则token就会失效,再去发请求时,就会报401错误。
refresh_token和token的作用
-
token:
- 作用:一般情况下,在访问接口时,需要传入的token值就是它。
- 有效期:如2小时。
-
refresh_token
- 作用: 当token的有效期过了之后,可以使用refresh_token去请求一个特殊接口(这个接口也是后端指定的,明确需要传入refresh_token),并返回一个新的token回来(有效期还是2小时),以替换过期的那个token。
- 有效期:14天。(最理想的情况下,一次登陆可以持续14天。)
响应拦截器
axios中提供了响应拦截器功能:所有从后端回来的响应都会先进入响应拦截器,包括出错的请求中。所以,我们可以在响应拦截器中去写代码来统一解决。
src\utils\request.ts
// 添加响应拦截器
instance.interceptors.response.use(function (response) {
return response
}, async function (error) {
// 如果发生了错误,判断是否是401
console.dir(error)
// 开始处理
return Promise.reject(error)
})
401思路梳理
效果:让用户对因token过期产生的401错误无感知
路线:
request的响应拦截器中:
-
对于某次请求A,如果是401错误 (2)
-
有refresh_token,用refresh_token去请求回新的token (3)
-
新token请求成功 (4)
- 更新本地token (5)
- 再发一次请求A (6)
-
新token请求失败
- 携带请求地址,跳转到登陆页
-
-
没有refresh_token,说明没有登陆
- 携带请求地址,跳转到登陆页
-
封装独立的history
目的:在非组件中实现路由跳转功能
创建
src\utils\history.ts
import { createBrowserHistory } from 'history'
const history = createBrowserHistory()
export default history
在app.tsx中使用
import history from '@/utils/history'
function App () {
return (
<div className="app">
<Router history={history}>
使用异步action用于更新token
actions/login.ts
export function saveToken (token: Token) : LoginAction {
// 1. 本地持久化
setToken(token)
// 2. 保存token
return {
type: 'login/token',
payload: token
}
}
核心代码:
src\utils\request.ts
// 添加响应拦截器
instance.interceptors.response.use(
function (response) {
// 对响应数据做点什么
return response
},
async function (error: AxiosError<{message: string}>) {//获取axios的错误提示信息
// 对请求错误做些什么
if (!error.response) {//没有错误响应数据
Toast.show('网络异常')
return Promise.reject(error)
}
if (error.response.status !== 401) {//非401错误简单处理
Toast.show('后端错误')
return Promise.reject(error)
}
const { refresh_token } = getToken()//拿到refresh_token
if (!refresh_token) {//沒有refresh_token時需要退到登录并记录
// alert('refresh_token')
history.replace('/login', { from: history.location.pathname })
Toast.show('登录信息过期')
return Promise.reject(error)
}
try {//有refresh_token时
// 使用refresh_token去换取新token
// 使用 axios 来发送请求
const res = await axios.put(`${baseURL}authorizations`, null, {//该请求需要传请求地址,baseURL为不变请求地址,请求格式及携带refresh_token的请求头
headers: {
Authorization: `Bearer ${refresh_token}`
}
})
// 将新获得的token及refresh_token持久化存储
store.dispatch(saveToken({ token: res.data.data.token, refresh_token }))
return instance(error.config)
} catch (e) {//有refresh_token但是发请求获取token失败跳到登录页并记录
// setToken(tokens)
store.dispatch(logout())
history.replace('/login', { from: history.location.pathname })
return Promise.reject(e)
}
}
)