React + Node.JS 巧妙實現後臺管理系統の各種小技巧(前後端)

BB小天使發表於2020-01-31

目前因學業任務比較重,沒有好好的完善,現在比較完善的只有題庫管理,新增題庫,修改題庫以及登入的功能,但搭配小程式使用,主體功能已經實現了

此後臺系統是為了搭配我的另一個專案 School-Partners學習伴侶微信小程式而開發的。是一個採用Taro多端框架開發的跨平臺的小程式。感興趣的可以看一下之前的文章

這篇文章主要是分享一下在開發這個東東的時候,遇到的一些問題,以及一些技術的巧妙的方法分享給大家,如果對大家有幫助的話,請給我點贊一下給個star鼓勵一下~無比感謝嘿嘿

React + Node.JS 巧妙實現後臺管理系統の各種小技巧(前後端)

希望大佬們走過路過可以給個star鼓勵一下~感激不盡~這個是小程式還有後臺都整合在一起的倉庫,client是小程式端的前端程式碼,server是小程式端和管理端的後臺,admin是管理端的前端程式碼

github.com/zhcxk1998/S…

這個是小程式的介紹文章
這是配套的小程式介紹文章,使勁戳!

無圖無真相!先上幾個圖~

執行截圖

1. 登入介面

React + Node.JS 巧妙實現後臺管理系統の各種小技巧(前後端)

2. 題庫管理

React + Node.JS 巧妙實現後臺管理系統の各種小技巧(前後端)

3. 修改題庫

React + Node.JS 巧妙實現後臺管理系統の各種小技巧(前後端)

技術分析

就來說一下專案中自己推敲做出來的幾個算是亮點的東西吧

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

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外使用這個鉤子,並且定義好返回的資訊
介面返回體如下

React + Node.JS 巧妙實現後臺管理系統の各種小技巧(前後端)

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. 使用巢狀路由

專案因為涉及到後臺資訊的管理,所以個人認為導航欄與主題資訊欄應該一同顯示,如同下圖

React + Node.JS 巧妙實現後臺管理系統の各種小技巧(前後端)

這樣可以清晰的展示出資訊以及給使用者提供導航效果

我們現在專案的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. 側邊欄的選中條目動態變化

React + Node.JS 巧妙實現後臺管理系統の各種小技巧(前後端)

通過圖片我們可以看出,側邊導航欄有一個選中的內容,那麼我們該如何判斷不同的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()這個方法吧嘿嘿,他是如果驗證成功就返回表單的值給你,不用自己去繫結輸入元件的值,很方便,來看看官方的例子

React + Node.JS 巧妙實現後臺管理系統の各種小技巧(前後端)
可以看到,最簡單的一個登入框,然後我們就可以得到一組資料啦,不過我們可以發現,這些資料就是一個物件中的幾個值。

假如我們有很多資料,想用多個物件來構造資料結構,這應該怎麼辦呢,就例如這樣子的資料結構,我們還是舉上面這個例子

React + Node.JS 巧妙實現後臺管理系統の各種小技巧(前後端)

假如吼,我們提交後臺的資料需要是這樣子的資料結構,使用者名稱和密碼在userInfo這個物件內,然後是否記住密碼是在other物件裡面,自己得到資料之後再構造又十分麻煩,這可怎麼辦呢。

在此之前,我們不如看看官方給的另一個例子,一個動態新增表單項的例子,於此我們就可以發揮想象力,然後就可以解決我們上面的問題啦

React + Node.JS 巧妙實現後臺管理系統の各種小技巧(前後端)

可以看到這個動態新增表單項的,是以陣列形式來儲存資料的,他的程式碼是這樣的

{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是一個列表,裡面存的是每一個題目對應的資料物件

React + Node.JS 巧妙實現後臺管理系統の各種小技巧(前後端)

這裡的propName,我指定成了topicList[$(index)]就代表,這個屬於這個列表裡面的第幾個物件,然後後面的.topicContent就代表這個物件裡面的值是什麼,最後我們的出的結構就是這樣子的啦!

React + Node.JS 巧妙實現後臺管理系統の各種小技巧(前後端)

我們如願得到了想要的資料結構了,這裡面有物件,有陣列,十分方便,可以靈活根據實際情況進行使用,關鍵就在於getFieldDecorator()裡面的propName,直接以物件的形式命名,就可以啦!就按照下面這種形式就好啦!

<Form.Item label="itemName" >
    {getFieldDecorator(`object.itemName`, {
      initialValue: 'BB小天使'
    })(<Input />)}
</Form.Item>
複製程式碼

之後就可以得到物件型別的表單值啦!

大功告成!!!

6. 後臺介面獲取資訊後填充Antd表單

因為有一個題庫修改的功能,所以打算獲取完介面資訊之後,直接將內容通過Antd表單的setFields的方法來直接填充表格中的資訊,結果控制檯報錯了

React + Node.JS 巧妙實現後臺管理系統の各種小技巧(前後端)

看了看大致意思就是說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>
複製程式碼

因為題庫的題目是有挺多,所以是一個列表,類似下圖

React + Node.JS 巧妙實現後臺管理系統の各種小技巧(前後端)
所以我們實現設定好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>
複製程式碼

例如題目內容的話,我們就設定他的initialValuetopicList[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
複製程式碼

React + Node.JS 巧妙實現後臺管理系統の各種小技巧(前後端)

如上圖所示,我們每次請求介面的時候就會帶上這個請求頭啦!那麼接下來我們就談談後端如何獲取這個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
}
複製程式碼

這樣我們就可以通過驗證密碼與資料庫中加密的資訊對不對得上,來判斷是否登入成功等等

我們現在註冊中使用上,當然我們需要兩個表進行資料儲存,一個是使用者資訊,一個是使用者密碼錶,這樣分開更加安全,例如這樣

React + Node.JS 巧妙實現後臺管理系統の各種小技巧(前後端)

這樣就可以將使用者資訊還有密碼分開存放,更加安全,這裡就不重點敘述啦

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一下嘿嘿
小程式介紹文章,使勁戳!

相關文章