若依-Vue(前后端分离)集成阿里云短信服务并实现手机验证码登录

2,327 阅读10分钟

一.前言

本人也是一名学习者,在编写过程中也借鉴了诸多博主的文章学习,本人写文章主要是防止以后忘记,用于复习,如果能够帮助到大家也倍感荣幸,同时文章中如果觉得侵权的地方请联系删除,本文只做学习交流使用。如果错误或更好的实现方式欢迎指出更正。

二.集成阿里云短信服务

2.1 集成前简单说一下

我们想要发送短信验证码,是需要国内的三大运营服务商来提供的,我们个人去对接会比较麻烦,所以我们一般会选择已经对接好的服务产品,就比如我们这里选用的阿里云短信服务。

那么要使用这个产品就要去购买它,当然这里阿里云提供了免费试用,大家首先要去购买这个产品,大家需要去阿里云官网注册账号和购买他的阿里云短信服务。这里博主就不一步一步带大家去操作了。

这方面的文章也有很多,大家跟着学习一下就OK

重点: 这里的重点就是要拿取到 ACCESS_KEY_IDACCESS_KEY_SECRET 这个是你的身份凭证,有这个凭证在后端编写SDK的时候才能去访问阿里云提供的接口。

2.2 购买产品后正式开始后端集成

2.2.1 引入短信服务坐标代码

这里我选择集成到common模块下,选择 common 模块的 pom.xml 文件下引入下面的坐标。

<!-- 阿里云短信服务 -->
<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>dysmsapi20170525</artifactId>
    <version>3.1.0</version>
</dependency>

2.2.2 编写短信发送工具类

在这个路径下 com.quickclean.common.utils 下编写工具类。我这里新建了一个alicloud包用来存放阿里云的各种工具类。

注意: 这里的 quickclean 是我自己使用若依项目生成器修改过的,后面涉及到的代码请注意修改

image.png

下面的代码请注意,这个工具类中的一些变量我抽取出来单独写了一个常量类,方便以后修改模板和节点。请大家按照自己的修改更改,同时这里的凭证我是直接从本地计算机读取的,没有以明文的方式写入。

SmsUtil工具类代码

package com.quickclean.common.utils.alicloud;

import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.tea.*;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.models.RuntimeOptions;
import com.quickclean.common.constant.SmsConstants;

import java.util.HashMap;
import java.util.Map;

import static com.aliyun.teautil.Common.assertAsString;

/**
 * 阿里云短信服务工具类
 */
public class SmsUtil {
    public static Client createClient() throws Exception {
        Config config = new Config()
                // 配置 AccessKey ID,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
                .setAccessKeyId(System.getenv(SmsConstants.ACCESS_KEY_ID))
                // 配置 AccessKey Secret,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
                .setAccessKeySecret(System.getenv(SmsConstants.ACCESS_KEY_SECRET));

        // 配置 Endpoint
        config.endpoint = SmsConstants.END_POINT;

        return new Client(config);
    }

    /**
     * 发送验证码短信
     * @param phone
     * @param code
     * @return
     * @throws Exception
     */
    public static Map<String, Object> sendMsg(String phone, String code) throws Exception {
        // 所以构造Map类型的返回
        HashMap<String, Object> sendSmsResponseMap = new HashMap<>();
        try {
            // 初始化请求客户端
            Client client = SmsUtil.createClient();

            // 构造请求对象,请填入请求参数值
            SendSmsRequest sendSmsRequest = new SendSmsRequest()
                    .setPhoneNumbers(phone)
                    .setSignName(SmsConstants.SIGN_NAME)
                    .setTemplateCode(SmsConstants.TEMPLATE_CODE)
                    .setTemplateParam("{"code":""+code+""}");

            // 获取响应对象,响应包含服务端响应的 body 和 headers
            SendSmsResponse sendSmsResponse = client.sendSmsWithOptions(sendSmsRequest, new RuntimeOptions());

            // 获取响应体状态码和Body状态码
            Integer statusCode = sendSmsResponse.getStatusCode();
            String responseCode = sendSmsResponse.getBody().getCode();

            // 由于framework模块没有导入阿里云短信服务坐标-无法导入SendSmsResponse
            sendSmsResponseMap.put("statusCode", statusCode);
            sendSmsResponseMap.put("bodyCode", responseCode);
        } catch (TeaException error) {
            // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
            // 错误 message
            System.out.println(error.getMessage());
            // 诊断地址
            System.out.println(error.getData().get("Recommend"));
            assertAsString(error.message);
        } catch (Exception _error) {
            TeaException error = new TeaException(_error.getMessage(), _error);
            // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
            // 错误 message
            System.out.println(error.getMessage());
            // 诊断地址
            System.out.println("诊断地址" + error.getData().get("Recommend"));
            assertAsString(error.message);
        }
        return sendSmsResponseMap;
    }
}

