数据校验不仅需要前端,也需要后端,为了防止恶意攻击。
前端数据校验是vue自带
的,后端校验我们用JSR303(java规范第303号相关标准)
,springboot进行了实现
异常处理我们使用springmvc提供的统一异常处理
自定义异常
异常枚举
package com.atlinxi.common.exception;
/* * 错误码和错误信息定义类 *
* 1. 错误码定义规则为 5 为数字
* 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知 异常
* 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式 *
*
* 错误码列表:
* 10: 通用
* 001:参数格式校验
* 11: 商品
* 12: 订单
* 13: 购物车
* 14: 物流
*
*
**/
public enum BizCodeEnume {
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VAILD_EXCEPTION(10001,"参数格式校验失败");
private int code;
private String message;
BizCodeEnume(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
自定义异常
package com.atlinxi.gulimall.product.exception;
import com.atlinxi.common.exception.BizCodeEnume;
import com.atlinxi.common.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
/**
* 集中处理所有异常
* <p>
* 处理basePackages下的异常,该包下抛出的所有异常都会进入到这儿
*
* @RestControllerAdvice 是 @ControllerAdvice @ResponseBody 的集合
* @Slf4j lombok提供的日志功能
*/
@Slf4j
@RestControllerAdvice(basePackages = "com.atlinxi.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {
// MethodArgumentNotValidException 异常的信息可以从这里获取
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleVaildException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
Map<String, String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach((fieldError) -> {
String field = fieldError.getField();
String defaultMessage = fieldError.getDefaultMessage();
errorMap.put(field, defaultMessage);
});
return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(), BizCodeEnume.VAILD_EXCEPTION.getMessage()).put("data", errorMap);
}
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable) {
log.error("错误:",throwable);
return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(), BizCodeEnume.UNKNOW_EXCEPTION.getMessage());
}
}
数据校验
前端
brand-add-or-update.vue
<template>
<el-dialog
:title="!dataForm.brandId ? '新增' : '修改'"
:close-on-click-modal="false"
:visible.sync="visible"
>
<el-form
:model="dataForm"
:rules="dataRule"
ref="dataForm"
@keyup.enter.native="dataFormSubmit()"
label-width="140px"
>
<el-form-item label="品牌名" prop="name">
<el-input v-model="dataForm.name" placeholder="品牌名"></el-input>
</el-form-item>
<el-form-item label="品牌logo地址" prop="logo">
<!-- <singleUpload></singleUpload> -->
<!-- 也可以使用短横线 -->
<single-upload v-model="dataForm.logo"></single-upload>
</el-form-item>
<el-form-item label="介绍" prop="descript">
<el-input v-model="dataForm.descript" placeholder="介绍"></el-input>
</el-form-item>
<el-form-item label="显示状态" prop="showStatus">
<el-switch
v-model="dataForm.showStatus"
active-color="#13ce66"
inactive-color="#ff4949"
:active-value="1"
:inactive-value="0"
>
</el-switch>
</el-form-item>
<el-form-item label="检索首字母" prop="firstLetter">
<el-input
v-model="dataForm.firstLetter"
placeholder="检索首字母"
></el-input>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input v-model.number="dataForm.sort" placeholder="排序"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()">确定</el-button>
</span>
</el-dialog>
</template>
<script>
// 导入单图片上传
// @ 代表src路径,说是自定义的,但我不知道在哪儿定义的,不关心
// import SingleUpload from "@/components/upload/singleUpload"
// 这个是敲上面的短横线写法,自动导入的
import singleUpload from "../../../components/upload/singleUpload.vue";
export default {
components: { singleUpload },
data() {
return {
visible: false,
dataForm: {
brandId: 0,
name: "",
logo: "",
descript: "",
showStatus: 1,
firstLetter: "",
sort: 0,
},
dataRule: {
name: [{ required: true, message: "品牌名不能为空", trigger: "blur" }],
logo: [
{ required: true, message: "品牌logo地址不能为空", trigger: "blur" },
],
descript: [
{ required: true, message: "介绍不能为空", trigger: "blur" },
],
showStatus: [
{
required: true,
message: "显示状态[0-不显示;1-显示]不能为空",
trigger: "blur",
},
],
firstLetter: [
{
// 自定义校验器
validator: (rule, value, callback) => {
if (value == "") {
callback(new Error("首字母必须填写"));
} else if (!/^[a-zA-Z]+$/.test()) {
callback(new Error("首字母必须是a-z或者A-Z之间"));
} else {
callback();
}
},
trigger: "blur",
},
],
sort: [
{
validator: (rule, value, callback) => {
if (value === "") {
console.log("排序字段:" + value + (value == ""));
callback(new Error("排序字段必须填写"));
// Number.isInteger() 只能验证整数
} else if (!(Number.isInteger(value) || value == 0)) {
callback(new Error("排序必须是0或正整数"));
} else {
callback();
}
},
trigger: "blur",
},
],
},
};
},
methods: {
init(id) {
this.dataForm.brandId = id || 0;
this.visible = true;
this.$nextTick(() => {
this.$refs["dataForm"].resetFields();
if (this.dataForm.brandId) {
this.$http({
url: this.$http.adornUrl(
`/product/brand/info/${this.dataForm.brandId}`
),
method: "get",
params: this.$http.adornParams(),
}).then(({ data }) => {
if (data && data.code === 0) {
this.dataForm.name = data.brand.name;
this.dataForm.logo = data.brand.logo;
this.dataForm.descript = data.brand.descript;
this.dataForm.showStatus = data.brand.showStatus;
this.dataForm.firstLetter = data.brand.firstLetter;
this.dataForm.sort = data.brand.sort;
}
});
}
});
},
// 表单提交
dataFormSubmit() {
this.$refs["dataForm"].validate((valid) => {
if (valid) {
this.$http({
url: this.$http.adornUrl(
`/product/brand/${!this.dataForm.brandId ? "save" : "update"}`
),
method: "post",
data: this.$http.adornData({
brandId: this.dataForm.brandId || undefined,
name: this.dataForm.name,
logo: this.dataForm.logo,
descript: this.dataForm.descript,
showStatus: this.dataForm.showStatus,
firstLetter: this.dataForm.firstLetter,
sort: this.dataForm.sort,
}),
}).then(({ data }) => {
if (data && data.code === 0) {
this.$message({
message: "操作成功",
type: "success",
duration: 1500,
onClose: () => {
this.visible = false;
this.$emit("refreshDataList");
},
});
} else {
this.$message.error(data.msg);
}
});
}
});
},
},
};
</script>
后端
笔记
package com.atlinxi.gulimall.product;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.context.config.annotation.RefreshScope;
/**
* 1、整合mybatis-plus
* 1) 导入依赖(实际上我们在common工程中导入了该依赖)
* <dependency>
* <groupId>com.baomidou</groupId>
* <artifactId>mybatis-plus-boot-starter</artifactId>
* <version>3.3.1</version>
* </dependency>
* 2)配置
* 1、配置数据源(导入数据库驱动和在application.yml配置数据源相关信息)
* 2、使用@MapperScan,告诉mybatis-plus,sql映射文件的位置
*
*
*
* 2、逻辑删除
* 1)在配置文件中配置全局的逻辑删除规则
*
*
* 3. JSR303(java规范第303号相关标准)
*
* SpringBoot2.3.0以后版本没有引入javax.validation,需要手动引入对应版本
*
* 这些注解都在javax.validation.constraints下
*
* 1) springboot提供的,给bean添加校验注解,定义自己的message提示
*
* @Future 字段必须是未来的时间
*
* @Max,@Min,@NotEmpty,@Size,@Email
*
*
* 2)开启校验功能
*
* BindingResult 获取校验的结果
*
* public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
*
* 效果,校验错误以后会有默认的响应
* (我这儿没有响应,就是postman请求返回的时候不会响应message,但是不影响后续的操作)
*
*
* 3)分组校验(多场景的复杂校验)
* 例如新增和修改字段的校验要求可能是不一样的,
* 例如id,新增是不需要的,因为id是自增的,但是修改就必须携带了
*
* 1)给校验注解标注什么情况需要进行校验
*
* 如果在函数参数中标注了分组,则字段必须标注分组,否则不生效
* 如果在函数参数中没有标注分组,则字段不能标注分组,标注分组的反而不生效
*
* @NotBlank(message = "品牌名必须提交",groups = {UpdateGroup.class,AddGroup.class})
*
* public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand ) {
@NotNull(message = "修改必须指定品牌id",groups = {UpdateGroup.class})
@Null(message = "新增不能指定id",groups = {AddGroup.class})
@TableId
private Long brandId;
*
* 4)自定义校验
*
* 1)编写一个自定义的校验注解(定义校验的基本规范)
*
* 2)编写一个自定义校验器
*
* 判断传入的参数是否符合规范 ConstraintValidator
* 3)关联自定义的校验器和自定义的校验注解
*
* 自定义注解中有这么一个注解
*
* 校验注解可以使用多个校验器,
* @Constraint(
* * validatedBy = {ListValueConstraintValidator.class,}}
* * )
*
*
*
* 4. 统一异常处理
*
* @ControllerAdvice
*
*
*
*
*/
@MapperScan("com.atlinxi.gulimall.product.dao")
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallProductApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallProductApplication.class, args);
}
}
添加依赖
product模块
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
common模块
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
BrandEntity
package com.atlinxi.gulimall.product.entity;
import com.atlinxi.common.valid.AddGroup;
import com.atlinxi.common.valid.ListValue;
import com.atlinxi.common.valid.UpdateGroup;
import com.atlinxi.common.valid.UpdateStatusGroup;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.*;
/**
* 品牌
*
* @author linxi
* @email qazokmzjl@gmail.com
* @date 2022-10-12 23:06:12
*/
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@NotNull(message = "修改必须指定品牌id",groups = {UpdateGroup.class})
@Null(message = "新增不能指定id",groups = {AddGroup.class})
@TableId
private Long brandId;
/**
* 品牌名
*/
// @NotEmpty 不为null 或者 ""
// @NotNull 不为null
// @NotBlank 必须包含一个非空字符,不能是一个空格
// message 默认的值在ValidationMessages.properties
@NotBlank(message = "品牌名必须提交",groups = {UpdateGroup.class,AddGroup.class})
private String name;
/**
* 品牌logo地址
*/
@URL(message = "logo必须是一个合法的url地址",groups = {AddGroup.class,UpdateGroup.class})
@NotBlank(groups = {AddGroup.class})
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*
* @ListValue 自定义注解,该值只能是0/1
*/
@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
@ListValue(vals = {0,1},groups = {AddGroup.class, UpdateStatusGroup.class})
private Integer showStatus;
/**
* 检索首字母
* @Pattern 自定义注解,可以传正则表达式
*
* java中指定正则的时候不需要前后两个 / /^[a-zA-Z]$/
*/
@Pattern(regexp = "^[a-zA-Z]$",message = "检索首字母必须是一个字母",groups = {AddGroup.class,UpdateGroup.class})
@NotEmpty(groups = {AddGroup.class})
private String firstLetter;
/**
* 排序
*/
@Min(value = 0,message = "排序必须大于等于0")
@NotNull(groups = {AddGroup.class})
private Integer sort;
}
BrandController
package com.atlinxi.gulimall.product.controller;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import com.atlinxi.common.valid.AddGroup;
import com.atlinxi.common.valid.UpdateGroup;
import com.atlinxi.common.valid.UpdateStatusGroup;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.atlinxi.gulimall.product.entity.BrandEntity;
import com.atlinxi.gulimall.product.service.BrandService;
import com.atlinxi.common.utils.PageUtils;
import com.atlinxi.common.utils.R;
/**
* 品牌
*
* @author linxi
* @email qazokmzjl@gmail.com
* @date 2022-10-12 23:06:12
*/
@RestController
@RequestMapping("product/brand")
public class BrandController {
@Autowired
private BrandService brandService;
/**
* 列表
*/
@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params) {
PageUtils page = brandService.queryPage(params);
return R.ok().put("page", page);
}
/**
* 信息
*/
@RequestMapping("/info/{brandId}")
public R info(@PathVariable("brandId") Long brandId) {
BrandEntity brand = brandService.getById(brandId);
return R.ok().put("brand", brand);
}
/**
* 保存
*
* @Valid 开启校验,不支持分组
* @Validated 开启校验,支持分组
*/
@RequestMapping("/save")
public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand /*,BindingResult result*/) {
// if (result.hasErrors()) {
// Map<String, String> map = new HashMap<>();
// // 1. 获取校验的错误结果
// result.getFieldErrors().forEach((item) -> {
// // FieldError
// // 校验错误之后的提示消息
// String defaultMessage = item.getDefaultMessage();
// // 获取错误的属性的名字
// String field = item.getField();
//
// map.put(field, defaultMessage);
//
//
// });
// return R.error(400, "提交的数据不合法").put("data", map);
// } else {
// brandService.save(brand);
// }
brandService.save(brand);
return R.ok();
}
/**
* 修改
*/
@RequestMapping("/update")
public R update(@Validated({UpdateGroup.class})@RequestBody BrandEntity brand) {
brandService.updateById(brand);
return R.ok();
}
/**
* 修改状态
* @param brand
* @return
*/
@RequestMapping("/update/status")
public R updateStatus(@Validated({UpdateStatusGroup.class})@RequestBody BrandEntity brand) {
brandService.updateById(brand);
return R.ok();
}
/**
* 删除
*/
@RequestMapping("/delete")
public R delete(@RequestBody Long[] brandIds) {
brandService.removeByIds(Arrays.asList(brandIds));
return R.ok();
}
}
自定义校验
自定义注解
package com.atlinxi.common.valid;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
/**
* JSR303 规定校验注解必须有三个属性 message、groups、Payload
* <p>
* 校验注解还必须包含以下元注解
* <p>
*
* 指定校验器
* @Constraint( validatedBy = {}
* )
*/
@Documented
@Constraint(
validatedBy = {ListValueConstraintValidator.class}
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ListValue {
// valid自带的注解是从 ValidationMessages.properties 获取到对应的message
// 我们也建一个配置文件,配置 com.atlinxi.common.valid.ListValue.message
String message() default "{com.atlinxi.common.valid.ListValue.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int[] vals() default {};
}
自定义校验器
package com.atlinxi.common.valid;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;
/**
* 自定义校验器
*
* 第一个泛型指定注解,第二个泛型指定校验数据的类型
* public interface ConstraintValidator<A extends Annotation, T> {
*/
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
private Set<Integer> set = new HashSet<>();
/**
* 初始化
*
* 会将字段的详细信息给到这儿
* @ListValue(vals = {0,1})
*
*
* @param constraintAnnotation
*/
@Override
public void initialize(ListValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
for (int val : vals) {
set.add(val);
}
}
/**
*
* 判断是否校验成功
*
* @param value 前端提交的值,也就是需要校验的值
* @param constraintValidatorContext 上下文环境信息
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
return set.contains(value);
}
}
最终让李国华决心走这一步的是房思琪的自尊心。一个如此精致的小孩是不会说出去的,因为这太脏了。自尊心往往是一根伤人伤己的针,但是在这里,自尊心会缝起她的嘴。
房思琪的初恋乐园
林奕含