React + Node.JS умело реализует различные приемы системы фонового управления (front-end и back-end)

React.js

В настоящее время из-за тяжелых академических задач он не был полностью улучшен.В настоящее время относительно завершено только управление банками вопросов, добавление новых банков вопросов, изменение банков вопросов и вход в систему, но при использовании с небольшими программами реализованы основные функции

Эта бэкэнд-система для другого моего проектаSchool-Partners学习伴侣Разработан для апплета WeChat. принимаетTaroКроссплатформенный апплет, разработанный многотерминальной структурой. Если вам интересно, вы можете прочитать предыдущую статью

Эта статья в основном предназначена для того, чтобы рассказать о некоторых проблемах, возникающих при разработке этого материала, а также о некоторых оригинальных технических методах, которыми я могу поделиться с вами. ты очень хехе

Я надеюсь, что вы можете дать звезду, чтобы ободрить вас, когда вы проходите мимо.Большое спасибо~ Это склад, где апплет и фон интегрированы.clientэто интерфейсный код апплета,serverЭто фон стороны апплета и боковой стороны управления.adminЯвляется интерфейсным кодом управления

GitHub.com/Китайская Суперлига выходит из класса 1998/S…

Это введение в апплет
Это вводная статья о поддержке апплета, ткните посильнее!

Нет картинок, нет правды! Сначала несколько фото~

запустить скриншот

1. Интерфейс входа

2. Управление вопросительным банком

3. Измените банк вопросов

технический анализ

Расскажем о некоторых особенностях проекта.

1. Используйте Hook для инкапсуляции инструментов доступа к API

Фреймворк пользовательского интерфейса, используемый в этом проекте, — это фреймворк 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
}

На этом основная часть завершена, вы можете использовать этот хук для доступа к интерфейсу, а дальше мы немного поработаем

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метод доступа к интерфейсу

Конкретное использование заключается в следующем:

Сначала мы используем этот хук внутри компонента и вне рендера и определяем возвращаемую информацию
Тело возврата интерфейса выглядит следующим образом

const { isLoading = false, response } = useService(fetchConfig)
const { data = {} } = response || {}
const { exerciseList = [], total: totalPage = 0 } = data

Потому что наш хук — это зависимостьfetchConfigэтого объекта, вот его тип

export interface FetchConfig {
  url: string,
  method: 'GET' | 'POST' | 'PUT' | 'DELETE',
  params?: object,
  config?: object
}

Так что нам просто нужно вызвать его, когда страница загружаетсяuseEffectобновить этоfetchConfigВы можете активировать этот хук, чтобы получить данные.

  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. Используйте вложенную маршрутизацию

Поскольку проект включает в себя управление фоновой информацией, лично я считаю, что панель навигации и панель информации о теме должны отображаться вместе, как показано на рисунке ниже.

Это может четко отображать информацию и предоставлять пользователям навигационные эффекты.

Теперь мы проектируемся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. Выбранный элемент на боковой панели меняется динамически

На картинке видно, что на боковой панели навигации есть выделенный контент, так как же определить, какая выделенная часть соответствует разным 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 без хуков, вы можете использовать их здесь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()Этот метод, хе-хе, в случае успешной проверки, он вернет вам значение формы. Вам не нужно самостоятельно привязывать значение входного компонента. Это очень удобно. Давайте рассмотрим официальный пример.

Как видите, простейшее поле для входа, и тогда мы можем получить набор данных, а можем обнаружить, что эти данные представляют собой несколько значений в объекте.

Если у нас много данных и нужно использовать несколько объектов для построения структуры данных, то должно быть как это сделать, например, вот такая структура данных, мы приводим пример выше

Если вы говорите, мы отправляем данные о том, что должны быть такая структура данных, имя пользователя и пароль.userInfoвнутри этого объекта, то следует ли помнить, что пароль находится вotherВ объекте очень хлопотно конструировать после получения данных самостоятельно, как это сделать?

Перед этим давайте взглянем на другой пример, приведенный официальным лицом, пример динамического добавления элементов формы, здесь мы можем использовать наше воображение, а затем мы можем решить наши вышеперечисленные проблемы.

Как видите, этот динамически добавляемый элемент формы хранит данные в виде массива, его код такой

