登录前验证码校验实现

1. 后端

1.1. dto 新增字段

LoginRequest

java 复制代码
package com.yu.cloudpicturebackend.model.dto;

import com.yu.cloudpicturebackend.common.ValidationGroups;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import javax.validation.constraints.*;
import java.io.Serializable;

@Data
public class LoginRequest implements Serializable {

    private static final long serialVersionUID = -6263796057820657368L;

    /**
     * 邮箱
     */
    @NotEmpty(message = "邮箱不能为空", groups = ValidationGroups.NotNullCheck.class)
    @ApiModelProperty(value = "邮箱", required = true)
    private String email;

    /**
     * 密码
     */
    @NotBlank(message = "密码不能为空", groups = ValidationGroups.NotNullCheck.class)
    @Size(min = 8, max = 15, message = "密码长度应为8-15位", groups = ValidationGroups.LengthCheck.class)
    @ApiModelProperty(value = "密码", required = true)
    private String password;

    /**
     * 验证码 id
     */
    @NotBlank(message = "验证码Id不能为空")
    @ApiModelProperty(value = "验证码Id", required = true)
    private String captchaId;

    /**
     * 验证码
     */
    @NotBlank(message = "验证码不能为空")
    @ApiModelProperty(value = "验证码", required = true)
    private String captchaCode;

}

1.2. 新增 vo

CaptchaVO

java 复制代码
package com.yu.cloudpicturebackend.model.vo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CaptchaVO {
    /**
     * 验证码 id
     */
    private String captchaId;

    /**
     * base64图片地址
     */
    private String imageBase64;

    /**
     * 过期时长
     */
    private Integer expireTime;

    /**
     * 算术验证码的问题
     */
    private String question;

    /**
     * 验证码类型:line, circle, math, gif
     */
    private String captchaType;
}

1.3. 新增错误码

ErrorCode

java 复制代码
package com.yu.cloudpicturebackend.exception;

import lombok.Getter;

@Getter
public enum ErrorCode {

    SUCCESS(0, "ok"),
    PARAMS_ERROR(40000, "请求参数错误"),
    PASSWORD_NOT_MATCH(40001, "账号或密码错误"),
    CAPTCHA_ERROR(40002, "验证码错误"),
    NOT_LOGIN_ERROR(40100, "未登录"),
    NO_AUTH_ERROR(40101, "无权限"),
    NOT_FOUND_ERROR(40400, "请求数据不存在"),
    FORBIDDEN_ERROR(40300, "禁止访问"),
    SYSTEM_ERROR(50000, "系统内部异常"),
    OPERATION_ERROR(50001, "操作失败");

    /**
     * 错误码
     */
    private final int code;

    /**
     * 信息
     */
    private final String message;

    ErrorCode(int code, String message) {
        this.code = code;
        this.message = message;
    }
}

1.4. 新增常量类

AuthConstant

java 复制代码
package com.yu.cloudpicturebackend.constant;

public final class AuthConstant {

    private AuthConstant() {
        // 防止实例化
    }

    public static final String SEND_KEY_PRE = "email:send:";
    public static final String STATUS_KEY_PRE = "email:verified:";

    public static final String CAPTCHA_PREFIX = "captcha:";
    public static final int CAPTCHA_EXPIRE_SECONDS = 300; // 5分钟
    public static final int CAPTCHA_WIDTH = 120;
    public static final int CAPTCHA_HEIGHT = 40;

}

1.5. 接口层

java 复制代码
/**
 * 获取图片验证码
 *
 * @return
 */
@GetMapping("/captcha")
@ApiOperation("获取图片验证码")
@ApiImplicitParams({
        @ApiImplicitParam(
                name = "type",
                value = "验证码类型: line(线段干扰), circle(圆圈干扰), shear(扭曲干扰), gif(gif类型), math(算术类型)",
                required = true,
                dataType = "string",
                paramType = "query",
                defaultValue = "line",
                allowableValues = "line,circle,shear,gif,math"
        )
})
public BaseResponse<CaptchaVO> generateCaptcha(@RequestParam(defaultValue = "line") String type) {
    if (!Arrays.asList("line", "circle", "shear", "gif", "math").contains(type)) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR, "不支持的验证码类型");
    }

    try {
        CaptchaVO captchaVO = authService.generateCaptcha(type);
        return ResultUtils.success(captchaVO);
    } catch (Exception e) {
        throw new BusinessException(ErrorCode.SYSTEM_ERROR, "验证码生成失败");
    }
}

