React + Node.JS 巧妙实现后台管理系统の各种小技巧(前后端)

> 目前因学业任务比较重,没有好好的完善,现在比较完善的只有题库管理,新增题库,修改题库以及登录的功能,但搭配小程序使用,主体功能已经实现了

### 此后台系统是为了搭配我的另一个项目 `School-Partners学习伴侣`微信小程序而开发的。是一个采用`Taro`多端框架开发的跨平台的小程序。感兴趣的可以看一下之前的文章 

这篇文章主要是分享一下在开发这个东东的时候,遇到的一些问题,以及一些技术的巧妙的方法分享给大家,如果对大家有帮助的话,请给我点赞一下给个star鼓励一下~无比感谢嘿嘿

![](https://user-gold-cdn.xitu.io/2020/2/1/16ffe713d1a3b94a?w=70&h=57&f=gif&s=16110)

希望大佬们走过路过可以给个star鼓励一下~感激不尽~这个是小程序还有后台都整合在一起的仓库,`client`是小程序端的前端代码,`server`是小程序端和管理端的后台,`admin`是管理端的前端代码

> https://github.com/zhcxk1998/School-Partners

这个是小程序的介绍文章 

[这是配套的小程序介绍文章,使劲戳!](https://juejin.im/post/5dd161675188254efb3bceea)

无图无真相!先上几个图~

## 运行截图

### 1. 登录界面

![](https://user-gold-cdn.xitu.io/2020/1/31/16ff951b2e55cfd1?w=1920&h=1080&f=png&s=1311104)

### 2. 题库管理

![](https://user-gold-cdn.xitu.io/2020/1/31/16ff9521995fee62?w=1920&h=1080&f=png&s=165232)

### 3. 修改题库

![](https://user-gold-cdn.xitu.io/2020/1/31/16ff9525742f881c?w=1920&h=1080&f=png&s=108784)

## 技术分析

就来说一下项目中自己推敲做出来的几个算是亮点的东西吧

### 1. 使用Hook封装API访问工具

本项目采用的UI框架是Ant-Design框架 

因为这个项目的后台对于表格有着比较大的需求,而表格加载就需要使用到`Loading`的状态,所以就特地封装一下便于之后使用

首先我们先新建一个文件`useService.ts`

然后我们先引入`axios`来作为我们的api访问工具

```

import axios from 'axios'

const instance = axios.create({

  baseURL: '/api',

  timeout: 10000,

  headers: {

    'Content-Type': "application/json;charset=utf-8",

  },

})

instance.interceptors.request.use(

  config => {

    const token = localStorage.getItem('token');

    if (token) {

      config.headers.common['Authorization'] = token;

    }

    return config

  },

  error => {

    return Promise.reject(error)

  }

)

instance.interceptors.response.use(

  res => {

    let { data, status } = res

    if (status === 200) {

      return data

    }

    return Promise.reject(data)

  },

  error => {

    const { response: { status } } = error

    switch (status) {

      case 401:

        localStorage.removeItem('token')

        window.location.href = './#/login'

        break;

      case 504:

        message.error('代理请求失败')

    }

    return Promise.reject(error)

  }

)

```

先将`axios`的拦截器,基本配置这些写好先

接着我们实现一个获取接口信息的方法`useServiceCallback`

```typescript

const useServiceCallback = (fetchConfig: FetchConfig) => {

  // 定义状态,包括返回信息,错误信息,加载状态等

  const [isLoading, setIsLoading] = useState<boolean>(false)

  const [response, setResponse] = useState<any>(null)

  const [error, setError] = useState<any>(null)

  const { url, method, params = {}, config = {} } = fetchConfig

  const callback = useCallback(

    () => {

      setIsLoading(true)

      setError(null)

      // 调用axios来进行接口访问,并且将传来的参数传进去

      instance(url, {

        method,

        data: params,

        ...config

      })

        .then((response: any) => {

          // 获取成功后,则将loading状态恢复,并且设置返回信息

          setIsLoading(false)

          setResponse(Object.assign({}, response))

        })

        .catch((error: any) => {

          const { response: { data } } = error

          const { data: { msg } } = data

          message.error(msg)

          setIsLoading(false)

          setError(Object.assign({}, error))

        })

    }, [fetchConfig]

  )

  return [callback, { isLoading, error, response }] as const

}

```

这样就完成了主体部分了,可以利用这个hook来进行接口访问,接下来我们再做一点小工作

```

const useService = (fetchConfig: FetchConfig) => {

  const preParams = useRef({})

  const [callback, { isLoading, error, response }] = useServiceCallback(fetchConfig)

  useEffect(() => {

    if (preParams.current !== fetchConfig && fetchConfig.url !== '') {

      preParams.current = fetchConfig

      callback()

    }

  })

  return { isLoading, error, response }

}

export default useService

```

我们定义一个useService的方法,我们通过定义一个`useRef`来判断前后传过来的参数是否一致,如果不一样且接口访问配置信息的`url`不为空就可以开始调用`useServiceCallback`方法来进行接口访问了

具体使用如下: 

我们先在组件内render外使用这个钩子,并且定义好返回的信息 

接口返回体如下

![](https://user-gold-cdn.xitu.io/2020/1/31/16ff962e25349c58?w=443&h=106&f=png&s=7711)

```

const { isLoading = false, response } = useService(fetchConfig)

const { data = {} } = response || {}

const { exerciseList = [], total: totalPage = 0 } = data

```

因为我们这个hook是依赖`fetchConfig`这个对象的,这里是他的类型

```

export interface FetchConfig {

  url: string,

  method: 'GET' | 'POST' | 'PUT' | 'DELETE',

  params?: object,

  config?: object

}

```

所以我们只需要再页面加载时候调用`useEffect`来进行更新这个`fetchConfig`就可以触发这个获取数据的hook啦

```

  const [fetchConfig, setFetchConfig] = useState<FetchConfig>({

    url: '', method: 'GET', params: {}, config: {}

  })


  ...


  useEffect(() => {

    const fetchConfig: FetchConfig = {

      url: '/exercises',

      method: 'GET',

      params: {},

      config: {}

    }

    setFetchConfig(Object.assign({}, fetchConfig))

  }, [fetchFlag])

```

这样就大功告成啦!然后我们再到表格组件内传入相关数据就可以啦

```

<Table

          rowSelection={rowSelection}

          dataSource={exerciseList}

          columns={columns}

          rowKey="exerciseId"

          scroll={{

            y: "calc(100vh - 300px)"

          }}

          loading={{

            spinning: isLoading,

            tip: "加载中...",

            size: "large"

          }}

          pagination={{

            pageSize: 10,

            total: totalPage,

            current: currentPage,

            onChange: (pageNo) => setCurrentPage(pageNo)

          }}

          locale={{

            emptyText: <Empty

              image={Empty.PRESENTED_IMAGE_SIMPLE}

              description="暂无数据" />

          }}

        />

```

大功告成!!

### 2. 实现懒加载通用组件

我们这里使用的是`react-loadable`这个组件,挺好用的嘿嘿,搭配`nprogress`来进行过渡处理,具体效果参照`github`网站上的加载效果 

我们先封装好一个组件,在`components/LoadableComponent`内定义如下内容

```

import React, { useEffect, FC } from 'react'

import Loadable from 'react-loadable'

import NProgress from 'nprogress'

import 'nprogress/nprogress.css'

const LoadingPage: FC = () => {

  useEffect(() => {

    NProgress.start()

    return () => {

      NProgress.done()

    }

  }, [])

  return (

    <div className="load-component" />

  )

}

const LoadableComponent = (component: () => Promise<any>) => Loadable({

  loader: component,

  loading: () => <LoadingPage />,

})

export default LoadableComponent

```

我们先定义好一个组件`LoadingPage`这个是我们再加载中的时候需要展示的页面,在`useEffect`中使用`nprogress`的加载条进行显示,组件卸载时候则结束,而下面的`div`则可以由用户自己定义需要展示的样式效果

下面的`LoadableCompoennt`就是我们这个的主体,我们需要获取到一个组件,赋值给`loader`,具体的赋值方法如下,我们可以在项目内的`pages`部分将所有需要展示的页面引入进来,再导出,这样就可以方便的实现所有页面的懒加载了

```

// 引入刚刚定义的懒加载组件

import { LoadableComponent } from '@/admin/components'

// 定义组件,传给LoadableCompoennt组件需要的组件信息

const Login = LoadableComponent(() => import('./Login'))

const Register = LoadableComponent(() => import('./Register'))

const Index = LoadableComponent(() => import('./Index/index'))

const ExerciseList = LoadableComponent(() => import('./ExerciseList'))

const ExercisePublish = LoadableComponent(() => import('./ExercisePublish'))

const ExerciseModify = LoadableComponent(() => import('./ExerciseModify'))

// 导出,到时候再从这个pages/index.ts中引入,即可拥有懒加载效果了

export {

  Login,

  Register,

  Index,

  ExerciseList,

  ExercisePublish,

  ExerciseModify

}

```

大功告成!!!

### 3. 使用嵌套路由

项目因为涉及到后台信息的管理,所以个人认为导航栏与主题信息栏应该一同显示,如同下图 

![](https://user-gold-cdn.xitu.io/2020/1/31/16ff96cd89fd6959?w=1920&h=943&f=png&s=154028)

这样可以清晰的展示出信息以及给用户提供导航效果 

我们现在项目的`routes/index.tsx`定义一个全局通用的路由组件

```

import React from 'react'

import {

  Switch, Redirect, Route,

} from 'react-router-dom'

// 这个是私有路由,下面会提到

import PrivateRoute from '../components/PrivateRoute'

import { Login, Register } from '../pages'

import Main from '../components/Main/index'

const Routes = () => (

  <Switch>

    <Route exact path="/login" component={Login} />

    <Route exact path="/register" component={Register} />

    <PrivateRoute component={Main} path="/admin" />

    <Redirect exact from="/" to="/admin" />

  </Switch>

)

export default Routes

```

这里的意思就是,登录以及注册页面是独立开来的,而Main这个组件就是负责包裹导航条以及内容部分的组件啦

接下来看看`components/Main`中的内容吧

```

import React, { ComponentType } from 'react'

import { Layout } from 'antd';

import HeaderNav from '../HeaderNav'

import ContentMain from '../ContentMain'

import SiderNav from '../SiderNav'

import './index.scss'

const Main = () => (

  <Layout className="index__container">

    // 头部导航栏

    <HeaderNav />

    <Layout>

      // 侧边栏

      <SiderNav />

      <Layout>

        // 主体内容

        <ContentMain />

      </Layout>

    </Layout>

  </Layout>

)

export default Main as ComponentType

```

接下来重点就是这个`ContentMain`组件啦

```

import React, { FC } from 'react'

import { withRouter, Switch, Redirect, RouteComponentProps, Route } from 'react-router-dom'

import { Index, ExerciseList, ExercisePublish, ExerciseModify } from '@/admin/pages'

import './index.scss'

const ContentMain: FC<RouteComponentProps> = () => {

  return (

    <div className="main__container">

      <Switch>

        <Route exact path="/admin" component={Index} />

        <Route exact path="/admin/content/exercise-list" component={ExerciseList} />

        <Route exact path="/admin/content/exercise-publish" component={ExercisePublish} />

        <Route exact path="/admin/content/exercise-modify/:id" component={ExerciseModify} />

        <Redirect exact from="/" to="/admin" />

      </Switch>

    </div>

  )

}

export default withRouter(ContentMain)

```

这个就是一个嵌套路由啦,在这里面使用withRouter来包裹一下,然后在这里再次定义路由信息,这样就可以只切换主体部分的内容而不改变导航栏啦

大功告成!!!

### 4. 侧边栏的选中条目动态变化

![](https://user-gold-cdn.xitu.io/2020/1/31/16ff972021c83c47?w=334&h=626&f=png&s=20027)

通过图片我们可以看出,侧边导航栏有一个选中的内容,那么我们该如何判断不同的url页面对应哪一个选中部分呢?

```

  const [selectedKeys, setSelectedKeys] = useState(['index'])

  const [openedKeys, setOpenedKeys] = useState([''])

  const { location: { pathname } } = props

  const rank = pathname.split('/')

  useEffect(() => {

    switch (rank.length) {

      case 2: // 一级目录

        setSelectedKeys([pathname])

        setOpenedKeys([''])

        break

      case 4: // 二级目录

        setSelectedKeys([pathname])

        setOpenedKeys([rank.slice(0, 3).join('/')])

        break

    }

  }, [pathname])

```

如果是用React的没有使用到hook,则这里可以使用`componentWillReceiveProps()` 还有 `componentDidMount()`搭配使用,意思就是页面加载好之后设置一下这个选中,然后有更新也设置一下

这就是最重要的部分啦,我们通过定义几个状态`selectedKeys`选中的条目,`openedKeys`打开的多级导航栏

我们通过在页面加载时候,判断页面url路径,如果是一级目录,例如首页,就直接设置选中的条目即可,如果是二级目录,例如导航栏中`内容管理/题库管理`这个功能,他的url链接是`/admin/content/exercise-list`,所以我们的`case 4`就可以捕获到啦,然后设置当前选中的条目以及打开的多级导航,具体的导航信息请看下面

```

<Menu

        mode="inline"

        defaultSelectedKeys={['/admin']}

        selectedKeys={selectedKeys}

        openKeys={openedKeys}

        onOpenChange={handleMenuChange}

      >

        <Menu.Item key="/admin">

          <Link to="/admin">

            <Icon type="home" />

            首页

        </Link>

        </Menu.Item>

        <SubMenu

          key="/admin/content"

          title={

            <span>

              <Icon type="profile" />

              内容管理

            </span>

          }

        >

          <Menu.Item key="/admin/content/exercise-list">

            <Link to="/admin/content/exercise-list">题库管理</Link>

          </Menu.Item>

        </SubMenu>

    </Menu>

```

这样我们无论是通过点击侧边导航栏,或者是直接输入url访问页面,这个导航栏选中的条目就会与我们访问的页面对应的啦~

大功告成!!!

### 5. 巧妙利用Antd表单来构造特殊的数据结构

使用过Antd表单的胖友们一定知道`this.props.form.validateFields()`这个方法吧嘿嘿,他是如果验证成功就返回表单的值给你,不用自己去绑定输入组件的值,很方便,来看看官方的例子

![](https://user-gold-cdn.xitu.io/2020/2/1/16ffe5c3647acf7e?w=1419&h=724&f=png&s=85764)

可以看到,最简单的一个登录框,然后我们就可以得到一组数据啦,不过我们可以发现,这些数据就是一个对象中的几个值。 

假如我们有很多数据,想用多个对象来构造数据结构,这应该怎么办呢,就例如这样子的数据结构,我们还是举上面这个例子

![](https://user-gold-cdn.xitu.io/2020/2/1/16ffe5f52d53246c?w=502&h=358&f=png&s=59764)

假如吼,我们提交后台的数据需要是这样子的数据结构,用户名和密码在`userInfo`这个对象内,然后是否记住密码是在`other`对象里面,自己得到数据之后再构造又十分麻烦,这可怎么办呢。 

在此之前,我们不如看看官方给的另一个例子,一个动态添加表单项的例子,于此我们就可以发挥想象力,然后就可以解决我们上面的问题啦

![](https://user-gold-cdn.xitu.io/2020/2/1/16ffe620163efad1?w=1341&h=745&f=png&s=103764)

可以看到这个动态添加表单项的,是以数组形式来存储数据的,他的代码是这样的

```

{getFieldDecorator(`names[${k}]`, {

  validateTrigger: ['onChange', 'onBlur'],

  rules: [

    {

      required: true,

      whitespace: true,

      message: "Please input passenger's name or delete this field.",

    },

  ],

})(<Input placeholder="passenger name" style={{ width: '60%', marginRight: 8 }} />)}

```

Antd表单的构造数据关键就在于里面的`getFieldDecorator`内的第一个参数,也就是我们的`propName`用来指定数据叫啥,跟之后验证表单传回的值是对应的了。这就给了我们一个很大的提示啦!! 

> 这个`propName`叫什么,之后生成的数据结构里面就是什么,是`a`,之后数据就对应`a`,是`b`,就对应`b`

这里通过一个`names[$k]`,就可以让之后得到的数据变成一个数组`names:Array(2): ['1', '2']`这样子的形式,那么我们稍加改造一下,就可以变成对象的形式啦!下面看看代码,其实也很简单!

```

<Form.Item label="题目内容" >

{getFieldDecorator(`topicList[${index}].topicContent`, {

  rules: TopicContentRules,

  initialValue: topicList[index].topicContent

})(<Input.TextArea />)}

</Form.Item>

```

这里我就直接举项目中题库提交的例子啦,`topicList`是一个列表,里面存的是每一个题目对应的数据对象

![](https://user-gold-cdn.xitu.io/2020/2/1/16ffe6790b090234?w=1023&h=241&f=png&s=132789)

这里的`propName`,我指定成了`topicList[$(index)]`就代表,这个属于这个列表里面的第几个对象,然后后面的`.topicContent`就代表这个对象里面的值是什么,最后我们的出的结构就是这样子的啦!

![](https://user-gold-cdn.xitu.io/2020/2/1/16ffe6a59130cebe?w=850&h=477&f=png&s=68397)

我们如愿得到了想要的数据结构了,这里面有对象,有数组,十分方便,可以灵活根据实际情况进行使用,关键就在于`getFieldDecorator()`里面的`propName`,直接以对象的形式命名,就可以啦!就按照下面这种形式就好啦!

```

<Form.Item label="itemName" >

    {getFieldDecorator(`object.itemName`, {

      initialValue: 'BB小天使'

    })(<Input />)}

</Form.Item>

```

之后就可以得到对象类型的表单值啦!

大功告成!!!

### 6. 后台接口获取信息后填充Antd表单

因为有一个题库修改的功能,所以打算获取完接口信息之后,直接将内容通过Antd表单的`setFields`的方法来直接填充表格中的信息,结果控制台报错了

![](https://user-gold-cdn.xitu.io/2020/1/31/16ffbaf24642b709?w=1039&h=69&f=png&s=10756)

看了看大致意思就是说emmmm不可以在渲染之前就设置表单的值,嘶~这可难受了,这时候想到他的表单内有一个`initialValue`的属性,是表单项的默认值,这可好办啦,这样我们先拉取信息,存入对象中,然后再通过这个属性给表单传值,果然不出所料,真的ok了没有报错了哈哈哈,具体看下面

```

  // 定义选项列表来存储题库的题目列表信息

  const [topicList, setTopicList] = useState<TopicList[]>([{

    topicType: 1,

    topicAnswer: [],

    topicContent: '',

    topicOptions: []

  }])

  // 定义题库基本信息对象

  const [exerciseInfo, setExerciseInfo] = useState<ExerciseInfo>({

    exerciseName: '',

    exerciseContent: '',

    exerciseDifficulty: 1,

    exerciseType: 1,

    isHot: false

  })

  // 首先先拉取信息,这就是题库的信息啦

  const { data } = await http.get(`/exercises/${id}`)

  const {

    exerciseName,

    exerciseContent,

    exerciseDifficulty,

    exerciseType,

    isHot,

    topicList } = data

  topicList.forEach((_: any, index: number) => {

    topicList[index].topicOptions = topicList[index].topicOptions.map((item: any) => item.option)

  })


  // 获取信息后,设置状态

  setTopicList([...topicList])

  setExerciseInfo({

    exerciseName,

    exerciseContent,

    exerciseDifficulty,

    exerciseType,

    isHot,

  })

```

这样我们就得到了题库信息的对象啦,待会我们就可以用来传默认值给表单啦!

```

// 这里就通过题库名称来做例子,就从刚才设置的信息对象中取值然后设置默认值就可以啦

<Form.Item label="题库名称">

  {getFieldDecorator('exerciseName', {

    rules: ExerciseNameRules,

    initialValue: exerciseInfo.exerciseName

  })(<Input />)}

</Form.Item>

```

因为题库的题目是有挺多,所以是一个列表,类似下图

![](https://user-gold-cdn.xitu.io/2020/1/31/16ffbb790d7df312?w=1920&h=1080&f=png&s=98397)

所以我们实现设置好`topicList`这个数组来存储题目的信息,然后我们通过遍历这个列表来实现多题目编辑

```

<Form.Item label="新增题目">

    {topicList && topicList.map((_: any, index: number) => {

      return (

        <Fragment key={index}>

          <div className="form__subtitle">

            第{index + 1}题

            <Tooltip title="删除该题目">

              <Icon

                type="delete"

                theme="twoTone"

                twoToneColor="#fa4b2a"

                style={{ marginLeft: 16, display: topicList.length > 1 ? 'inline' : 'none' }}

                onClick={() => handleTopicDeleteClick(index)} />

            </Tooltip>

          </div>

          <Form.Item label="题目内容" >

            {getFieldDecorator(`topicList[${index}].topicContent`, {

              rules: TopicContentRules,

              initialValue: topicList[index].topicContent

            })(<Input.TextArea />)}

          </Form.Item>


          ...... 省略一堆~


        </Fragment>

      )

    })}

    <Form.Item>

      <Button onClick={handleTopicAddClick}>新增题目</Button>

    </Form.Item>

  </Form.Item>

```

例如**题目内容**的话,我们就设置他的`initialValue`为`topicList[index].topicContent`即可,别的属性同理,然后点击新增题目按钮,就直接往topicList内添加对象信息即可完成题目列表的增加,点击删除图标,就删除列表中某一项,是不是十分方便!!哈哈哈

大功告成!!!

### 7. 使用JWTToken来验证用户登录状态以及返回信息

要想使用登录注册功能,还有用户权限的问题,我们就需要使用到这个token啦!为什么我们要使用token呢?而不是用传统的cookies呢,因为使用token可以避免跨域啊还有更多的复杂问题,大大简化我们的开发效率

> 本项目后台采用nodeJs来进行开发 

我们先在后台定义一个工具`utils/token.js`

```

// token的秘钥,可以存在数据库中,我偷懒就卸载这里面啦hhh

const secret = "zhcxk1998"

const jwt = require('jsonwebtoken')

// 生成token的方法,注意前面一定要有Bearer ,注意后面有一个空格,我们设置的时间是1天过期

const generateToken = (payload = {}) => (

  'Bearer ' + jwt.sign(payload, secret, { expiresIn: '1d' })

)

// 这里是获取token信息的方法

const getJWTPayload = (token) => (

  jwt.verify(token.split(' ')[1], secret)

)

module.exports = {

  generateToken,

  getJWTPayload

}

```

这里采用的是`jsonwebtoken`这个库,来进行token的生成以及验证。

有了这个token啦,我们就可以再登录或者注册的时候给用户返回一个token信息啦

```

router.post('/login', async (ctx) => {

  const responseBody = {

    code: 0,

    data: {}

  }

  try {

    if (登录成功) {

      responseBody.data.msg = '登陆成功'

      // 在这里就可以返回token信息给前端啦

      responseBody.data.token = generateToken({ username })

      responseBody.code = 200

    } else {

      responseBody.data.msg = '用户名或密码错误'

      responseBody.code = 401

    }

  } catch (e) {

    responseBody.data.msg = '用户名不存在'

    responseBody.code = 404

  } finally {

    ctx.response.status = responseBody.code

    ctx.response.body = responseBody

  }

})

```

这样前端就可以获取这个token啦,前端部分只需要将token存入`localStorage`中即可,不用担心`localStorage`是永久保存,因为我们的token有个过期时间,所以不用担心

```

  /* 登录成功 */

  if (code === 200) {

    const { msg, token } = data

    // 登录成功后,将token存入localStorage中

    localStorage.setItem('token', token)

    message.success(msg)

    props.history.push('/admin')

  }

```

好嘞,现在前端获取token也搞定啦,接下来我们就需要在访问接口的时候带上这个token啦,这样才可以让后端知道这个用户的权限如何,是否过期等

需要传tokne给后端,我们可以通过每次接口都传一个字段`token`,但是这样十分浪费成本,所以我们再封装好的`axios`中,我们设置请求头信息即可

```

import axios from 'axios'

const instance = axios.create({

  baseURL: '/api',

  timeout: 10000,

  headers: {

    'Content-Type': "application/json;charset=utf-8",

  },

})

instance.interceptors.request.use(

  config => {

    // 请求头带上token信息

    const token = localStorage.getItem('token');

    if (token) {

      config.headers.common['Authorization'] = token;

    }

    return config

  },

  error => {

    return Promise.reject(error)

  }

)

...

export default instance

```

![](https://user-gold-cdn.xitu.io/2020/1/31/16ff9814a9d0e5d8?w=1094&h=306&f=png&s=40494)

如上图所示,我们每次请求接口的时候就会带上这个请求头啦!那么接下来我们就谈谈后端如何获取这个token并且验证吧

有获取token,以及验证部分,那么就需要出动我们的中间件啦!

我们验证token的话,要是用户是访问的登录或者注册接口,那么这个时候token其实是没有作用哒,所以我们需要将它隔离一下,所以我们定义一个中间件,用来跳过某些路由,我们再`middleware/verifyToken.js`中定义(这里我们采用`koa-jwt`来验证token)

```

const koaJwt = require('koa-jwt')

const verifyToken = () => {

  return koaJwt({ secret: 'zhcxk1998' }).unless({

    path: [

      /login/,

      /register/

    ]

  })

}

module.exports = verifyToken

```

这样就可以忽略这登录注册路由啦,别的路由就验证token

拦截已经成功啦,那么我们该如何捕获,然后进行处理呢?我们再`middleware/interceptToken`定义一个中间件,来处理捕获的token信息

```

const interceptToken = async (ctx, next) => {

  return await next().catch((err) => {

    const { status } = err

    if (status === 401) {

      ctx.response.status = 401

      ctx.response.body = {

        code: 401,

        data: {

          msg: '请登录后重试'

        }

      }

    } else {

      throw err

    }

  })

}

module.exports = () => (

  interceptToken

)

```

由于`koa-jwt`拦截的token,如果过期,他会自动抛出一个401的异常以表示该token已经过期,所以我们只需要判断这个状态`status`然后进行处理即可

好嘞,中间件也定义好了,我们就在后端服务中使用起来吧!

```

const Koa = require('koa')

const Router = require('koa-router');

const bodyParser = require('koa-bodyparser')

const cors = require('koa2-cors');

const routes = require('../routes/routes')

const router = new Router()

const admin = new Koa();

const {

  verifyToken,

  interceptToken

} = require('../middleware')

const {

  login,

  info,

  register,

  exercises

} = require('../routes/admin')

admin.use(cors())

admin.use(bodyParser())

/* 拦截token */

admin.use(interceptToken())

admin.use(verifyToken())

/* 管理端 */

admin.use(routes(router, { login, info, register, exercises }))

module.exports = admin

```

我们直接使用`router.use()`的方法就可以使用中间件啦,这里要记住!验证拦截token一定要在路由信息之前,否则是拦截不到的哟(如果在后面,路由都先执行了,还拦截啥嘛!)

大功告成!!!

### 8. 密码使用加密加盐的方式存储

我们在处理用户的信息的时候,需要存储密码,但是直接存储肯定不安全啦!所以我们需要加密以及加盐的处理,在这里我用到的是`crypto`这个库

首先我们再`utils/encrypt.js`中定义一个工具函数用来生成盐值以及获取加密信息

```

const crypto = require('crypto')

// 获取随机盐值,例如 c6ab1 这样子的字符串

const getRandomSalt = () => {

  const start = Math.floor(Math.random() * 5)

  const count = start + Math.ceil(Math.random() * 5)

  return crypto.randomBytes(10).toString('hex').slice(start, count)

}

// 获取密码转换成md5之后的加密信息

const getEncrypt = (password) => {

  return crypto.createHash('md5').update(password).digest('hex')

}

module.exports = {

  getRandomSalt,

  getEncrypt

}

```

这样我们就可以通过验证密码与数据库中加密的信息对不对得上,来判断是否登录成功等等

我们现在注册中使用上,当然我们需要两个表进行数据存储,一个是用户信息,一个是用户密码表,这样分开更加安全,例如这样

![](https://user-gold-cdn.xitu.io/2020/1/31/16ff98e72c1d20f5?w=1525&h=613&f=png&s=183724)

这样就可以将用户信息还有密码分开存放,更加安全,这里就不重点叙述啦

```

const { getRandomSalt, getEncrypt } = require('../../utils/encrypt')

// 注册部分

router.post('/register', async (ctx) => {

  const { username, password, phone, email } = ctx.request.body

  // 获取盐值以及加密后的信息

  const salt = getRandomSalt()

  // 数据库存放的密码是由用户输入的密码加上随机盐值,然后再进行加密所得到的的炒鸡加密密码

  const encryptPassword = getEncrypt(password + salt)


  // 插入用户信息,以及获取这个的id

  const { insertId: user_id } = await query(INSERT_TABLE('user_info'), { username, phone, email });

  // 插入用户密码信息,user_id与上面对应

  await query(INSERT_TABLE('user_password'), {

    user_id,

    password: encryptPassword,

    salt

  })

  ...



})

```

接下来再来看登录部分,登录的话,就需要从用户密码表中取出加密密码,以及盐值,然后进行对比

```

// 通过用户名,先获取加密密码以及盐值

const { password: verifySign, salt } = await query(`select password, salt from user_password where user_id = '${userId}'`)[0]

// 这个就是用户输入的密码加上盐值一起加密后的密码

const sign = getEncrypt(password + salt)

// 这个加密的密码与数据库中加密的密码对比,如果一样则登陆成功

if (sign === verifySign) {

  responseBody.data.msg = '登陆成功'

  responseBody.data.token = generateToken({ username })

  responseBody.code = 200

} else {

  responseBody.data.msg = '用户名或密码错误'

  responseBody.code = 401

}

```

大功告成!!!

## 结语

大部分的内容就大概这样子,这是自己开发中遇到的小问题还有解决方法,希望对大家有所帮助,大家一起成长!现在得看看面试题准备一波春招了,不然大学毕业了都找不到工作啦!有时间再继续更新这个文章!

## 最后还是顺便求一波star还有点赞!!!

何时才能上100点赞,100star啊呜呜呜

[github项目猛戳进来star一下嘿嘿](https://github.com/zhcxk1998/School-Partners) 

[小程序介绍文章,使劲戳!](https://juejin.im/post/5dd161675188254efb3bceea)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容