{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представляет собой список, в котором хранятся объекты данных, соответствующие каждой теме

здесьpropName, я указал какtopicList[$(index)]Это означает, что это относится к первым нескольким объектам в этом списке, а затем к следующим.topicContentОн представляет значение этого объекта, и, наконец, наша структура выглядит так!

Получили желаемую структуру данных как и хотели.В ней есть объекты и массивы,что очень удобно и можно гибко использовать по реальной ситуации.Ключ кроется вgetFieldDecorator()внутриpropName, Назовите его прямо в форме объекта, и все готово! Просто следуйте форме ниже!

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

Затем вы можете получить значение формы типа объекта!

Готово! ! !

6. Фоновый интерфейс получает информацию и заполняет форму Antd

Поскольку есть функция модификации банка вопросов, я планирую напрямую передать содержимое через форму Antd после получения информации об интерфейсе.setFieldsметод прямого заполнения информации в таблице, консоль сообщила об ошибке

Глядя на это, это значит, что эммммм не может установить значение формы перед рендерингом, шипение~ Это неудобно.В это время я думал, что в его форме есть форма.initialValueАтрибут элемента формы — это значение элемента формы по умолчанию, с которым легко обращаться, поэтому мы сначала извлекаем информацию, сохраняем ее в объекте, а затем передаем значение в форму через этот атрибут.

  // 定义选项列表来存储题库的题目列表信息
  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>

Так как вопросов в банке вопросов довольно много, это список, как на картинке ниже

Итак, реализуем настройку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Вот и все, то же самое относится и к другим атрибутам, а затем нажмите кнопку «Добавить тему», чтобы добавить информацию об объекте непосредственно в список тем, чтобы завершить добавление списка тем. это очень удобно! ! Хахаха

Готово! ! !

7. Используйте JWTToken для проверки статуса входа пользователя и возврата информации.

Чтобы использовать функцию регистрации входа, также есть проблема с правами пользователей, нам нужно использовать этот токен! Почему мы должны использовать токен? Неиспользование традиционных файлов cookie, поскольку использование токена позволяет избежать междоменного взаимодействия, возникают более сложные проблемы, что значительно упрощает нашу разработку.

Фон этого проекта использует 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Эта библиотека используется для генерации и проверки токенов.

С помощью этого токена мы можем вернуть информацию о токене пользователю при входе в систему или регистрации.

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
  }
})

Таким образом, интерфейс может получить этот токен, а интерфейсной части нужно только сохранить токен вlocalStorageв, не волнуйсяlocalStorageОн хранится постоянно, потому что у нашего токена есть срок годности, так что не беспокойтесь

  /* 登录成功 */
  if (code === 200) {
    const { msg, token } = data
    // 登录成功后,将token存入localStorage中
    localStorage.setItem('token', token)
    message.success(msg)
    props.history.push('/admin')
  }

Хорошо, теперь фронтенд получает токен, а затем нам нужно этот токен приносить при доступе к интерфейсу, чтобы бэкенд мог знать, какие права у пользователя, не просрочены ли они и т.д.

Нужно передать токен на сервер, мы можем передать поле через каждый интерфейс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

Как показано на рисунке выше, мы будем приносить этот заголовок запроса каждый раз, когда запрашиваем интерфейс! Затем давайте поговорим о том, как серверная часть получает этот токен и проверяет его.

Получил токен и проверить часть, то нам нужно мобилизовать наше промежуточное программное обеспечение!

Если мы проверим токен, если пользователь получает доступ к интерфейсу входа или регистрации, то токен на самом деле бесполезен в настоящее время, поэтому нам нужно его изолировать, поэтому мы определяем промежуточное программное обеспечение для пропуска определенных маршрутов, а затем мыmiddleware/verifyToken.jsопределено в (здесь мы используемkoa-jwtдля проверки токена)

const koaJwt = require('koa-jwt')

const verifyToken = () => {
  return koaJwt({ secret: 'zhcxk1998' }).unless({
    path: [
      /login/,
      /register/
    ]
  })
}

module.exports = verifyToken

Таким образом, вы можете игнорировать этот маршрут регистрации входа, и другие маршруты будут проверять токен.

Перехват прошел успешно, так как же нам его захватить и что с ним делать? мы сноваmiddleware/interceptTokenОпределите промежуточное ПО для обработки захваченной информации о токенах.

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Если срок действия перехваченного токена истек, он автоматически выдаст исключение 401, чтобы указать, что срок действия токена истек, поэтому нам нужно только оценить этот статус.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()метод, вы можете использовать промежуточное программное обеспечение, помните здесь! Токен перехвата верификации должен стоять перед информацией о маршрутизации, иначе его невозможно перехватить (если он в бэке, то сначала выполняется маршрутизация, чего тут перехватывать!)

Готово! ! !

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
}

Таким образом, мы можем судить об успешном входе в систему, проверяя, соответствует ли пароль зашифрованной информации в базе данных и т. д.

Мы используем его сейчас при регистрации.Конечно, нам нужны две таблицы для хранения данных, одна - информация о пользователе, а другая - таблица паролей пользователя, которую более безопасно разделить, например, это

Таким образом, информация о пользователе и пароли могут храниться отдельно, что более безопасно, здесь это описываться не будет.

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
}

Готово! ! !

Эпилог

Большая часть контента примерно такая.Это небольшая проблема, возникшая в моей собственной разработке и ее решение.Надеюсь всем будет полезно, и все будут расти вместе! Теперь вам предстоит просмотреть вопросы собеседования и подготовиться к волне весеннего набора, иначе вы не сможете найти работу после окончания колледжа! Я продолжу обновлять эту статью, когда у меня будет время!

Напоследок, кстати, еще прошу волну звездочек и лайков! ! !

Когда я смогу получить 100 лайков, 100 звездПроект github врезался в звезду, хе-хе
Небольшая вводная статья о программе, ткните посильнее!