Vue 3 全栈教程:225.输入验证与过滤

概述

输入验证与过滤是Web应用安全的重要组成部分,它可以防止恶意输入、确保数据完整性,并提供良好的用户体验。本集将深入探讨Vue 3应用中的输入验证与过滤机制,包括客户端验证、服务器端验证、常见的验证规则以及具体的实现方案。

输入验证的重要性

1. 防止恶意输入

恶意输入是Web应用安全的主要威胁之一,如SQL注入、XSS攻击、命令注入等。通过输入验证,可以过滤掉恶意字符和恶意代码,防止这些攻击。

2. 确保数据完整性

输入验证可以确保用户输入的数据符合预期格式和范围,如确保邮箱格式正确、密码强度足够、年龄在合理范围内等。

3. 提供良好的用户体验

实时的客户端验证可以在用户输入过程中提供即时反馈,帮助用户正确填写表单,减少提交错误,提高用户体验。

4. 减轻服务器负担

客户端验证可以在数据提交到服务器之前过滤掉大部分无效数据,减少服务器的处理负担和网络流量。

输入验证的类型

1. 客户端验证

客户端验证是在浏览器端进行的验证,通常使用JavaScript实现。它可以提供即时反馈,但不能完全依赖,因为攻击者可以绕过客户端验证。

2. 服务器端验证

服务器端验证是在服务器端进行的验证,是输入验证的最后一道防线。无论客户端是否进行了验证,服务器端都必须进行验证,以确保数据的安全性和完整性。

3. 数据库验证

数据库验证是在数据库层面进行的验证,如使用约束(如NOT NULL、UNIQUE、CHECK等)来确保数据的完整性。

Vue 3中的输入验证实现

1. 原生表单验证

HTML5提供了原生的表单验证功能,如required、type、pattern、min、max等属性。Vue 3可以直接使用这些原生验证功能。

<template>
  <div>
    <h2>原生表单验证示例</h2>
    <form @submit.prevent="submitForm">
      <div>
        <label>用户名:</label>
        <input
          v-model="form.username"
          type="text"
          required
          minlength="3"
          maxlength="20"
          pattern="^[a-zA-Z0-9_]+$"
          title="用户名只允许字母、数字和下划线"
        >
      </div>
      <div>
        <label>邮箱:</label>
        <input
          v-model="form.email"
          type="email"
          required
        >
      </div>
      <div>
        <label>密码:</label>
        <input
          v-model="form.password"
          type="password"
          required
          minlength="6"
        >
      </div>
      <div>
        <label>年龄:</label>
        <input
          v-model.number="form.age"
          type="number"
          required
          min="18"
          max="100"
        >
      </div>
      <button type="submit">提交</button>
    </form>
    <div v-if="submittedData">
      <h3>提交的数据:</h3>
      <pre>{{ submittedData }}</pre>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const form = ref({
  username: '',
  email: '',
  password: '',
  age: null
})

const submittedData = ref(null)

const submitForm = (event) => {
  const formElement = event.target
  // 检查表单是否通过原生验证
  if (formElement.checkValidity()) {
    submittedData.value = { ...form.value }
    console.log('表单提交成功', submittedData.value)
  } else {
    // 触发浏览器的原生验证提示
    formElement.reportValidity()
  }
}
</script>

2. 使用VeeValidate进行表单验证

VeeValidate是一个功能强大的Vue表单验证库,支持多种验证规则和自定义验证。

安装VeeValidate:

npm install vee-validate@next yup

基本使用:

<template>
  <div>
    <h2>VeeValidate表单验证示例</h2>
    <Form @submit="submitForm" v-slot="{ errors }">
      <div>
        <label>用户名:</label>
        <Field
          name="username"
          v-model="form.username"
          type="text"
          placeholder="请输入用户名"
        />
        <span v-if="errors.username" class="error">{{ errors.username }}</span>
      </div>
      <div>
        <label>邮箱:</label>
        <Field
          name="email"
          v-model="form.email"
          type="email"
          placeholder="请输入邮箱"
        />
        <span v-if="errors.email" class="error">{{ errors.email }}</span>
      </div>
      <div>
        <label>密码:</label>
        <Field
          name="password"
          v-model="form.password"
          type="password"
          placeholder="请输入密码"
        />
        <span v-if="errors.password" class="error">{{ errors.password }}</span>
      </div>
      <div>
        <label>确认密码:</label>
        <Field
          name="confirmPassword"
          v-model="form.confirmPassword"
          type="password"
          placeholder="请确认密码"
        />
        <span v-if="errors.confirmPassword" class="error">{{ errors.confirmPassword }}</span>
      </div>
      <button type="submit">提交</button>
    </Form>
    <div v-if="submittedData">
      <h3>提交的数据:</h3>
      <pre>{{ submittedData }}</pre>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { Form, Field, ErrorMessage } from 'vee-validate'
