最近使用 antd pro 開發項目時遇到個新的需求, 就是在登陸界面經過短信驗證碼來登陸, 不使用以前的用戶名密碼之類登陸方式.javascript
這種方式雖然增長了額外的短信費用, 可是對於安全性確實提升了很多. antd 中並無自帶可以倒計時的按鈕,
可是 antd pro 的 ProForm components 中卻是提供了針對短信驗證碼相關的組件.
組件說明可參見: https://procomponents.ant.design/components/form前端
經過短信驗證碼登陸的流程很簡單:java
1 import React, { useState } from 'react'; 2 import { connect } from 'umi'; 3 import { message } from 'antd'; 4 import ProForm, { ProFormText, ProFormCaptcha } from '@ant-design/pro-form'; 5 import { MobileTwoTone, MailTwoTone } from '@ant-design/icons'; 6 import { sendSmsCode } from '@/services/login'; 7 8 const Login = (props) => { 9 const [countDown, handleCountDown] = useState(5); 10 const { dispatch } = props; 11 const [form] = ProForm.useForm(); 12 return ( 13 <div 14 style={{ 15 width: 330, 16 margin: 'auto', 17 }} 18 > 19 <ProForm 20 form={form} 21 submitter={{ 22 searchConfig: { 23 submitText: '登陸', 24 }, 25 render: (_, dom) => dom.pop(), 26 submitButtonProps: { 27 size: 'large', 28 style: { 29 width: '100%', 30 }, 31 }, 32 onSubmit: async () => { 33 const fieldsValue = await form.validateFields(); 34 console.log(fieldsValue); 35 await dispatch({ 36 type: 'login/login', 37 payload: { username: fieldsValue.mobile, sms_code: fieldsValue.code }, 38 }); 39 }, 40 }} 41 > 42 <ProFormText 43 fieldProps={{ 44 size: 'large', 45 prefix: <MobileTwoTone />, 46 }} 47 name="mobile" 48 placeholder="請輸入手機號" 49 rules={[ 50 { 51 required: true, 52 message: '請輸入手機號', 53 }, 54 { 55 pattern: new RegExp(/^1[3-9]\d{9}$/, 'g'), 56 message: '手機號格式不正確', 57 }, 58 ]} 59 /> 60 <ProFormCaptcha 61 fieldProps={{ 62 size: 'large', 63 prefix: <MailTwoTone />, 64 }} 65 countDown={countDown} 66 captchaProps={{ 67 size: 'large', 68 }} 69 name="code" 70 rules={[ 71 { 72 required: true, 73 message: '請輸入驗證碼!', 74 }, 75 ]} 76 placeholder="請輸入驗證碼" 77 onGetCaptcha={async (mobile) => { 78 if (!form.getFieldValue('mobile')) { 79 message.error('請先輸入手機號'); 80 return; 81 } 82 let m = form.getFieldsError(['mobile']); 83 if (m[0].errors.length > 0) { 84 message.error(m[0].errors[0]); 85 return; 86 } 87 let response = await sendSmsCode(mobile); 88 if (response.code === 10000) message.success('驗證碼發送成功!'); 89 else message.error(response.message); 90 }} 91 /> 92 </ProForm> 93 </div> 94 ); 95 }; 96 97 export default connect()(Login);
1 import request from '@/utils/request'; 2 3 export async function login(params) { 4 return request('/api/v1/login', { 5 method: 'POST', 6 data: params, 7 }); 8 } 9 10 export async function sendSmsCode(mobile) { 11 return request(`/api/v1/send/smscode/${mobile}`, { 12 method: 'GET', 13 }); 14 }
1 import { stringify } from 'querystring'; 2 import { history } from 'umi'; 3 import { login } from '@/services/login'; 4 import { getPageQuery } from '@/utils/utils'; 5 import { message } from 'antd'; 6 import md5 from 'md5'; 7 8 const Model = { 9 namespace: 'login', 10 status: '', 11 loginType: '', 12 state: { 13 token: '', 14 }, 15 effects: { 16 *login({ payload }, { call, put }) { 17 payload.client = 'admin'; 18 // payload.password = md5(payload.password); 19 const response = yield call(login, payload); 20 if (response.code !== 10000) { 21 message.error(response.message); 22 return; 23 } 24 25 // set token to local storage 26 if (window.localStorage) { 27 window.localStorage.setItem('jwt-token', response.data.token); 28 } 29 30 yield put({ 31 type: 'changeLoginStatus', 32 payload: { data: response.data, status: response.status, loginType: response.loginType }, 33 }); // Login successfully 34 35 const urlParams = new URL(window.location.href); 36 const params = getPageQuery(); 37 let { redirect } = params; 38 39 console.log(redirect); 40 if (redirect) { 41 const redirectUrlParams = new URL(redirect); 42 43 if (redirectUrlParams.origin === urlParams.origin) { 44 redirect = redirect.substr(urlParams.origin.length); 45 46 if (redirect.match(/^\/.*#/)) { 47 redirect = redirect.substr(redirect.indexOf('#') + 1); 48 } 49 } else { 50 window.location.href = '/home'; 51 } 52 } 53 history.replace(redirect || '/home'); 54 }, 55 56 logout() { 57 const { redirect } = getPageQuery(); // Note: There may be security issues, please note 58 59 window.localStorage.removeItem('jwt-token'); 60 if (window.location.pathname !== '/user/login' && !redirect) { 61 history.replace({ 62 pathname: '/user/login', 63 search: stringify({ 64 redirect: window.location.href, 65 }), 66 }); 67 } 68 }, 69 }, 70 reducers: { 71 changeLoginStatus(state, { payload }) { 72 return { 73 ...state, 74 token: payload.data.token, 75 status: payload.status, 76 loginType: payload.loginType, 77 }; 78 }, 79 }, 80 }; 81 export default Model;
後端主要就 2 個接口, 一個處理短信驗證碼的發送, 一個處理登陸驗證react
路由的代碼片斷:redis
1 apiV1.POST("/login", authMiddleware.LoginHandler) 2 apiV1.GET("/send/smscode/:mobile", controller.SendSmsCode)
短信驗證碼的處理有幾點須要注意:數據庫
如下代碼生成 6 位的數字, 隨機數不足 6 位前面補 0後端
1 r := rand.New(rand.NewSource(time.Now().UnixNano())) 2 code := fmt.Sprintf("%06v", r.Int31n(1000000))
這個簡單, 根據購買的短信接口的說明調用便可api
這裏須要注意的是驗證碼要有個過時時間, 不能一個驗證碼一直可用.
臨時存儲的驗證碼能夠放在數據庫, 也可使用 redis 之類的 KV 存儲, 這裏爲了簡單, 直接在內存中使用 map 結構來存儲驗證碼安全
1 package util 2 3 import ( 4 "fmt" 5 "math/rand" 6 "sync" 7 "time" 8 ) 9 10 type loginItem struct { 11 smsCode string 12 smsCodeExpire int64 13 } 14 15 type LoginMap struct { 16 m map[string]*loginItem 17 l sync.Mutex 18 } 19 20 var lm *LoginMap 21 22 func InitLoginMap(resetTime int64, loginTryMax int) { 23 lm = &LoginMap{ 24 m: make(map[string]*loginItem), 25 } 26 } 27 28 func GenSmsCode(key string) string { 29 r := rand.New(rand.NewSource(time.Now().UnixNano())) 30 code := fmt.Sprintf("%06v", r.Int31n(1000000)) 31 32 if _, ok := lm.m[key]; !ok { 33 lm.m[key] = &loginItem{} 34 } 35 36 v := lm.m[key] 37 v.smsCode = code 38 v.smsCodeExpire = time.Now().Unix() + 600 // 驗證碼10分鐘過時 39 40 return code 41 } 42 43 func CheckSmsCode(key, code string) error { 44 if _, ok := lm.m[key]; !ok { 45 return fmt.Errorf("驗證碼未發送") 46 } 47 48 v := lm.m[key] 49 50 // 驗證碼是否過時 51 if time.Now().Unix() > v.smsCodeExpire { 52 return fmt.Errorf("驗證碼(%s)已通過期", code) 53 } 54 55 // 驗證碼是否正確 56 if code != v.smsCode { 57 return fmt.Errorf("驗證碼(%s)不正確", code) 58 } 59 60 return nil 61 }
登陸驗證的代碼比較簡單, 就是先調用上面的 CheckSmsCode 方法驗證是否合法.
驗證經過以後, 根據手機號獲取用戶信息, 再生成 jwt-token 返回給客戶端便可.antd
使用 antd pro 的 ProForm 要使用 antd 的最新版本, 最好 >= v4.8, 不然前端組件會有不兼容的錯誤.
上面實現的比較粗糙, 還有如下方面能夠繼續優化: