基于 antd pro 的短信驗(yàn)證碼登錄功能(流程分析)
概要
最近使用 antd pro 開發(fā)項(xiàng)目時(shí)遇到個(gè)新的需求, 就是在登錄界面通過短信驗(yàn)證碼來(lái)登錄, 不使用之前的用戶名密碼之類登錄方式.
這種方式雖然增加了額外的短信費(fèi)用, 但是對(duì)于安全性確實(shí)提高了不少. antd 中并沒有自帶能夠倒計(jì)時(shí)的按鈕,
但是 antd pro 的 ProForm components 中倒是提供了針對(duì)短信驗(yàn)證碼相關(guān)的組件.
組件說明可參見: https://procomponents.ant.design/components/form
整體流程
通過短信驗(yàn)證碼登錄的流程很簡(jiǎn)單:
- 請(qǐng)求短信驗(yàn)證碼(客戶端)
- 生成短信驗(yàn)證碼, 并設(shè)置驗(yàn)證碼的過期時(shí)間(服務(wù)端)
- 調(diào)用短信接口發(fā)送驗(yàn)證碼(服務(wù)端)
- 根據(jù)收到的短信驗(yàn)證碼登錄(客戶端)
- 驗(yàn)證手機(jī)號(hào)和短信驗(yàn)證碼, 驗(yàn)證通過之后發(fā)行 jwt-token(服務(wù)端)
前端
頁(yè)面代碼
import React, { useState } from 'react'; import { connect } from 'umi'; import { message } from 'antd'; import ProForm, { ProFormText, ProFormCaptcha } from '@ant-design/pro-form'; import { MobileTwoTone, MailTwoTone } from '@ant-design/icons'; import { sendSmsCode } from '@/services/login'; const Login = (props) => { const [countDown, handleCountDown] = useState(5); const { dispatch } = props; const [form] = ProForm.useForm(); return ( <div style={{ width: 330, margin: 'auto', }} > <ProForm form={form} submitter={{ searchConfig: { submitText: '登錄', }, render: (_, dom) => dom.pop(), submitButtonProps: { size: 'large', style: { width: '100%', }, }, onSubmit: async () => { const fieldsValue = await form.validateFields(); console.log(fieldsValue); await dispatch({ type: 'login/login', payload: { username: fieldsValue.mobile, sms_code: fieldsValue.code }, }); }, }} > <ProFormText fieldProps={{ size: 'large', prefix: <MobileTwoTone />, }} name="mobile" placeholder="請(qǐng)輸入手機(jī)號(hào)" rules={[ { required: true, message: '請(qǐng)輸入手機(jī)號(hào)', }, { pattern: new RegExp(/^1[3-9]\d{9}$/, 'g'), message: '手機(jī)號(hào)格式不正確', }, ]} /> <ProFormCaptcha fieldProps={{ size: 'large', prefix: <MailTwoTone />, }} countDown={countDown} captchaProps={{ size: 'large', }} name="code" rules={[ { required: true, message: '請(qǐng)輸入驗(yàn)證碼!', }, ]} placeholder="請(qǐng)輸入驗(yàn)證碼" onGetCaptcha={async (mobile) => { if (!form.getFieldValue('mobile')) { message.error('請(qǐng)先輸入手機(jī)號(hào)'); return; } let m = form.getFieldsError(['mobile']); if (m[0].errors.length > 0) { message.error(m[0].errors[0]); return; } let response = await sendSmsCode(mobile); if (response.code === 10000) message.success('驗(yàn)證碼發(fā)送成功!'); else message.error(response.message); }} /> </ProForm> </div> ); }; export default connect()(Login);
請(qǐng)求驗(yàn)證碼和登錄的 service (src/services/login.js)
import request from '@/utils/request'; export async function login(params) { return request('/api/v1/login', { method: 'POST', data: params, }); } export async function sendSmsCode(mobile) { return request(`/api/v1/send/smscode/${mobile}`, { method: 'GET', }); }
處理登錄的 model (src/models/login.js)
import { stringify } from 'querystring'; import { history } from 'umi'; import { login } from '@/services/login'; import { getPageQuery } from '@/utils/utils'; import { message } from 'antd'; import md5 from 'md5'; const Model = { namespace: 'login', status: '', loginType: '', state: { token: '', }, effects: { *login({ payload }, { call, put }) { payload.client = 'admin'; // payload.password = md5(payload.password); const response = yield call(login, payload); if (response.code !== 10000) { message.error(response.message); return; } // set token to local storage if (window.localStorage) { window.localStorage.setItem('jwt-token', response.data.token); } yield put({ type: 'changeLoginStatus', payload: { data: response.data, status: response.status, loginType: response.loginType }, }); // Login successfully const urlParams = new URL(window.location.href); const params = getPageQuery(); let { redirect } = params; console.log(redirect); if (redirect) { const redirectUrlParams = new URL(redirect); if (redirectUrlParams.origin === urlParams.origin) { redirect = redirect.substr(urlParams.origin.length); if (redirect.match(/^\/.*#/)) { redirect = redirect.substr(redirect.indexOf('#') + 1); } } else { window.location.href = '/home'; } } history.replace(redirect || '/home'); }, logout() { const { redirect } = getPageQuery(); // Note: There may be security issues, please note window.localStorage.removeItem('jwt-token'); if (window.location.pathname !== '/user/login' && !redirect) { history.replace({ pathname: '/user/login', search: stringify({ redirect: window.location.href, }), }); } }, }, reducers: { changeLoginStatus(state, { payload }) { return { ...state, token: payload.data.token, status: payload.status, loginType: payload.loginType, }; }, }, }; export default Model;
后端
后端主要就 2 個(gè)接口, 一個(gè)處理短信驗(yàn)證碼的發(fā)送, 一個(gè)處理登錄驗(yàn)證
路由的代碼片段:
apiV1.POST("/login", authMiddleware.LoginHandler) apiV1.GET("/send/smscode/:mobile", controller.SendSmsCode)
短信驗(yàn)證碼的處理
- 短信驗(yàn)證碼的處理有幾點(diǎn)需要注意:
- 生成隨機(jī)的固定長(zhǎng)度的數(shù)字調(diào)用短信接口發(fā)送驗(yàn)證碼保存已經(jīng)驗(yàn)證碼, 以備驗(yàn)證用
- 生成固定長(zhǎng)度的數(shù)字
以下代碼生成 6 位的數(shù)字, 隨機(jī)數(shù)不足 6 位前面補(bǔ) 0
r := rand.New(rand.NewSource(time.Now().UnixNano())) code := fmt.Sprintf("%06v", r.Int31n(1000000))
調(diào)用短信接口
這個(gè)簡(jiǎn)單, 根據(jù)購(gòu)買的短信接口的說明調(diào)用即可
保存已經(jīng)驗(yàn)證碼, 以備驗(yàn)證用
這里需要注意的是驗(yàn)證碼要有個(gè)過期時(shí)間, 不能一個(gè)驗(yàn)證碼一直可用.
臨時(shí)存儲(chǔ)的驗(yàn)證碼可以放在數(shù)據(jù)庫(kù), 也可以使用 redis 之類的 KV 存儲(chǔ), 這里為了簡(jiǎn)單, 直接在內(nèi)存中使用 map 結(jié)構(gòu)來(lái)存儲(chǔ)驗(yàn)證碼
package util import ( "fmt" "math/rand" "sync" "time" ) type loginItem struct { smsCode string smsCodeExpire int64 } type LoginMap struct { m map[string]*loginItem l sync.Mutex } var lm *LoginMap func InitLoginMap(resetTime int64, loginTryMax int) { lm = &LoginMap{ m: make(map[string]*loginItem), } } func GenSmsCode(key string) string { r := rand.New(rand.NewSource(time.Now().UnixNano())) code := fmt.Sprintf("%06v", r.Int31n(1000000)) if _, ok := lm.m[key]; !ok { lm.m[key] = &loginItem{} } v := lm.m[key] v.smsCode = code v.smsCodeExpire = time.Now().Unix() + 600 // 驗(yàn)證碼10分鐘過期 return code } func CheckSmsCode(key, code string) error { if _, ok := lm.m[key]; !ok { return fmt.Errorf("驗(yàn)證碼未發(fā)送") } v := lm.m[key] // 驗(yàn)證碼是否過期 if time.Now().Unix() > v.smsCodeExpire { return fmt.Errorf("驗(yàn)證碼(%s)已經(jīng)過期", code) } // 驗(yàn)證碼是否正確 if code != v.smsCode { return fmt.Errorf("驗(yàn)證碼(%s)不正確", code) } return nil }
登錄驗(yàn)證
登錄驗(yàn)證的代碼比較簡(jiǎn)單, 就是先調(diào)用上面的 CheckSmsCode 方法驗(yàn)證是否合法.
驗(yàn)證通過之后, 根據(jù)手機(jī)號(hào)獲取用戶信息, 再生成 jwt-token 返回給客戶端即可.
FAQ
antd 版本問題
使用 antd pro 的 ProForm 要使用 antd 的最新版本, 最好 >= v4.8, 否則前端組件會(huì)有不兼容的錯(cuò)誤.
可以優(yōu)化的點(diǎn)
上面實(shí)現(xiàn)的比較粗糙, 還有以下方面可以繼續(xù)優(yōu)化:
驗(yàn)證碼需要控制頻繁發(fā)送, 畢竟發(fā)送短信需要費(fèi)用驗(yàn)證碼直接在內(nèi)存中, 系統(tǒng)重啟后會(huì)丟失, 可以考慮放在 redis 之類的存儲(chǔ)中
到此這篇關(guān)于基于 antd pro 的短信驗(yàn)證碼登錄功能(流程分析)的文章就介紹到這了,更多相關(guān)antd pro 驗(yàn)證碼登錄內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
bootstrap滾動(dòng)監(jiān)控器使用方法解析
這篇文章主要為大家詳細(xì)解析了bootstrap滾動(dòng)監(jiān)控器使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01uniapp項(xiàng)目?jī)?yōu)化方式及建議
性能優(yōu)化自古以來(lái)就是重中之重,本文關(guān)于uniapp項(xiàng)目?jī)?yōu)化方式最全整理,會(huì)根據(jù)開發(fā)情況進(jìn)行補(bǔ)充,感興趣的可以了解一下2021-08-08javascript DOM編程實(shí)例(智播客學(xué)習(xí))
最近一直在努力學(xué)習(xí)DOM編程這塊,這是目前成就感最強(qiáng)烈的一塊了,畢老師很認(rèn)真的給我們講解了相關(guān)知識(shí),并在網(wǎng)上找了很多做的非常棒的網(wǎng)頁(yè)作為例程給我們進(jìn)行講解2009-11-11javascript判斷網(wǎng)頁(yè)是關(guān)閉還是刷新
本篇文章給大家介紹js判斷網(wǎng)頁(yè)是關(guān)閉還是刷新,實(shí)現(xiàn)原理就是通過離開頁(yè)面行為時(shí)間onunload觸發(fā)時(shí)間去檢測(cè)此時(shí)的瀏覽器的窗口大小,根據(jù)大小由此判斷用戶是刷新,跳轉(zhuǎn)或是關(guān)閉行為程序,需要的朋友可以參考下本文2015-09-09javascript 循環(huán)調(diào)用示例介紹
循環(huán)調(diào)用,如果已經(jīng)獲取到了結(jié)果,則退出循環(huán),下面有個(gè)不錯(cuò)的示例,感興趣的朋友可以嘗試操作下2013-11-11js Math數(shù)學(xué)簡(jiǎn)單使用操作示例
這篇文章主要介紹了js Math數(shù)學(xué)簡(jiǎn)單使用,結(jié)合實(shí)例形式分析了js Math數(shù)學(xué)相關(guān)函數(shù)的基本用法與操作注意事項(xiàng),需要的朋友可以參考下2020-03-03