1.6. 服务层

java 复制代码
package com.yu.cloudpicturebackend.service;

import cn.hutool.captcha.*;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.yu.cloudpicturebackend.model.vo.CaptchaVO;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.ByteArrayOutputStream;
import java.util.Base64;
import java.util.Random;
import java.util.concurrent.TimeUnit;

import static com.yu.cloudpicturebackend.constant.AuthConstant.*;

@Service
@Slf4j
public class AuthService {

    //获取配置文件的信息
    @Value("${spring.mail.host}")
    private String host;
    @Value("${spring.mail.username}")
    private String username;
    @Value("${spring.mail.password}")
    private String password;

    // springboot 整合 redis
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 生成验证码(支持算术验证码)
     */
    public CaptchaVO generateCaptcha(String type) {
        try {
            if ("math".equals(type)) {
                return generateMathCaptcha();
            } else {
                ICaptcha captcha = createCaptchaInstance(type);
                captcha.createCode();
                return buildCaptchaVO(captcha, type);
            }
        } catch (Exception e) {
            log.error("生成验证码失败, type: {}", type, e);
            throw new RuntimeException("验证码生成失败");
        }
    }

    /**
     * 生成算术验证码
     */
    private CaptchaVO generateMathCaptcha() {
        try {
            // 生成算术题
            MathProblem problem = generateMathProblem();

            // 创建基础验证码
            LineCaptcha captcha = new LineCaptcha(120, 40, 4, 50);

            // 设置自定义生成器
            captcha.setGenerator(new CodeGenerator() {
                @Override
                public String generate() {
                    return problem.getQuestion() + "=?";
                }

                @Override
                public boolean verify(String code, String userInput) {
                    return problem.getAnswer().equals(userInput.trim());
                }
            });

            // 生成验证码
            captcha.createCode();

            String captchaId = IdUtil.fastUUID();
            storeCaptcha(captchaId, problem.getAnswer());

            String base64Image = getBase64Image(captcha);

            return CaptchaVO.builder()
                    .captchaId(captchaId)
                    .imageBase64("data:image/png;base64," + base64Image)
                    .question(problem.getQuestion() + " = ?")
                    .expireTime(CAPTCHA_EXPIRE_SECONDS)
                    .captchaType("math")
                    .build();

        } catch (Exception e) {
            log.error("生成算术验证码失败", e);
            throw new RuntimeException("算术验证码生成失败");
        }
    }

    /**
     * 生成数学问题
     */
    private MathProblem generateMathProblem() {
        Random random = new Random();
        int num1 = random.nextInt(10) + 1;
        int num2 = random.nextInt(10) + 1;

        String[] operators = {"+", "-", "*"};
        String operator = operators[random.nextInt(operators.length)];

        String question = num1 + " " + operator + " " + num2;
        String answer = calculateResult(num1, num2, operator);

        return new MathProblem(question, answer);
    }

    private String calculateResult(int num1, int num2, String operator) {
        switch (operator) {
            case "+": return String.valueOf(num1 + num2);
            case "-": return String.valueOf(num1 - num2);
            case "*": return String.valueOf(num1 * num2);
            default: return String.valueOf(num1 + num2);
        }
    }

    /**
     * 创建ICaptcha实例
     */
    private ICaptcha createCaptchaInstance(String type) {
        switch (type) {
            case "circle":
                return new CircleCaptcha(120, 40, 4, 20);
            case "shear":
                return new ShearCaptcha(120, 40, 4, 4);
            case "gif":
                return new GifCaptcha(120, 40, 4);
            case "line":
            default:
                return new LineCaptcha(120, 40, 4, 50);
        }
    }

