2020年11月30日星期一

基于 antd pro 的短信验证码登录

  • 概要
  • 整体流程
  • 前端
    • 页面代码
    • 请求验证码和登录的 service (src/services/login.js)
    • 处理登录的 model (src/models/login.js)
  • 后端
    • 短信验证码的处理
      • 生成固定长度的数字
      • 调用短信接口
      • 保存已经验证码, 以备验证用
    • 登录验证
  • FAQ
    • antd 版本问题
    • 可以优化的点

概要

最近使用 antd pro 开发项目时遇到个新的需求, 就是在登录界面通过短信验证码来登录, 不使用之前的用户名密码之类登录方式.

这种方式虽然增加了额外的短信费用, 但是对于安全性确实提高了不少. antd 中并没有自带能够倒计时的按钮,
但是 antd pro 的 ProForm components 中倒是提供了针对短信验证码相关的组件.
组件说明可参见: https://procomponents.ant.design/components/form

整体流程

通过短信验证码登录的流程很简单:

  1. 请求短信验证码(客户端)
  2. 生成短信验证码, 并设置验证码的过期时间(服务端)
  3. 调用短信接口发送验证码(服务端)
  4. 根据收到的短信验证码登录(客户端)
  5. 验证手机号和短信验证码, 验证通过之后发行 jwt-token(服务端)

前端

页面代码

 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  <div14  style={{15   width: 330,16   margin: 'auto',17  }}18  >19  <ProForm20   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   <ProFormText43   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   <ProFormCaptcha61   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);

请求验证码和登录的 service (src/services/login.js)

 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 }

处理登录的 model (src/models/login.js)

 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 storage26  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 successfully34 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 note58 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 个接口, 一个处理短信验证码的发送, 一个处理登录验证

路由的代码片段:

1 apiV1.POST("/login", authMiddleware.LoginHandler)2 apiV1.GET("/send/smscode/:mobile", controller.SendSmsCode)

短信验证码的处理

短信验证码的处理有几点需要注意:

  1. 生成随机的固定长度的数字
  2. 调用短信接口发送验证码
  3. 保存已经验证码, 以备验证用

生成固定长度的数字

以下代码生成 6 位的数字, 随机数不足 6 位前面补 0

1 r := rand.New(rand.NewSource(time.Now().UnixNano()))2 code := fmt.Sprintf("%06v", r.Int31n(1000000))

调用短信接口

这个简单, 根据购买的短信接口的说明调用即可

保存已经验证码, 以备验证用

这里需要注意的是验证码要有个过期时间, 不能一个验证码一直可用.
临时存储的验证码可以放在数据库, 也可以使用 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  string12 smsCodeExpire int6413 }14 15 type LoginMap struct {16 m   map[string]*loginItem17 l   sync.Mutex18 }19 20 var lm *LoginMap21 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 = code38 v.smsCodeExpire = time.Now().Unix() + 600 // 验证码10分钟过期39 40 return code41 }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 nil61 }

登录验证

登录验证的代码比较简单, 就是先调用上面的 CheckSmsCode 方法验证是否合法.
验证通过之后, 根据手机号获取用户信息, 再生成 jwt-token 返回给客户端即可.

FAQ

antd 版本问题

使用 antd pro 的 ProForm 要使用 antd 的最新版本, 最好 >= v4.8, 否则前端组件会有不兼容的错误.

可以优化的点

上面实现的比较粗糙, 还有以下方面可以继续优化:

  1. 验证码需要控制频繁发送, 毕竟发送短信需要费用
  2. 验证码直接在内存中, 系统重启后会丢失, 可以考虑放在 redis 之类的存储中








原文转载:http://www.shaoqun.com/a/493357.html

tiki:https://www.ikjzd.com/w/2053

易佰:https://www.ikjzd.com/w/1482

usps国际快递查询:https://www.ikjzd.com/w/513


概要整体流程前端页面代码请求验证码和登录的service(src/services/login.js)处理登录的model(src/models/login.js)后端短信验证码的处理生成固定长度的数字调用短信接口保存已经验证码,以备验证用登录验证FAQantd版本问题可以优化的点概要最近使用antdpro开发项目时遇到个新的需求,就是在登录界面通过短信验证码来登录,不使用之前的用户名密码之类登录
tiki:tiki
focalprice:focalprice
【韩国旅游签证申请表】--韩国旅游签证怎么办:【韩国旅游签证申请表】--韩国旅游签证怎么办
河源天上人间温泉怎么样?:河源天上人间温泉怎么样?
2020东部华侨城元旦活动?元旦深圳东部华侨城门票优惠价格:2020东部华侨城元旦活动?元旦深圳东部华侨城门票优惠价格

没有评论:

发表评论