说明:本系列笔记总结自
雷丰阳
老师教学项目《谷粒商城》
- 视频地址:直达BiliBili
- 完整项目地址:直达gitee
- 项目资料获取:
一、JSR303校验
问题引入:提交from表单时前端有校验,后端也应该有校验,保证程序安全
如何使用?
1. 给Bean添加校验注解
使用
javax.validation.constraints
包下的校验注解
message
为自定义的message提示
groups
为自定义分组(后面介绍)
<!--参考依赖-->
<!--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校验注解生效
- 编写校验分组(一组一个接口)
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 | 信用卡验证 |
验证是否是邮件地址,如果为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
类:
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:让服务器告诉预检请求能跨域
解决方法二
在响应头中添加:参考
- 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里面指定。
- CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:
- 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;
}
}
}