    /**
     * 构建验证码VO对象
     */
    private CaptchaVO buildCaptchaVO(ICaptcha captcha, String type) {
        String code = captcha.getCode();
        String base64Image = getBase64Image(captcha);
        String captchaId = IdUtil.fastUUID();

        storeCaptcha(captchaId, code);

        return CaptchaVO.builder()
                .captchaId(captchaId)
                .imageBase64(getImageDataUrl(type, base64Image))
                .expireTime(CAPTCHA_EXPIRE_SECONDS)
                .captchaType(type)
                .build();
    }

    /**
     * 获取Base64图片数据
     */
    private String getBase64Image(ICaptcha captcha) {
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            captcha.write(outputStream);
            return Base64.getEncoder().encodeToString(outputStream.toByteArray());
        } catch (Exception e) {
            log.error("获取验证码Base64数据失败", e);
            throw new RuntimeException("验证码生成失败");
        }
    }

    /**
     * 根据类型生成Data URL
     */
    private String getImageDataUrl(String type, String base64Image) {
        switch (type) {
            case "gif":
                return "data:image/gif;base64," + base64Image;
            default:
                return "data:image/png;base64," + base64Image;
        }
    }

    /**
     * 存储验证码到Redis
     */
    private void storeCaptcha(String captchaId, String code) {
        String key = CAPTCHA_PREFIX + captchaId;
        stringRedisTemplate.opsForValue().set(
                key,
                code,
                CAPTCHA_EXPIRE_SECONDS,
                TimeUnit.SECONDS
        );
    }

    /**
     * 验证验证码
     */
    public boolean validateCaptcha(String captchaId, String userInput) {
        if (StrUtil.isBlank(captchaId) || StrUtil.isBlank(userInput)) {
            return false;
        }

        String key = CAPTCHA_PREFIX + captchaId;
        String storedCode = stringRedisTemplate.opsForValue().get(key);

        if (storedCode == null) {
            return false;
        }

        boolean isValid = storedCode.equalsIgnoreCase(userInput.trim());
        if (isValid) {
            stringRedisTemplate.delete(key);
        }
        return isValid;
    }

    @Data
    @AllArgsConstructor
    private static class MathProblem {
        private String question;
        private String answer;
    }
}

2. 前端

javascript 复制代码
<template>
  <div id="login-page">
    <div class="login-card">
      <div class="title">密码登录</div>
      <div class="form-box">
        <a-form
          :model="formState"
          name="normal_login"
          class="login-form"
          @finish="onFinish"
          @finishFailed="onFinishFailed"
        >
          <a-form-item name="email" :rules="[{ required: true, message: '请输入你的账号!' }]">
            <a-input v-model:value="formState.email" placeholder="请输入账号/邮箱">
              <template #prefix>
                <UserOutlined class="site-form-item-icon" />
              </template>
            </a-input>
          </a-form-item>

          <a-form-item name="password" :rules="[{ required: true, message: '请输入你的密码!' }]">
            <a-input-password v-model:value="formState.password" placeholder="请输入密码">
              <template #prefix>
                <LockOutlined class="site-form-item-icon" />
              </template>
            </a-input-password>
          </a-form-item>

          <a-form-item name="captchaCode" :rules="[{ required: true, message: '请输入验证码!' }]">
            <a-row :gutter="8">
              <a-col :span="16">
                <a-input v-model:value="formState.captchaCode" placeholder="请输入验证码">
                  <template #prefix>
                    <SafetyCertificateOutlined class="site-form-item-icon" />
                  </template>
                </a-input>
              </a-col>
              <a-col :span="8">
                <img
                  :src="captcha.imageBase64"
                  style="width: 100%"
                  alt="captcha"
                  @click="getCaptcha"
                />
              </a-col>
            </a-row>
          </a-form-item>

          <a-form-item>
            <a-button
              :disabled="disabled"
              block
              type="primary"
              html-type="submit"
              class="login-form-button"
            >
              登录
            </a-button>
            或者
            <a href="/user/register">立即注册</a>
          </a-form-item>
        </a-form>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { reactive, computed, onMounted, ref } from 'vue'