import * as yup from 'yup'

// 定义验证规则
const validationSchema = yup.object({
  username: yup
    .string()
    .required('用户名不能为空')
    .min(3, '用户名长度不能少于3个字符')
    .max(20, '用户名长度不能超过20个字符')
    .matches(/^[a-zA-Z0-9_]+$/, '用户名只允许字母、数字和下划线'),
  email: yup
    .string()
    .required('邮箱不能为空')
    .email('邮箱格式不正确'),
  password: yup
    .string()
    .required('密码不能为空')
    .min(6, '密码长度不能少于6个字符')
    .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/, '密码必须包含大小写字母和数字'),
  confirmPassword: yup
    .string()
    .required('请确认密码')
    .oneOf([yup.ref('password')], '两次输入的密码不一致')
})

const form = ref({
  username: '',
  email: '',
  password: '',
  confirmPassword: ''
})

const submittedData = ref(null)

const submitForm = async (values) => {
  try {
    // 验证表单数据
    const validatedData = await validationSchema.validate(values, { abortEarly: false })
    submittedData.value = validatedData
    console.log('表单提交成功', validatedData)
  } catch (error) {
    console.error('表单验证失败', error)
  }
}
</script>

<style scoped>
.error {
  color: red;
  font-size: 12px;
  margin-top: 4px;
  display: block;
}
</style>

3. 自定义验证规则

在某些情况下,我们需要自定义验证规则来满足特定的业务需求。VeeValidate允许我们轻松地定义和使用自定义验证规则。

<template>
  <div>
    <h2>自定义验证规则示例</h2>
    <Form @submit="submitForm" v-slot="{ errors }">
      <div>
        <label>手机号:</label>
        <Field
          name="phone"
          v-model="form.phone"
          type="text"
          placeholder="请输入手机号"
        />
        <span v-if="errors.phone" class="error">{{ errors.phone }}</span>
      </div>
      <div>
        <label>身份证号:</label>
        <Field
          name="idCard"
          v-model="form.idCard"
          type="text"
          placeholder="请输入身份证号"
        />
        <span v-if="errors.idCard" class="error">{{ errors.idCard }}</span>
      </div>
      <button type="submit">提交</button>
    </Form>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { Form, Field, defineRule } from 'vee-validate'
import * as yup from 'yup'

// 定义自定义验证规则:手机号验证
const validatePhone = (value) => {
  const phoneRegex = /^1[3-9]\d{9}$/
  return phoneRegex.test(value) || '请输入正确的手机号'
}

// 定义自定义验证规则:身份证号验证
const validateIdCard = (value) => {
  const idCardRegex = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/
  return idCardRegex.test(value) || '请输入正确的身份证号'
}

// 注册自定义验证规则
defineRule('phone', validatePhone)
defineRule('idCard', validateIdCard)

// 使用yup定义验证规则
const validationSchema = yup.object({
  phone: yup
    .string()
    .required('手机号不能为空')
    .test('phone', '请输入正确的手机号', (value) => {
      return /^1[3-9]\d{9}$/.test(value)
    }),
  idCard: yup
    .string()
    .required('身份证号不能为空')
    .test('idCard', '请输入正确的身份证号', (value) => {
      return /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(value)
    })
})

const form = ref({
  phone: '',
  idCard: ''
})

const submitForm = async (values) => {
  try {
    const validatedData = await validationSchema.validate(values, { abortEarly: false })
    console.log('表单提交成功', validatedData)
  } catch (error) {
    console.error('表单验证失败', error)
  }
}
</script>

<style scoped>
.error {
  color: red;
  font-size: 12px;
  margin-top: 4px;
  display: block;
}
</style>

4. 实时验证

实时验证可以在用户输入过程中提供即时反馈,帮助用户正确填写表单。VeeValidate支持实时验证,可以通过设置validateOnInput属性来实现。

