目前因學業任務比較重,沒有好好的完善,現在比較完善的只有題庫管理,新增題庫,修改題庫以及登入的功能,但搭配小程式使用,主體功能已經實現了
此後臺系統是為了搭配我的另一個專案 School-Partners學習伴侶
微信小程式而開發的。是一個採用Taro
多端框架開發的跨平臺的小程式。感興趣的可以看一下之前的文章
這篇文章主要是分享一下在開發這個東東的時候,遇到的一些問題,以及一些技術的巧妙的方法分享給大家,如果對大家有幫助的話,請給我點贊一下給個star鼓勵一下~無比感謝嘿嘿
希望大佬們走過路過可以給個star鼓勵一下~感激不盡~這個是小程式還有後臺都整合在一起的倉庫,client
是小程式端的前端程式碼,server
是小程式端和管理端的後臺,admin
是管理端的前端程式碼
這個是小程式的介紹文章
這是配套的小程式介紹文章,使勁戳!
無圖無真相!先上幾個圖~
執行截圖
1. 登入介面
2. 題庫管理
3. 修改題庫
技術分析
就來說一下專案中自己推敲做出來的幾個算是亮點的東西吧
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外使用這個鉤子,並且定義好返回的資訊
介面返回體如下
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. 使用巢狀路由
專案因為涉及到後臺資訊的管理,所以個人認為導航欄與主題資訊欄應該一同顯示,如同下圖
這樣可以清晰的展示出資訊以及給使用者提供導航效果
我們現在專案的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的沒有使用到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()
這個方法吧嘿嘿,他是如果驗證成功就返回表單的值給你,不用自己去繫結輸入元件的值,很方便,來看看官方的例子
假如我們有很多資料,想用多個物件來構造資料結構,這應該怎麼辦呢,就例如這樣子的資料結構,我們還是舉上面這個例子
假如吼,我們提交後臺的資料需要是這樣子的資料結構,使用者名稱和密碼在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
的方法來直接填充表格中的資訊,結果控制檯報錯了
看了看大致意思就是說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>
複製程式碼
因為題庫的題目是有挺多,所以是一個列表,類似下圖
所以我們實現設定好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
複製程式碼
如上圖所示,我們每次請求介面的時候就會帶上這個請求頭啦!那麼接下來我們就談談後端如何獲取這個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
}
複製程式碼
這樣我們就可以通過驗證密碼與資料庫中加密的資訊對不對得上,來判斷是否登入成功等等
我們現在註冊中使用上,當然我們需要兩個表進行資料儲存,一個是使用者資訊,一個是使用者密碼錶,這樣分開更加安全,例如這樣
這樣就可以將使用者資訊還有密碼分開存放,更加安全,這裡就不重點敘述啦
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一下嘿嘿
小程式介紹文章,使勁戳!