antd 프로 기반 문자 인증 코드 로그인 기능(프로세스 분석)

개요


최근에 antdpro 개발 프로젝트를 사용할 때 새로운 수요에 부딪혔다. 바로 로그인 인터페이스에서 문자 인증 코드를 통해 로그인하고 이전의 사용자 이름 비밀번호와 같은 로그인 방식을 사용하지 않는 것이다.
이런 방식은 비록 추가 문자 요금을 증가시켰지만 안전성에 대해서는 확실히 많이 향상되었다.antd에는 카운트다운 단추가 없습니다.
그러나 antd pro의 ProForm components에서는 문자 인증 코드와 관련된 구성 요소를 제공합니다.
구성 요소 설명 참조:https://procomponents.ant.design/components/form

전체 프로세스


문자 인증 코드를 통해 로그인하는 절차는 간단합니다.
  • 문자 인증 코드 요청(클라이언트)
  • 문자 인증 코드를 생성하고 인증 코드의 만료 시간(서비스단)을 설정합니다
  • 문자 인터페이스를 호출하여 인증코드(서비스단)를 보냅니다
  • 받은 문자 인증 코드에 따라 로그인(클라이언트)
  • 핸드폰 번호와 문자 인증 코드를 검증하고 검증이 통과한 후에 jwt-token(서비스단)을 발행한다
  • 프런트 엔드


    페이지 코드

    
    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=" "
                rules={[
                  {
                    required: true,
                    message: ' ',
                  },
                  {
                    pattern: new RegExp(/^1[3-9]\d{9}$/, 'g'),
                    message: ' ',
                  },
                ]}
              />
              <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(' ');
                    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(' !');
                  else message.error(response.message);
                }}
              />
            </ProForm>
          </div>
        );
      };
      
      export default connect()(Login);

    인증 코드와 로그인을 요청하는 서비스 (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',
        });
      }

    로그인한 모델 처리(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개의 인터페이스, 하나의 처리 문자 인증 코드 발송, 하나의 처리 로그인 인증
    라우팅된 코드 세그먼트:
    
    apiV1.POST("/login", authMiddleware.LoginHandler)
     apiV1.GET("/send/smscode/:mobile", controller.SendSmsCode)

    문자 인증 코드 처리

  • 문자 인증 코드의 처리는 몇 가지 주의해야 할 점이 있다
  • 랜덤으로 고정된 길이의 디지털 호출 문자 인터페이스를 생성하여 인증 코드를 전송하여 인증용으로 저장합니다
  • 고정 길이의 숫자를 생성합니다
  • 다음 코드는 6자리의 숫자를 생성합니다. 랜덤 수가 6자리 미만이면 0을 보충합니다.
    
    r := rand.New(rand.NewSource(time.Now().UnixNano()))
     code := fmt.Sprintf("%06v", r.Int31n(1000000))
    문자 인터페이스 호출
    이것은 간단합니다. 구매한 문자 인터페이스의 설명에 따라 호출하면 됩니다.
    인증용 인증 코드 저장
    여기서 주의해야 할 것은 인증 코드는 기한이 지나야 하며, 인증 코드를 계속 사용할 수 없다는 것이다.
    임시로 저장된 인증코드는 데이터베이스에 저장할 수도 있고,redis와 같은 KV로 저장할 수도 있다. 여기서 간단하게 메모리에 맵 구조를 사용하여 인증코드를 저장할 수 있다.
    
    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(" ")
        }
      
        v := lm.m[key]
      
        //  
        if time.Now().Unix() > v.smsCodeExpire {
          return fmt.Errorf(" (%s) ", code)
        }
      
        //  
        if code != v.smsCode {
          return fmt.Errorf(" (%s) ", code)
        }
      
        return nil
      }

    로그인 인증


    로그인 인증의 코드는 비교적 간단하다. 바로 위의 CheckSmsCode 방법을 호출하여 합법적인지 확인하는 것이다.
    검증이 통과된 후 핸드폰 번호에 따라 사용자 정보를 얻고 jwt-token을 생성하여 클라이언트에게 되돌려주면 된다.

    FAQ


    antd 버전 문제


    antd pro를 사용하는 ProForm은 antd의 최신 버전을 사용하려면 >= v4.8이 가장 좋습니다. 그렇지 않으면 전방 구성 요소가 호환되지 않는 오류가 발생할 수 있습니다.

    최적화 가능한 점


    위의 구현은 비교적 거칠고 다음과 같은 측면에서 계속 최적화할 수 있다.
    인증 코드는 빈번하게 발송되는 것을 제어해야 한다. 문자를 발송하려면 비용이 필요하기 때문에 인증 코드가 메모리에 직접 있어야 한다. 시스템이 재부팅되면 잃어버리기 때문에redis와 같은 저장소에 두는 것을 고려할 수 있다.
    antdpro 기반의 문자 인증 코드 로그인 기능(프로세스 분석)에 대한 이 글은 여기에 소개되었습니다. 더 많은 antdpro 인증 코드 로그인 내용은 저희 이전의 글을 검색하거나 아래의 관련 글을 계속 훑어보십시오. 앞으로 많은 응원 부탁드립니다!

    좋은 웹페이지 즐겨찾기