说明:本系列笔记总结自雷丰阳老师教学项目《谷粒商城》


一、JSR303校验

问题引入:提交from表单时前端有校验,后端也应该有校验,保证程序安全

如何使用?

1. 给Bean添加校验注解

使用javax.validation.constraints包下的校验注解

  • message为自定义的message提示

  • groups为自定义分组(后面介绍)

image-20220802122700238
<!--参考依赖-->

<!--jsr303参数校验器,里面依赖了hibernate-validator-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<dependency>
    <groupId>jakarta.validation</groupId>
    <artifactId>jakarta.validation-api</artifactId>
    <version>2.0.2</version>
    <scope>compile</scope>
</dependency>

2. 开启Controller层校验

controller中加校验注解@Valid或@Validated,开启校验

二者区别:

  • @Validated是只用Spring Validator校验机制使用

  • @Valid是使用Hibernate validation的时候使用

  • @Validated:用在类型、方法和方法参数上。但不能用于成员属性(field)

  • @Valid:可以用在方法、构造函数、方法参数和成员属性(field)上

  • @Validated:提供分组功能,可以在参数验证时,根据不同的分组采用不同的验证机制

  • @Valid:没有分组功能

详细介绍:CSND

这里使用@Validated注解实现

@Validation@Valid进行了二次封装,在使用上并没有区别,但在分组、注解位置、嵌套验证等功能上有所不同

效果:校验错误以后会有其他响应

给校验的bean后紧跟一个BindingResult,就可以获取到校验的结果

@RequestMapping("/save")
public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand , BindingResult result ){
    //后面介绍使用自定义全局异常处理,有了@Valid注解会抛出相应异常被自定义异常处理类捕获
    if (result.hasErrors()) {
        HashMap<String, String> map = new HashMap<>();
        result.getFieldErrors().forEach(error->{
            map.put("错误消息",error.getDefaultMessage());   //获取发生错误时的message
            map.put("字段名",error.getField());              //获取发生错误的字段
        });
        return R.error(400,"提交的数据不合法!").put("data",map);
    }else {
        brandService.save(brand);
    }
    return R.ok();
}

3. 分组校验

多场景的复杂校验,比如分为查询场景、修改场景,不同情况使不同的Bean校验注解生效

  • 编写校验分组(一组一个接口)
image-20220802125254816
package com.qiandao.common.valid;

public interface AddGroup {
}
package com.qiandao.common.valid;

public interface UpdateGroup {
}
  • 在Bean字段上添加校验注解
@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;
	/**
	 * 品牌名
	 */
	@NotBlank(message = "品牌名不能为空",groups = {AddGroup.class})
	private String name;
	/**
	 * 品牌logo地址
	 */
	@NotBlank(message = "品牌logo不能为空",groups = {AddGroup.class})
	@URL(message = "不是一个合法的url地址",groups = {AddGroup.class,UpdateGroup.class})
	private String logo;
	/**
	 * 介绍
	 */
	@NotBlank(message = "描述不能为空",groups = {AddGroup.class})
	private String descript;
	/**
	 * 显示状态[0-不显示;1-显示]
	 */
	@NotNull(message = "状态不能为空")
	@Max(value = 1)
	@Min(value = 0)
	@PositiveOrZero
	@NotNull(groups = {AddGroup.class, UpdateGroup.class})
	// @ListValue(vals={0,1},groups = {AddGroup.class, UpdateGroup.class})	//自定义注解,见后文
	private Integer showStatus;
	/**
	 * 检索首字母
	 */
	@NotBlank(message = "索检首字母不能为空",groups = {AddGroup.class})
	@Pattern(regexp = "^[A-Za-z]$",message = "必须是一个字母",groups = {AddGroup.class,UpdateGroup.class})
	private String firstLetter;
	/**
	 * 排序
	 */
	@NotNull(message = "排序不能为空",groups = {AddGroup.class})
	@PositiveOrZero(message = "排序要大于等于零且为整数",groups = {AddGroup.class,UpdateGroup.class})
	private Integer sort;
}

指定了分组校验后,该注解只会在执行相应的方法使才会生效