<template>
  <div>
    <h2>实时验证示例</h2>
    <Form @submit="submitForm" v-slot="{ errors }" :validate-on-input="true">
      <div>
        <label>用户名:</label>
        <Field
          name="username"
          v-model="form.username"
          type="text"
          placeholder="请输入用户名"
        />
        <span v-if="errors.username" class="error">{{ errors.username }}</span>
      </div>
      <div>
        <label>邮箱:</label>
        <Field
          name="email"
          v-model="form.email"
          type="email"
          placeholder="请输入邮箱"
        />
        <span v-if="errors.email" class="error">{{ errors.email }}</span>
      </div>
      <button type="submit">提交</button>
    </Form>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { Form, Field } from 'vee-validate'
import * as yup from 'yup'

const validationSchema = yup.object({
  username: yup
    .string()
    .required('用户名不能为空')
    .min(3, '用户名长度不能少于3个字符'),
  email: yup
    .string()
    .required('邮箱不能为空')
    .email('邮箱格式不正确')
})

const form = ref({
  username: '',
  email: ''
})

const submitForm = async (values) => {
  try {
    const validatedData = await validationSchema.validate(values, { abortEarly: false })
    console.log('表单提交成功', validatedData)
  } catch (error) {
    console.error('表单验证失败', error)
  }
}
</script>

<style scoped>
.error {
  color: red;
  font-size: 12px;
  margin-top: 4px;
  display: block;
}
</style>

输入过滤

输入过滤是指在用户输入数据时,自动过滤掉无效字符或转换数据格式,如自动去除空格、转换为大写、过滤掉HTML标签等。

1. 使用计算属性进行输入过滤

<template>
  <div>
    <h2>计算属性输入过滤示例</h2>
    <div>
      <label>输入:</label>
      <input v-model="inputValue" type="text" placeholder="请输入内容">
    </div>
    <div>
      <label>过滤后:</label>
      <input v-model="filteredValue" type="text" readonly>
    </div>
    <div>
      <label>大写转换:</label>
      <input v-model="uppercaseValue" type="text" readonly>
    </div>
    <div>
      <label>去除HTML标签:</label>
      <input v-model="noHtmlValue" type="text" readonly>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const inputValue = ref('')

// 过滤掉空格
const filteredValue = computed(() => {
  return inputValue.value.replace(/\s+/g, '')
})

// 转换为大写
const uppercaseValue = computed(() => {
  return inputValue.value.toUpperCase()
})

// 过滤掉HTML标签
const noHtmlValue = computed(() => {
  return inputValue.value.replace(/<[^>]*>/g, '')
})
</script>

2. 使用watch进行输入过滤

<template>
  <div>
    <h2>Watch输入过滤示例</h2>
    <div>
      <label>手机号:</label>
      <input v-model="phone" type="text" placeholder="请输入手机号">
    </div>
    <div>
      <label>只允许数字:</label>
      <input v-model="onlyNumbers" type="text" placeholder="请输入数字">
    </div>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue'

const phone = ref('')
const onlyNumbers = ref('')

// 手机号格式化:自动添加分隔符
watch(phone, (newValue) => {
  // 只保留数字
  const cleaned = newValue.replace(/\D/g, '')
  // 格式化手机号:138 8888 8888
  const formatted = cleaned.replace(/(\d{3})(\d{4})(\d{4})/, '$1 $2 $3')
  // 防止无限循环
  if (formatted !== phone.value) {
    phone.value = formatted
  }
})

// 只允许输入数字
watch(onlyNumbers, (newValue) => {
  const cleaned = newValue.replace(/\D/g, '')
  if (cleaned !== onlyNumbers.value) {
    onlyNumbers.value = cleaned
  }
})
</script>

3. 使用自定义指令进行输入过滤

<template>
  <div>
    <h2>自定义指令输入过滤示例</h2>
    <div>
      <label>只允许字母:</label>
      <input v-model="onlyLetters" type="text" v-only-letters placeholder="请输入字母">
    </div>
    <div>
      <label>只允许数字:</label>
      <input v-model="onlyDigits" type="text" v-only-digits placeholder="请输入数字">
    </div>
    <div>
      <label>去除空格:</label>
      <input v-model="noSpaces" type="text" v-trim placeholder="请输入内容">
    </div>
  </div>
</template>