import { UserOutlined, LockOutlined, SafetyCertificateOutlined } from '@ant-design/icons-vue'
import { loginUsingPost1 } from '@/api/yonghujiekou.ts'
import { message } from 'ant-design-vue'
import { useRouter } from 'vue-router'
import { useLoginUserStore } from '@/stores/loginUserStore.ts'
import { generateCaptchaUsingGet1 } from '@/api/shouquanjiekou.ts'

interface FormState {
  email: string
  password: string
  captchaId: string
  captchaCode: string
  remember: boolean
}

const formState = reactive<FormState>({
  email: '',
  password: '',
  captchaId: '',
  captchaCode: '',
  remember: true,
})

const router = useRouter()
const loginUserStore = useLoginUserStore()
const onFinish = async (values: any) => {
  const res = await loginUsingPost1(formState)
  if (res.data.code == 0 && res.data.data) {
    message.success('登录成功')
    // token 存入本地
    // localStorage.setItem('token', res.data.data)
    await loginUserStore.fetchLoginUser()
    await router.replace('/')
  }
  console.log('Success:', values)
}

const onFinishFailed = (errorInfo: any) => {
  console.log('Failed:', errorInfo)
}
const disabled = computed(() => {
  return !(formState.email && formState.password && formState.captchaCode)
})

let captcha = ref<API.CaptchaVO>({
  captchaId: '',
  imageBase64: '',
  expireTime: 0,
  question: '',
  captchaType: '',
})

/**
 * 获取验证码
 */
const getCaptcha = async () => {
  const res = await generateCaptchaUsingGet1({
    type: 'math',
  })
  if (res.data.code == 0 && res.data.data) {
    captcha.value = res.data.data
    formState.captchaId = captcha.value.captchaId
  }
}

onMounted(async () => {
  await getCaptcha()
})
</script>

<style scoped>
#login-page {
  width: 100%;
  height: 100%;
  justify-content: center;
  background-color: #f8f5f2;
  overflow: hidden;

  .login-card {
    width: 25%;
    min-width: 380px;
    border-radius: 15px;
    padding: 20px;
    box-sizing: border-box;
    background-color: #fff;
    margin: 6% auto 0;

    .title {
      text-align: center;
      font-size: 24px;
    }

    .form-box {
      margin-top: 30px;
      padding: 20px 20px 0;

      .login-form {
        min-width: 300px;
      }

      .login-form-forgot {
        float: right;
      }

      .login-form-button {
        margin-bottom: 10px;
      }
    }
  }
}
</style>
相关推荐
what丶k1 分钟前
Java连接人大金仓数据库(KingbaseES)全指南:从环境搭建到实战优化
java·开发语言·数据库
The_era_achievs_hero2 分钟前
封装api方法(全面)
前端·javascript·uni-app·api·封装接口
Mr Xu_6 分钟前
深入解析 getBoundingClientRect 与 offsetTop:解决 Vue 平滑滚动偏移误差问题
前端·javascript·vue.js
Mr-Wanter6 分钟前
vue 解决img图片路径存在但图片无法访问时显示错误的问题
前端·vue·img
muddjsv6 分钟前
近些年前端开发主流技术全景:趋势、工具与实践指南
前端
沛沛老爹7 分钟前
从Web到AI:多模态Agent Skills开发实战——JavaScript+Python全栈赋能视觉/语音能力
java·开发语言·javascript·人工智能·python·安全架构
0x538 分钟前
JAVA|智能仿真并发项目-进程与线程
java·开发语言·jvm
xiaolyuh1239 分钟前
Spring Boot 深度解析
java·spring boot·后端
storyseek9 分钟前
RAG的四种的检索方式
python
黎雁·泠崖9 分钟前
Java静态方法:用法+工具类设计+ArrayUtil实战
java·开发语言