默认没有指定分组的校验注解@NotBlank,在分组校验情况@Validated({AddGroup.class})下不生效,只会在@Validated生效

  • 在Controller层中需额外添加分组信息`@Validated(AddGroup.class)
@RequestMapping("/save")
public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand , BindingResult result ){...}

4.Bean校验注解

空检查
@Null 验证对象是否为null
@NotNull 验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty 检查约束元素是否为NULL或者是EMPTY.
Booelan检查
@AssertTrue 验证 Boolean 对象是否为 true
@AssertFalse 验证 Boolean 对象是否为 false
长度检查
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
@Length(min=, max=) Validates that the annotated string is between min and max included.
日期检查
@Past 验证 Date 和 Calendar 对象是否在当前时间之前
@Future 验证 Date 和 Calendar 对象是否在当前时间之后
@Pattern 验证 String 对象是否符合正则表达式的规则
数值检查 建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为Stirng为"",Integer为null
@Min 验证 Number 和 String 对象是否大等于指定的值
@Max 验证 Number 和 String 对象是否小等于指定的值
@DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
@DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
@Digits 验证 Number 和 String 的构成是否合法
@Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。
@Range(min=, max=) 检查数字是否介于min和max之间.
@Range(min=10000,max=50000,message=“range.bean.wage”) private BigDecimal wage;
@Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证)
@CreditCardNumber 信用卡验证
@Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。
@ScriptAssert(lang= ,script=, alias=)
@URL(protocol=,host=, port=,regexp=, flags=)

5.自定义校验规则

  • 编写一个自定义的校验注解
  • 编写一个自定义的校验器 ConstraintValidator
  • 关联自定义的校验器和自定义的校验注解

Hibernate Validator提供了一系列内置的校验注解,可以满足大部分的校验需求。但是,仍然有一部分校验需要特殊定制,例如某个字段的校验,我们提供两种校验强度,当为normal强度时我们除了<>号之外,都允许出现。当为strong强度时,我们只允许出现常用汉字,数字,字母。内置的注解对此则无能为力,我们试着通过自定义校验来解决这个问题。

场景:要校验showStatus的0/1状态,可以用正则,但我们可以利用其他方式解决复杂场景。比如我们想要下面的场景

/**
 * 显示状态[0-不显示;1-显示]
 */
@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
@ListValue(vals = {0,1}, groups = {AddGroup.class, UpdateGroup.class, UpdateStatusGroup.class})
private Integer showStatus;

添加依赖

<!--校验-->
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>7.0.4.Final</version>
</dependency>
<!--有的版本需要javax.el-->
<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>javax.el</artifactId>
    <version>3.0.1-b08</version>
</dependency>

(1)自定义校验注解

必须有3个属性

  • message()错误信息
  • groups()分组校验
  • payload()自定义负载信息
// 自定义注解
@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class}) // 校验器
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) // 哪都可以标注
@Retention(RUNTIME)
public @interface ListValue {
    // 使用该属性自动去Validation.properties配置文件中取
    String message() default "{com.qiandao.common.valid.ListValue.message}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    // 数组,需要用户自己指定
    int[] vals() default {};
}
com.qiandao.common.valid.ListValue.message=排序只能输入0或1

(2)自定义校验器

上面只是定义了异常消息,但是怎么验证是否异常还没说,下面的ConstraintValidator就是说的

比如我们要限定某个属性值必须在一个给定的集合里,那么就通过重写initialize()方法,指定可以有哪些元素。

而controller接收到的数据用isValid验证

public class ListValueConstraintValidator 
    implements ConstraintValidator<ListValue,Integer> { //<注解,校验值类型>
    
    // 存储所有可能的值
    private Set<Integer> set=new HashSet<>();
    
    @Override // 初始化,你可以获取注解上的内容并进行处理
    public void initialize(ListValue constraintAnnotation) {
        // 获取后端写好的限制 // 这个value就是ListValue里的value,我们写的注解是@ListValue(value={0,1})
        int[] value = constraintAnnotation.value();
        for (int i : value) {
            set.add(i);
        }
    }

    @Override // 覆写验证逻辑
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        // 看是否在限制的值里
        return  set.contains(value);
    }
}

具体的校验类需要实现ConstraintValidator接口,第一个泛型参数是所对应的校验注解类型,第二个是校验对象类型。在初始化方法initialize中,我们可以先做一些别的初始化工作,例如这里我们获取到注解上的value并保存下来,然后生成set对象。

真正的验证逻辑由isValid完成,如果传入形参的属性值在这个set里就返回true,否则返回false

(3)关联校验器和校验注解

@Constraint(validatedBy = { ListValueConstraintValidator.class})

一个校验注解可以匹配多个校验器

(4)使用实例

/**
 * 显示状态[0-不显示;1-显示]
   用value[]指定可以写的值
 */
@ListValue(value = {0,1},groups ={AddGroup.class})
private Integer showStatus;

如验证手机号格式,可以参考https://blog.csdn.net/GAMEloft9/article/details/81699500

二、异常处理

1. 全局异常捕获

全局接口异常处理的类,当发生异常没有捕获时,便会触发这个异常

@RestControllerAdvice是一个组合注解,由@ControllerAdvice、@ResponseBody组成,而@ControllerAdvice继承了@Component,因此@RestControllerAdvice本质上是个Component,用于定义@ExceptionHandler,@InitBinder和@ModelAttribute方法,适用于所有使用@RequestMapping方法。

@RestControllerAdvice的特点:

  • 通过@ControllerAdvice注解可以将对于控制器的全局配置放在同一个位置。
  • 注解了@RestControllerAdvice的类的方法可以使用@ExceptionHandler、@InitBinder、@ModelAttribute注解到方法上。
  • @RestControllerAdvice注解将作用在所有注解了@RequestMapping的控制器的方法上。
  • @ExceptionHandler:用于指定异常处理方法。当与@RestControllerAdvice配合使用时,用于全局处理控制器里的异常。
  • @InitBinder:用来设置WebDataBinder,用于自动绑定前台请求参数到Model中。
  • @ModelAttribute:本来作用是绑定键值对到Model中,当与@ControllerAdvice配合使用时,可以让全局的@RequestMapping都能获得在此处设置的键值对

全局异常类捕获处理方法

@Slf4j
//@ResponseBody
//@ControllerAdvice(basePackages = "com.qiandao.gulimall.product.controller")//捕获异常对象的包
@RestControllerAdvice(basePackages = "com.qiandao.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {

    //使用@ExceptionHandler标注方法可以处理的异常
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public R handleVaildException(MethodArgumentNotValidException e,HttpServletRequest request) {
        log.error("数据校验出现异常:{},异常类型:{},请求的url:{}",e.getMessage(),e.getClass(),request.getRequestURL());
        BindingResult bindingResult = e.getBindingResult();

        HashMap<String , String> errorMap = new HashMap<>();
        bindingResult.getFieldErrors().forEach(error->{
            errorMap.put("错误消息",error.getDefaultMessage());
            errorMap.put("校验字段",error.getField());
        });
        return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(),BizCodeEnume.VAILD_EXCEPTION.getMsg()).put("data",errorMap);
    }

    //全局最大异常捕获,返回未知异常
    @ExceptionHandler(value = Throwable.class)
    public R exception(Throwable e) {
        log.error("系统未知异常:{},异常类型:{},栈信息:{}",e.getMessage(),e.getClass(),e.getStackTrace());
        return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(), BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
    }
}

测试上文的JSR303注解校验

@RequestMapping("/save")
public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand){
    //字段校验不通过时便会跑出异常被GulimallExceptionControllerAdvice方法捕获
    //转到自定义全局异常处处理,有了@Valid注解会抛出相应异常被自定义异常处理类捕获
    brandService.save(brand);
    return R.ok();
}

2. 自定义异常类

独立编写每一个异常,只需要继承RuntimeException类:

image-20220802145141221
package com.qiandao.gulimall.member.exception;

public class PhoneExistException extends RuntimeException{

    public PhoneExistException() {
        super("该手机号码已注册");
    }
}
package com.qiandao.gulimall.member.exception;

public class UserNameExistException extends RuntimeException {

    public UserNameExistException(){
        super("用户名已存在");
    }
}

实际业务使用throw抛出异常即可:

@Override
public void checkPhoneUnique(String phone) {
    if (baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone)) > 0)
        throw new PhoneExistException();
}

3. 自定义异常工具类

public class RRException extends RuntimeException {
	private static final long serialVersionUID = 1L;
	
    private String msg;
    private int code = 500;
    
    public RRException(String msg) {
		super(msg);
		this.msg = msg;
	}
	
	public RRException(String msg, Throwable e) {
		super(msg, e);
		this.msg = msg;
	}
	
	public RRException(String msg, int code) {
		super(msg);
		this.msg = msg;
		this.code = code;
	}
	
	public RRException(String msg, int code, Throwable e) {
		super(msg, e);
		this.msg = msg;
		this.code = code;
	}

	public String getMsg() {
		return msg;
	}

	public void setMsg(String msg) {
		this.msg = msg;
	}

	public int getCode() {
		return code;
	}

	public void setCode(int code) {
		this.code = code;
	}	
}

业务使用,不用再编写具体的异常抛出类:

@Override
public void checkUserNameUnique(String username) {
    if (baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("username", username)) > 0) {
        //throw new UserNameExistException();
        throw new RRException("用户名已存在");
    }
}

三、全局返回处理

1. 全局返回类型

整个项目后端接口返回统一的数据对象R,该对象是HashMap类型

定义R对象:

public class R extends HashMap<String, Object> {
	private static final long serialVersionUID = 1L;

	public R setData(Object data) {
		put("data",data);
		return this;
	}

	public Object getData(){		
		return get("data");
	}

/*下面的操作有问题,这里遇到实际类型在业务代码中手动处理*/
//	//利用fastjson进行反序列化
//	public <T> T getData(TypeReference<T> typeReference) {
//		Object data = get("data");	//默认是map
//		String jsonString = JSON.toJSONString(data);
//		T t = JSON.parseObject(jsonString, (Type) typeReference);
//		return t;
//	}
//
//	//利用fastjson进行反序列化
//	public <T> T getData(String key,TypeReference<T> typeReference) {
//		Object data = get(key);	//默认是map
//		String jsonString = JSON.toJSONString(data);
//		T t = JSON.parseObject(jsonString, (Type) typeReference);
//		return t;
//	}

	public R() {
		put("code", 0);
		put("msg", "success");
	}

	public static R error() {
		return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
	}

	public static R error(String msg) {
		return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
	}

	public static R error(int code, String msg) {
		R r = new R();
		r.put("code", code);
		r.put("msg", msg);
		return r;
	}

	public static R ok(String msg) {
		R r = new R();
		r.put("msg", msg);
		return r;
	}

	public static R ok(Map<String, Object> map) {
		R r = new R();
		r.putAll(map);
		return r;
	}

	public static R ok() {
		return new R();
	}

	public R put(String key, Object value) {
		super.put(key, value);
		return this;
	}

	public Integer getCode() {

		return (Integer) this.get("code");
	}
}

使用实例:

@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params){
    PageUtils page = attrAttrgroupRelationService.queryPage(params);
    return R.ok().put("page", page);
}

@RequestMapping("/save")
public R save(@RequestBody AttrVo attr){
    attrService.saveAttr(attr);
    return R.ok();
}

@ExceptionHandler(value = Throwable.class)
public R exception(Throwable e) {
    log.error("系统未知异常:{},异常类型:{},栈信息:{}",e.getMessage(),e.getClass(),e.getStackTrace());
    return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(), BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
}

@GetMapping("/lates3DaySession")
public  R getLates3DaySession(){
    List<SeckillSessionEntity> sessions = seckillSessionService.getLates3DaySession();
    return R.ok().setData(sessions);	//这里放(Feign远程调用)
}

@Override
public void upSeckillSkuLatest3Days() {
    R session = couponFeignService.getLates3DaySession();
    if (session.getCode() == 0) {
        Object data = session.get("data");	//这里取(Feign远程调用)
        String s = JSON.toJSONString(data);
        List<SeckillSessionsWithSkus> sessionData = JSON.parseObject(s, new TypeReference<>() {
        });
        saveSessionInfos(sessionData);
        saveSessionSkuInfos(sessionData);
    }
}

2. 全局分页对象

结合MybatisPlus使用

引入分页工具类,封装查询到的数据信息

public class PageUtils implements Serializable {
	private static final long serialVersionUID = 1L;
	/**
	 * 总记录数
	 */
	private int totalCount;
	/**
	 * 每页记录数
	 */
	private int pageSize;
	/**
	 * 总页数
	 */
	private int totalPage;
	/**
	 * 当前页数
	 */
	private int currPage;
	/**
	 * 列表数据
	 */
	private List<?> list;
	
	/**
	 * 分页
	 * @param list        列表数据
	 * @param totalCount  总记录数
	 * @param pageSize    每页记录数
	 * @param currPage    当前页数
	 */
	public PageUtils(List<?> list, int totalCount, int pageSize, int currPage) {
		this.list = list;
		this.totalCount = totalCount;
		this.pageSize = pageSize;
		this.currPage = currPage;
		this.totalPage = (int)Math.ceil((double)totalCount/pageSize);
	}

	/**
	 * 分页
	 */
	public PageUtils(IPage<?> page) {
		this.list = page.getRecords();
		this.totalCount = (int)page.getTotal();
		this.pageSize = (int)page.getSize();
		this.currPage = (int)page.getCurrent();
		this.totalPage = (int)page.getPages();
	}

	public int getTotalCount() {
		return totalCount;
	}

	public void setTotalCount(int totalCount) {
		this.totalCount = totalCount;
	}

	public int getPageSize() {
		return pageSize;
	}

	public void setPageSize(int pageSize) {
		this.pageSize = pageSize;
	}

	public int getTotalPage() {
		return totalPage;
	}

	public void setTotalPage(int totalPage) {
		this.totalPage = totalPage;
	}

	public int getCurrPage() {
		return currPage;
	}

	public void setCurrPage(int currPage) {
		this.currPage = currPage;
	}

	public List<?> getList() {
		return list;
	}

	public void setList(List<?> list) {
		this.list = list;
	}	
}

引入分页查询工具类,其中封装了前端传递的各种分页参数,即通过此工具类创建MybatisPlus的查询对象page

//MybatisPlus分页查询源码
default <E extends IPage<T>> E page(E page, Wrapper<T> queryWrapper) {
        return getBaseMapper().selectPage(page, queryWrapper);
}
public class Query<T> {

    public IPage<T> getPage(Map<String, Object> params) {
        return this.getPage(params, null, false);
    }

    public IPage<T> getPage(Map<String, Object> params, String defaultOrderField, boolean isAsc) {
        //分页参数
        long curPage = 1;
        long limit = 10;

        if(params.get(Constant.PAGE) != null){
            curPage = Long.parseLong((String)params.get(Constant.PAGE));
        }
        if(params.get(Constant.LIMIT) != null){
            limit = Long.parseLong((String)params.get(Constant.LIMIT));
        }

        //分页对象
        Page<T> page = new Page<>(curPage, limit);

        //分页参数
        params.put(Constant.PAGE, page);

        //排序字段
        //防止SQL注入(因为sidx、order是通过拼接SQL实现排序的,会有SQL注入风险)
        String orderField = SQLFilter.sqlInject((String)params.get(Constant.ORDER_FIELD));
        String order = (String)params.get(Constant.ORDER);


        //前端字段排序
        if(StringUtils.isNotEmpty(orderField) && StringUtils.isNotEmpty(order)){
            if(Constant.ASC.equalsIgnoreCase(order)) {
                return  page.addOrder(OrderItem.asc(orderField));
            }else {
                return page.addOrder(OrderItem.desc(orderField));
            }
        }

        //没有排序字段,则不排序
        if(StringUtils.isBlank(defaultOrderField)){
            return page;
        }

        //默认排序
        if(isAsc) {
            page.addOrder(OrderItem.asc(defaultOrderField));
        }else {
            page.addOrder(OrderItem.desc(defaultOrderField));
        }

        return page;
    }
}

几个常量:

/**
 * 当前页码
 */
public static final String PAGE = "page";
/**
 * 每页显示记录数
 */
public static final String LIMIT = "limit";
/**
 * 排序字段
 */
public static final String ORDER_FIELD = "sidx";
/**
 * 排序方式
 */
public static final String ORDER = "order";
/**
 *  升序
 */
public static final String ASC = "asc";

前端传递的各种分页参数:

  • page: 1 //当前页码
  • limit: 10 //每页记录数
  • sidx: ‘id’ //排序字段
  • order: ‘asc/desc’ //排序方式

使用实例:

//Controller层
@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params){
    PageUtils page = couponService.queryPage(params);

    return R.ok().put("page", page);
}

//接口
PageUtils queryPage(Map<String, Object> params);

//Service层
@Override
public PageUtils queryPage(Map<String, Object> params) {
    IPage<CouponEntity> page = this.page(
            new Query<CouponEntity>().getPage(params),	//如果有排序`getPage(params,'排序字段','asc/desc')`
            new QueryWrapper<CouponEntity>()
    );

    return new PageUtils(page);		//通过工具类封装查询到的结果
}

前端查询:

{
   page: 1,//当前页码
   limit: 10,//每页记录数
   sidx: 'id',//排序字段
   order: 'asc/desc',//排序方式
   //key: '华为'//检索关键字
}

后端返回:

{
	"msg": "success",
	"code": 0,
	"page": { 
		"totalCount": 0, //总记录数
		"pageSize": 10,  //每页大小
		"totalPage": 0,  //总页码
		"currPage": 1, //当前页码
		"list": [{  //当前页所有数据
			"brandId": 1,
			"name": "aaa",
			"logo": "abc",
			"descript": "华为",
			"showStatus": 1,
			"firstLetter": null,
			"sort": null
		}]
	}
}

三、 全局其他处理

1. 全局跨域处理

前端调用后台接口出现问题:

:8001/#/login:1 Access to XMLHttpRequest at 'http://localhost:88/api/sys/login' from origin 'http://localhost:8001' has been blocked by `CORS` policy: Response to preflight request doesn't pass access control check: No '`Access-Control-Allow-Origin`' header is present on the requested resource.

从8001访问88,引发CORS跨域请求,浏览器会拒绝跨域请求。具体来说当前页面是8001端口,但是要跳转88端口,这是不可以的(post请求json可以)

问题描述:已拦截跨源请求:同源策略禁止8001端口页面读取位于 http://localhost:88/api/sys/login 的远程资源。(原因:CORS 头缺少 ‘Access-Control-Allow-Origin’)。

问题分析:这是一种跨域问题。访问的域名或端口和原来请求的域名端口一旦不同,请求就会被限制

  • 跨域:指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对js施加的安全限制。(ajax可以)
  • 同源策略:是指协议,域名,端囗都要相同,其中有一个不同都会产生跨域;
URL 说明 是否允许通信
http://www.a.com/a.js
http://www.a.com/b.js
同一域名下 允许
http://www.a.com/lab/a.js
http://www.a.com/script/b.js
同一域名下不同文件夹 允许
http://www.a.com:8000/a.js
http://www.a.com/b.js
同一域名,不同端口 不允许
http://www.a.com/a.js
https://www.a.com/b.js
同一域名,不同协议 不允许
http://www.a.com/a.js
http://70.32.92.74/b.js
域名和域名对应ip 不允许
http://www.a.com/a.js
http://script.a.com/b.js
主域相同,子域不同 不允许
http://www.a.com/a.js
http://a.com/b.js
同一域名,不同二级域名(同上) 不允许(cookie这种情况下也不允许访问)
http://www.cnblogs.com/a.js
http://www.a.com/b.js
不同域名 不允许

跨域流程:

这个跨域请求的实现是通过预检请求实现的,先发送一个OPSTIONS探路,收到响应允许跨域后再发送真实请求

什么意思呢?跨域是要请求的、新的端口那个服务器限制的,不是浏览器限制的。

跨域请求流程:
非简单请求(PUT、DELETE)等,需要先发送预检请求


       -----1、预检请求、OPTIONS ------>
       <----2、服务器响应允许跨域 ------
浏览器 |                               |  服务器
       -----3、正式发送真实请求 -------->
       <----4、响应数据   --------------

跨域的解决方案

  • 方法1:设置nginx包含admin和gateway。都先请求nginx,这样端口就统一了
  • 方法2:让服务器告诉预检请求能跨域
image-20220805133135476

解决方法二

在响应头中添加:参考

  • Access-Control-Allow-Origin : 支持哪些来源的请求跨域
  • Access-Control-Allow-Method : 支持那些方法跨域
  • Access-Control-Allow-Credentials :跨域请求默认不包含cookie,设置为true可以包含cookie
  • Access-Control-Expose-Headers : 跨域请求暴露的字段
    • CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:
      Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma
      如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
  • Access-Control-Max-Age :表明该响应的有效时间为多少秒。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将失效

解决方法:在网关服务中定义GulimallCorsConfiguration类,该类用来做过滤,允许所有的请求跨域。

@Configuration // gateway
public class GulimallCorsConfiguration {

    @Bean // 添加过滤器
    public CorsWebFilter corsWebFilter(){
        // 基于url跨域,选择reactive包下的
        UrlBasedCorsConfigurationSource source=new UrlBasedCorsConfigurationSource();
        // 跨域配置信息
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 允许跨域的头
        corsConfiguration.addAllowedHeader("*");
        // 允许跨域的请求方式,允许任何方法
        corsConfiguration.addAllowedMethod("*");
        // 允许跨域的请求来源,允许任何域名
        corsConfiguration.addAllowedOriginPattern("*");
        //预检请求的有效期,单位是秒
        corsConfiguration.setMaxAge(3600L);
        // 是否允许携带cookie跨域、是否支持安全证书
        corsConfiguration.setAllowCredentials(true);

        // 任意url都要进行跨域配置
        source.registerCorsConfiguration("/**",corsConfiguration);
        return new CorsWebFilter(source);
    }
}

2. 全局状态码

在common中以枚举类的形式定义

示例:

//全局异常状态码
public enum BizCodeEnume {

    UNKNOW_EXCEPTION(10000,"系统未知异常"),
    VAILD_EXCEPTION(10001,"参数格式校验失败"),
    SMS_CODE_EXCEPTION(10002,"发送验证码太频繁,请稍后再试"),
    TOO_MANY_REQUEST(10003,"请求流量过大"),
    PRODUCT_UP_EXCEPTION(11000, "商品上架异常"),
    PHONE_NULL_EXCEPTION(15003,"未输入手机号码"),
    USER_EXIST_EXCEPTION(15001,"用户已存在"),
    PHONE_EXIST_EXCEPTION(15002,"手机号码已存在"),
    LOGINACCT_PASSWORD_INVAILD_EXCEPTION(15002,"账号或密码错误"),
    PURCHASING_EXCEPTION(20000,"正在采购,无法进行分配"),
    NO_STOCK_EXCEPTION(21000,"商品库存不足");

    private int code;
    private String msg;

    BizCodeEnume(int code,String msg) {
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }

}
//库存状态
public class WareConstant {

    public enum PurchaseStatusEnum {
        CREATED(0,"新建"),
        ASSIGNED(1,"已分配"),
        RECEIVE(2,"已领取"),
        FINISH(3,"已完成"),
        HASERROR(4,"有异常"),

        ;

        private int code;

        private String msg;

        public int getCode() {
            return code;
        }

        public String getMsg() {
            return msg;
        }

        PurchaseStatusEnum(int code, String msg) {
            this.code = code;
            this.msg = msg;
        }

    }

    public enum PurchaseDetailStatusEnum {
        CREATED(0,"新建"),
        ASSIGNED(1,"已分配"),
        BUYING(2,"正在采购"),
        FINISH(3,"已完成"),
        HASERROR(4,"采购失败"),

        ;

        private int code;

        private String msg;

        public int getCode() {
            return code;
        }

        public String getMsg() {
            return msg;
        }

        PurchaseDetailStatusEnum(int code, String msg) {
            this.code = code;
            this.msg = msg;
        }
    }
    
}