token-登录退出鉴权401无痛刷新

554 阅读1分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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是为了方便处理登录时的异常

image.png

说明: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控制访问权限的功能

思路梳理:

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

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

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

具体请跳转这里这是一个审核未通过的链接QAQ

401处理

场景:

401是http的状态码,表示本次请求没有权限;

有如下两种情况会出现401错误:

  1. 未登陆用户做一些需要权限才能做的操作,network中会提示本次请求是401错误
  2. 登录用户的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思路梳理

image.png

效果:让用户对因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)
    }
  }
)