基于 antd pro 的短信驗證碼登錄功能(流程分析)
概要
最近使用 antd pro 開發(fā)項目時遇到個新的需求, 就是在登錄界面通過短信驗證碼來登錄, 不使用之前的用戶名密碼之類登錄方式.
這種方式雖然增加了額外的短信費用, 但是對于安全性確實提高了不少. antd 中并沒有自帶能夠倒計時的按鈕,
但是 antd pro 的 ProForm components 中倒是提供了針對短信驗證碼相關(guān)的組件.
組件說明可參見: https://procomponents.ant.design/components/form
整體流程
通過短信驗證碼登錄的流程很簡單:
- 請求短信驗證碼(客戶端)
- 生成短信驗證碼, 并設(shè)置驗證碼的過期時間(服務(wù)端)
- 調(diào)用短信接口發(fā)送驗證碼(服務(wù)端)
- 根據(jù)收到的短信驗證碼登錄(客戶端)
- 驗證手機(jī)號和短信驗證碼, 驗證通過之后發(fā)行 jwt-token(服務(wù)端)
前端
頁面代碼
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="請輸入手機(jī)號"
rules={[
{
required: true,
message: '請輸入手機(jī)號',
},
{
pattern: new RegExp(/^1[3-9]\d{9}$/, 'g'),
message: '手機(jī)號格式不正確',
},
]}
/>
<ProFormCaptcha
fieldProps={{
size: 'large',
prefix: <MailTwoTone />,
}}
countDown={countDown}
captchaProps={{
size: 'large',
}}
name="code"
rules={[
{
required: true,
message: '請輸入驗證碼!',
},
]}
placeholder="請輸入驗證碼"
onGetCaptcha={async (mobile) => {
if (!form.getFieldValue('mobile')) {
message.error('請先輸入手機(jī)號');
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('驗證碼發(fā)送成功!');
else message.error(response.message);
}}
/>
</ProForm>
</div>
);
};
export default connect()(Login);
請求驗證碼和登錄的 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 個接口, 一個處理短信驗證碼的發(fā)送, 一個處理登錄驗證
路由的代碼片段:
apiV1.POST("/login", authMiddleware.LoginHandler)
apiV1.GET("/send/smscode/:mobile", controller.SendSmsCode)
短信驗證碼的處理
- 短信驗證碼的處理有幾點需要注意:
- 生成隨機(jī)的固定長度的數(shù)字調(diào)用短信接口發(fā)送驗證碼保存已經(jī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)用短信接口
這個簡單, 根據(jù)購買的短信接口的說明調(diào)用即可
保存已經(jīng)驗證碼, 以備驗證用
這里需要注意的是驗證碼要有個過期時間, 不能一個驗證碼一直可用.
臨時存儲的驗證碼可以放在數(shù)據(jù)庫, 也可以使用 redis 之類的 KV 存儲, 這里為了簡單, 直接在內(nèi)存中使用 map 結(jié)構(gòu)來存儲驗證碼
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 // 驗證碼10分鐘過期
return code
}
func CheckSmsCode(key, code string) error {
if _, ok := lm.m[key]; !ok {
return fmt.Errorf("驗證碼未發(fā)送")
}
v := lm.m[key]
// 驗證碼是否過期
if time.Now().Unix() > v.smsCodeExpire {
return fmt.Errorf("驗證碼(%s)已經(jīng)過期", code)
}
// 驗證碼是否正確
if code != v.smsCode {
return fmt.Errorf("驗證碼(%s)不正確", code)
}
return nil
}
登錄驗證
登錄驗證的代碼比較簡單, 就是先調(diào)用上面的 CheckSmsCode 方法驗證是否合法.
驗證通過之后, 根據(jù)手機(jī)號獲取用戶信息, 再生成 jwt-token 返回給客戶端即可.
FAQ
antd 版本問題
使用 antd pro 的 ProForm 要使用 antd 的最新版本, 最好 >= v4.8, 否則前端組件會有不兼容的錯誤.
可以優(yōu)化的點
上面實現(xiàn)的比較粗糙, 還有以下方面可以繼續(xù)優(yōu)化:
驗證碼需要控制頻繁發(fā)送, 畢竟發(fā)送短信需要費用驗證碼直接在內(nèi)存中, 系統(tǒng)重啟后會丟失, 可以考慮放在 redis 之類的存儲中
到此這篇關(guān)于基于 antd pro 的短信驗證碼登錄功能(流程分析)的文章就介紹到這了,更多相關(guān)antd pro 驗證碼登錄內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javascript DOM編程實例(智播客學(xué)習(xí))
最近一直在努力學(xué)習(xí)DOM編程這塊,這是目前成就感最強(qiáng)烈的一塊了,畢老師很認(rèn)真的給我們講解了相關(guān)知識,并在網(wǎng)上找了很多做的非常棒的網(wǎng)頁作為例程給我們進(jìn)行講解2009-11-11
javascript判斷網(wǎng)頁是關(guān)閉還是刷新
本篇文章給大家介紹js判斷網(wǎng)頁是關(guān)閉還是刷新,實現(xiàn)原理就是通過離開頁面行為時間onunload觸發(fā)時間去檢測此時的瀏覽器的窗口大小,根據(jù)大小由此判斷用戶是刷新,跳轉(zhuǎn)或是關(guān)閉行為程序,需要的朋友可以參考下本文2015-09-09
javascript 循環(huán)調(diào)用示例介紹
循環(huán)調(diào)用,如果已經(jīng)獲取到了結(jié)果,則退出循環(huán),下面有個不錯的示例,感興趣的朋友可以嘗試操作下2013-11-11