到这里阿里云的短信服务的集成工作就算是完成了。可以在这里执行一下工具类的 sendMsg 方法查看能否正常收到短信。接下来我们完成接口编写的工作。

三.接口编写工作

3.1 接口编写前言

这里需要给大家说明一下,由于若依使用了 Spring Security 安全框架,我们登录的时候要先交给安全框架进行验证,同时我们编写的接口也是需要交给安全框架进行管理的。

我们都知道若依使用的是传统的账号密码的登录方式,在安全框架中也是默认对这种安全方式进行认证。当我们使用手机验证码进行登录的时候这个安全框架就没办法按照我们希望的方式进行认证,所以我们需要进行自定义认证。

对于这个安全框架的编写过程中的执行机制,这里我就不展开叙述了,大家参考下面的文章进行学习。 SpringBoot集成Spring Security(7)——认证流程_springboot整合springsecurity若依-CSDN博客

所以在编写接口之前我们先实现自定义认证。

3.2 安全框架自定义认证编写

3.2.1 我们需要编写如图的几个类

image.png

3.3.2 自定义认证SmsCodeAuthenticationToken类

package com.quickclean.framework.security.token;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;

import java.util.Collection;

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {



    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    /**
     * 在 UsernamePasswordAuthenticationToken 中该字段代表登录的用户名,
     * 在这里就代表登录的手机号码
     */
    private final Object principal;
    private final Object code;

    /**
     * 构建一个没有鉴权的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal,Object code) {
        super(null);
        this.principal = principal;
        this.code = code;
        setAuthenticated(false);
    }

    /**
     * 构建拥有鉴权的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities, Object code) {
        super(authorities);
        this.principal = principal;
        this.code = code;
        // must use super, as we override
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    public Object getCode() {
        return code;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }

        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
}

3.2.3 SmsCodeAuthenticationProvider类

package com.quickclean.framework.security.provider;

import com.quickclean.framework.security.token.SmsCodeAuthenticationToken;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {


    private UserDetailsService userDetailsService;

    public SmsCodeAuthenticationProvider() {
    }

    public SmsCodeAuthenticationProvider(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
        String mobile = (String) authenticationToken.getPrincipal();
        String code = (String) authenticationToken.getCode();
        UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
        // 此时鉴权成功后,应当重新 new 一个拥有鉴权的 authenticationResult 返回
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities(), code);
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }

}

3.2.4 SmsUserDetailsServiceImpl类

package com.quickclean.framework.web.service;

import com.quickclean.common.core.domain.entity.SysUser;
import com.quickclean.common.core.domain.model.LoginUser;
import com.quickclean.common.enums.UserStatus;
import com.quickclean.common.exception.base.BaseException;
import com.quickclean.common.utils.StringUtils;
import com.quickclean.system.service.ISysUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service("userDetailsByPhonenumber")
public class SmsUserDetailsServiceImpl implements UserDetailsService {

    private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

    @Autowired
    private ISysUserService userService;

    @Autowired
    private SysPermissionService permissionService;


    @Override
    public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {
        SysUser user = userService.selectUserByPhone(phone);
        if (StringUtils.isNull(user))
        {
            log.info("登录手机号:{} 不存在.", phone);
            throw new UsernameNotFoundException("登录手机号:" + phone + " 不存在");
        }
        else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
        {
            log.info("登录用户:{} 已被删除.", phone);
            throw new BaseException("对不起,您的账号:" + phone + " 已被删除");
        }
        else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
        {
            log.info("登录用户:{} 已被停用.", phone);
            throw new BaseException("对不起,您的账号:" + phone + " 已停用");
        }

        return createLoginUser(user);
    }

    public UserDetails createLoginUser(SysUser user)
    {
        return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
    }
}

注意: 这个类一定要注意一点,就是@Service注解一定要指定名称。同时我们接下来还要去修改另外一个认证方法的名称,防止 Spring 不知道使用哪一个实现类。 @Service("userDetailsByPhonenumber")

3.3 相关信息修改

3.3.1 UserDetailsServiceImpl 类修改

image.png

3.3.2 修改SecurityConfig文件

修改注入方式。 image.png

修改身份认证实现 image.png

这个地方的修改是和接口有关,这个两个接口就是我们等会要编写的。先让他允许访问,等下来编写。 image.png

3.4 发送验证码接口和登录验证接口实现

3.4.1 Contrlloer层

/**
 * 发送手机验证码
 *
 * @param mobile 手机验证码登录信息
 * @return 结果
 */
@PostMapping("/sendSms")
public AjaxResult sendSms(@RequestBody String mobile) {
    Integer status = loginService.sendSms(mobile);
    if (status.equals(1)) {
        return AjaxResult.success("发送成功,请注意查收");
    } else {
        return AjaxResult.error("发送失败,稍后请重试");
    }
}

/**
 * 验证码登录方法
 *
 * @param smsLoginBody 手机验证码登录信息
 * @return 结果
 */
@PostMapping("/smsLogin")
public AjaxResult smsLogin(@RequestBody SmsLoginBody smsLoginBody) {
    String phonenumber = smsLoginBody.getPhonenumber();
    String code = smsLoginBody.getCode();
    // 先判断验证码是否正确
    if (loginService.validateSmsCode(phonenumber, code)) {
        AjaxResult ajax = AjaxResult.success();
        //生成令牌
        String token = loginService.smsLogin(phonenumber, code);
        ajax.put(Constants.TOKEN, token);
        return ajax;
    } else {
        return AjaxResult.error("验证码错误");
    }
}

3.4.2 Impl层(就是SysLoginService)

/**
 * 发送手机验证码
 *
 * @param phone
 */
public Integer sendSms(String phone) {
    String code = generateSmsCode();

    try {
        // 调用阿里云短信服务,发送短信
        Map<String, Object> sendSmsResponseMap = SmsUtil.sendMsg(phone, code);

        // 获取响应体状态码和Body状态码
        Integer statusCode = (Integer)sendSmsResponseMap.get("statusCode");
        String bodyCode = (String) sendSmsResponseMap.get("bodyCode");

        // 判断响应码
        if (statusCode != null && bodyCode != null && statusCode.equals(200) && bodyCode.equals("OK")) {
            // 成功发送验证码,将验证码存储在 redis 中.
            log.info("手机验证码发送成功...");
            redisCache.setCacheObject(CacheConstants.SMS_CAPTCHA_CODE_KEY + phone, code, 300, TimeUnit.SECONDS);
            return 1;
        } else {
            return -1;
        }

    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

/**
 * 随机生成手机验证码
 *
 * @return
 */
public String generateSmsCode() {
    Random random = new Random();
    return String.format("%06d", random.nextInt(999999));
}

/**
 * 手机号登录验证*
 * @param mobile
 * @return
 */
public String smsLogin(String mobile,String code){
    // 先通过mobile查询用户是否存在
    SysUser user = userService.selectUserByPhone(mobile);

    if (user == null) {
        // 1.不存在先使用手机号注册用户
        SysUser registerUser = new SysUser();
        registerUser.setPhonenumber(mobile);
        registerUser.setUserName(mobile);
        registerUser.setNickName("user" + mobile);
        registerUser.setStatus("0");
        registerUser.setDelFlag("0");
        userService.registerUser(registerUser);
    }

    // 2.用户存在, 用户验证
    Authentication authentication = null;
    try
    {
        // 使用自定义的toke鉴权器构造一个没有鉴权的token
        SmsCodeAuthenticationToken authenticationToken = new SmsCodeAuthenticationToken(mobile,code);
        AuthenticationContextHolder.setContext(authenticationToken);
        // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
        authentication = authenticationManager.authenticate(authenticationToken);
    }
    catch (Exception e)
    {
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(mobile, Constants.LOGIN_FAIL, e.getMessage()));
    }
    finally
    {
        AuthenticationContextHolder.clearContext();
    }
    AsyncManager.me().execute(AsyncFactory.recordLogininfor(mobile, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
    LoginUser loginUser = (LoginUser) authentication.getPrincipal();
    recordLoginInfo(loginUser.getUserId());

    // 生成token
    return tokenService.createToken(loginUser);
}

/**
 * 短信验证码校验
 * @param phonenumber
 * @param code
 */
public Boolean validateSmsCode(String phonenumber, String code) {
    String smsCode = redisCache.getCacheObject(CacheConstants.SMS_CAPTCHA_CODE_KEY + phonenumber);
    if (smsCode == null || !smsCode.equals(code)) {
        return false;
    }
    return true;
}

3.4.3 其中调用了一个 selectUserByPhone 的方法大家自己去实现一下

这是对应xml文件中的查询语句

<select id="selectUserByPhone" parameterType="String" resultMap="SysUserResult">
    <include refid="selectUserVo"/>
    where u.phonenumber = #{phonenumber} and u.del_flag = '0'
</select>

3.5 最后导入模块到 admin 模块中。

在admin模块下的pom.xml文件下导入下面的坐标

<!-- 通用模块 -->
<dependency>
    <groupId>com.quickclean</groupId>
    <artifactId>quickclean-common</artifactId>
</dependency>

注意: 这里自己注意替换模块名

到此为止后端的工作就算是完成了。

四.前端工作

4.1 修改登录页面

账号密码的登录方式我使用的是滑块验证码,同时我使用Elemen-Plus el-tabs 组件实现点击切换的效果。这里主要新增的是

<el-tab-pane label="验证码登录" name="smsCode">
        <!-- 设计一个验证码登录框 -->
        <div class="smsLogin">
          <el-form ref="smsLoginRef" :model="smsLoginForm" :rules="smsLoginRules" class="login-form">
            <h3 class="title">验证码登录</h3>
            <el-from-item prop="phone">
              <el-row type="flex" justify="ceter">
                <el-col span="22">
                  <el-input v-model="smsLoginForm.phone" type="text" size="large" placeholder="手机号" auto-complet="off">
                    <template #prefix>
                      <svg-icon icon-class="phone" class="el-input__icon input-icon" />
                    </template>
                  </el-input>
                </el-col>
                <el-col span="2">
                  <el-button size="large" type="primary" style="width:100%;" class="sendCode" @click="sendSmsCode()"
                    :disabled="isSending">{{ isSending ? `${countdown}秒后重新发送` : '发送验证码' }}</el-button>
                </el-col>
              </el-row>
            </el-from-item>

            <el-form-item prop="code">
              <el-input v-model="smsLoginForm.code" type="text" size="large" auto-complete="off" placeholder="验证码"
                @keyup.enter="handleSmsLogin">
                <template #prefix>
                  <svg-icon icon-class="captcha" class="el-input__icon input-icon" />
                </template>
              </el-input>
            </el-form-item>

            <el-from-item>
              <el-button :loading="loading" size="large" type="primary" style="width:100%;"
                @click.prevent="handleSmsLogin">
                <span v-if="!loading">登 录</span>
                <span v-else>登 录 中...</span>
              </el-button>
            </el-from-item>
          </el-form>
        </div>
      </el-tab-pane>

这里是全部的模板

<template>
  <div class="login">

    <el-tabs v-model="loginMode" class="loginModetabs" stretch="true">
      <el-tab-pane label="密码登录" name="userAndPassWord">
        <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
          <h3 class="title">若依后台管理系统</h3>
          <el-form-item prop="username">
            <el-input v-model="loginForm.username" type="text" size="large" auto-complete="off" placeholder="账号">
              <template #prefix>
                <svg-icon icon-class="user" class="el-input__icon input-icon" />
              </template>
            </el-input>
          </el-form-item>
          <el-form-item prop="password">
            <el-input v-model="loginForm.password" type="password" size="large" auto-complete="off" placeholder="密码"
              @keyup.enter="handleLogin">
              <template #prefix>
                <svg-icon icon-class="password" class="el-input__icon input-icon" />
              </template>
            </el-input>
          </el-form-item>

          <Verify @success="capctchaCheckSuccess" :mode="'pop'" :captchaType="'blockPuzzle'"
            :imgSize="{ width: '330px', height: '155px' }" ref="verify" v-if="captchaEnabled"></Verify>

          <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
          <el-form-item style="width:100%;">
            <el-button :loading="loading" size="large" type="primary" style="width:100%;" @click.prevent="handleLogin">
              <span v-if="!loading">登 录</span>
              <span v-else>登 录 中...</span>
            </el-button>
            <div style="float: right;" v-if="register">
              <router-link class="link-type" :to="'/register'">立即注册</router-link>
            </div>
          </el-form-item>
        </el-form>
      </el-tab-pane>
      <el-tab-pane label="验证码登录" name="smsCode">
        <!-- 设计一个验证码登录框 -->
        <div class="smsLogin">
          <el-form ref="smsLoginRef" :model="smsLoginForm" :rules="smsLoginRules" class="login-form">
            <h3 class="title">验证码登录</h3>
            <el-from-item prop="phone">
              <el-row type="flex" justify="ceter">
                <el-col span="22">
                  <el-input v-model="smsLoginForm.phone" type="text" size="large" placeholder="手机号" auto-complet="off">
                    <template #prefix>
                      <svg-icon icon-class="phone" class="el-input__icon input-icon" />
                    </template>
                  </el-input>
                </el-col>
                <el-col span="2">
                  <el-button size="large" type="primary" style="width:100%;" class="sendCode" @click="sendSmsCode()"
                    :disabled="isSending">{{ isSending ? `${countdown}秒后重新发送` : '发送验证码' }}</el-button>
                </el-col>
              </el-row>
            </el-from-item>

            <el-form-item prop="code">
              <el-input v-model="smsLoginForm.code" type="text" size="large" auto-complete="off" placeholder="验证码"
                @keyup.enter="handleSmsLogin">
                <template #prefix>
                  <svg-icon icon-class="captcha" class="el-input__icon input-icon" />
                </template>
              </el-input>
            </el-form-item>

            <el-from-item>
              <el-button :loading="loading" size="large" type="primary" style="width:100%;"
                @click.prevent="handleSmsLogin">
                <span v-if="!loading">登 录</span>
                <span v-else>登 录 中...</span>
              </el-button>
            </el-from-item>
          </el-form>
        </div>
      </el-tab-pane>
    </el-tabs>

    <!--  底部  -->
    <div class="el-login-footer">
      <span>Copyright © 2018-2023 ruoyi.vip All Rights Reserved.</span>
    </div>
  </div>
</template>

4.2 在login.js中写入接口方法

// 验证码登录方法
export function smsLogin(phonenumber, code) {
  const data = {
    phonenumber,
    code
  }
  return request({
    url: '/smsLogin',
    headers: {
      isToken: false,
      repeatSubmit: false
    },
    method: 'post',
    data: data
  })
}

// 发送手机验证码方法
export function sendSms(mobile) {
  return request({
    url: '/sendSms',
    headers: {
      isToken: false,
    },
    method: 'post',
    data: mobile
  })
}

4.3 全局状态管理文件 user.js 中action中实现下面的方法

      // 手机验证码登录
      smsLogin(userInfo) {
        const phonenumber = userInfo.phone.trim()
        const code = userInfo.code
        return new Promise((resolve, reject) => {
          smsLogin(phonenumber, code).then(res => {
            setToken(res.token)
            this.token = res.token
            resolve()
          }).catch(error => {
            reject(error)
          })
        })
      },

4.4 回到login.vue 部分实现js逻辑代码

//登录模式
const loginMode = ref('userAndPassWord');

// 验证码登录逻辑
const smsLoginForm = ref({
  phone: '',
  code: ''
});

// 验证码登录规则
const smsLoginRules = {
  phone: [
    { required: true, message: '请输入手机号', trigger: 'blur' },
    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
  ],
  code: [
    { required: true, message: '请输入验证码', trigger: 'blur' },
    { pattern: /^\d{6}$/, message: '验证码长度为6位数字', trigger: 'blur' }
  ]
}

// 导入发送验证码接口和验证码登录接口
import { sendSms } from "@/api/login";

//发送验证码
const sendSmsCode = () => {
  console.log("开始执行验证码发送方法")
  //单独验证手机号字段
  proxy.$refs.smsLoginRef.validateField('phone', (valid) => {
    if (valid) {
      // 验证通过,允许发送验证码接口请求
      sendSms(smsLoginForm.value.phone).then(res => {
        proxy.$message({
          message: res.msg,
          type: 'success'
        })
        
        // 发送成功启用倒计时
        startCountdown();
      })
    } else {
      // 格式错误
      proxy.$message({
        message: '请输入正确的手机号',
        type: 'error'
      })
    }
  })
}

// 定义倒计时相关的状态变量
const countdown = ref(60); // 倒计时时间(秒)
const isSending = ref(false); // 按钮是否禁用

// 验证码重新发送倒计时方法
const startCountdown = () => {
  isSending.value = true;
  const timer = setInterval(() => {
    if (countdown.value > 0) {
      countdown.value--;
    } else {
      isSending.value = false;
      countdown.value = 60;
      clearInterval(timer);
    }
  }, 1000);
}

//处理验证码登录
const handleSmsLogin = () => {
  proxy.$refs.smsLoginRef.validate(valid => {
    if (valid) {
      userStore.smsLogin(smsLoginForm.value).then(() => {
        const query = route.query;
        const otherQueryParams = Object.keys(query).reduce((acc, cur) => {
          if (cur !== "redirect") {
            acc[cur] = query[cur];
          }
          return acc;
        }, {});
        router.push({ path: redirect.value || "/", query: otherQueryParams });
      })
    }
  })
}

五.总结

到这里我们前后端的全部工作就完成了,这里最后提醒大家不论是前端还是后端,记得把相应的包导入,同时包名不同的地方记得修改。同时笔者只是为大家提供一下思路,不一定复制上去就能完美运行,大家还是要多尝试,然后适应自己的程序,其中可能有部分地方缺失,欢迎大家指出补正。