<script setup>
import { ref, appContext } from 'vue'

const onlyLetters = ref('')
const onlyDigits = ref('')
const noSpaces = ref('')

// 获取app实例
const app = appContext.app

// 自定义指令:只允许字母
app.directive('only-letters', {
  mounted(el) {
    el.addEventListener('input', (e) => {
      const value = e.target.value
      e.target.value = value.replace(/[^a-zA-Z]/g, '')
      // 触发input事件,更新v-model
      el.dispatchEvent(new Event('input'))
    })
  }
})

// 自定义指令:只允许数字
app.directive('only-digits', {
  mounted(el) {
    el.addEventListener('input', (e) => {
      const value = e.target.value
      e.target.value = value.replace(/\D/g, '')
      el.dispatchEvent(new Event('input'))
    })
  }
})

// 自定义指令:去除空格
app.directive('trim', {
  mounted(el) {
    el.addEventListener('input', (e) => {
      const value = e.target.value
      e.target.value = value.trim()
      el.dispatchEvent(new Event('input'))
    })
  }
})
</script>

常见的验证规则

1. 格式验证

  • 邮箱验证:确保邮箱格式正确,如`/^[^\s@]+@[^\s@]+.[^\s@]+$/
  • 手机号验证:确保手机号格式正确,如`/^1[3-9]\d{9}$/
  • URL验证:确保URL格式正确,如`/^https?://[\w-]+(.[\w-]+)+([\w-.,@?^=%&:/+#]*[\w-@?^=%&/+#])?/
  • 身份证号验证:确保身份证号格式正确,如`/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/

2. 长度验证

  • 最小长度:确保输入内容的长度不小于指定值
  • 最大长度:确保输入内容的长度不大于指定值
  • 固定长度:确保输入内容的长度等于指定值

3. 范围验证

  • 数值范围:确保数值在指定范围内,如年龄在18-100之间
  • 日期范围:确保日期在指定范围内,如开始日期小于结束日期

4. 复杂度验证

  • 密码复杂度:确保密码包含大小写字母、数字和特殊字符
  • 字符串复杂度:确保字符串包含多种字符类型

5. 唯一性验证

  • 用户名唯一性:确保用户名尚未被注册
  • 邮箱唯一性:确保邮箱尚未被注册

输入验证的最佳实践

  1. 始终进行服务器端验证:客户端验证可以提供良好的用户体验,但不能完全依赖,服务器端验证是最后一道防线。
  2. 使用成熟的验证库:如VeeValidate、Yup、Joi等,避免自行实现复杂的验证逻辑。
  3. 提供清晰的错误信息:错误信息应该清晰、具体,帮助用户理解错误原因和如何修复。
  4. 实时验证:对于简单的验证规则,使用实时验证提供即时反馈;对于复杂的验证规则(如唯一性验证),可以在表单提交时进行验证。
  5. 输入过滤:在用户输入过程中自动过滤掉无效字符,减少用户的输入错误。
  6. 防止XSS攻击:对用户输入的HTML内容进行过滤或转义,防止XSS攻击。
  7. 防止SQL注入:使用参数化查询或ORM框架,避免直接拼接SQL语句。
  8. 使用HTTPS:确保表单提交的数据通过HTTPS传输,防止数据被窃取。
  9. 限制输入长度:在前端和后端都限制输入长度,防止缓冲区溢出攻击。
  10. 定期更新验证规则:根据业务需求和安全威胁的变化,定期更新验证规则。

总结

输入验证与过滤是Web应用安全的重要组成部分,它可以防止恶意输入、确保数据完整性,并提供良好的用户体验。Vue 3应用中可以使用多种方式实现输入验证与过滤,如原生表单验证、VeeValidate库、计算属性、watch、自定义指令等。

在实际应用中,应根据业务需求和安全要求选择合适的验证方式,并遵循输入验证的最佳实践,确保应用的安全性和数据完整性。

扩展阅读

  1. VeeValidate官方文档
  2. Yup官方文档
  3. HTML5表单验证
  4. OWASP输入验证 cheat sheet
  5. Vue官方文档 - 表单输入绑定
  6. Joi官方文档
  7. SQL注入防护
  8. XSS防护
« 上一篇 Vue 3 输入验证与过滤:确保数据安全与完整性 下一篇 » Vue 3 文件上传安全深度指南:防范恶意文件攻击