【雷丰阳-谷粒商城 】【分布式基础篇-全栈开发篇】【04】跨域_OSS_后端校验


持续学习&持续更新中…

学习态度:守破离


跨域

问题

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS

在这里插入图片描述

在这里插入图片描述

解决

在这里插入图片描述

在这里插入图片描述

实现

gulimall-gateway:

spring:
  cloud:
    gateway:
      routes:
        - id: product_route
          uri: lb://gulimall-product
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}
        #需要把精确的路由放到上面【/api/product/**比/api/**更精确】
        - id: admin_route
          uri: lb://renren-fast
          predicates:
            - Path=/api/**
          filters:
            - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}

## 前端项目,/api
## http://localhost:88/api/captcha.jpg   http://localhost:8080/renren-fast/captcha.jpg
## http://localhost:88/api/product/category/list/tree http://localhost:10000/product/category/list/tree

#        - id: qq_route
#          uri: https://www.qq.com
#          predicates:
#            - Query=url,qq
#        - id: bing
#          uri: https://cn.bing.com/
#          predicates:
#            - Query=url,bing
@Configuration
public class GulimallCorsConfiguration {
    @Bean
    public CorsWebFilter corsWebFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 配置跨域
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.addAllowedOrigin("*"); // 请求来源
        corsConfiguration.setAllowCredentials(true); // cookie
        source.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsWebFilter(source);
    }
}

逻辑删除

数据库:

在这里插入图片描述

application.yml:【全局设置】

mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
      logic-delete-field: deleted # 全局逻辑删除的实体字段名
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

Model:【局部设置】

@Data
@TableName("pms_category")
public class CategoryEntity implements Serializable {
	// ...
    /**
     * 是否显示[0-不显示,1显示]
     */
    @TableLogic(value = "1", delval = "0")
    private Integer showStatus;
}

Controller:

@RestController
@RequestMapping("product/category")
public class CategoryController {
    /**
     * 删除
     * @RequestBody:获取请求体,必须发送POST请求
     * SpringMVC自动将请求体的数据(json),转为对应的对象
     */
    @RequestMapping("/delete")
    //@RequiresPermissions("product:category:delete")
    public R delete(@RequestBody Long[] catIds){

        //categoryService.removeByIds(Arrays.asList(catIds));

        categoryService.removeMenuByIds(Arrays.asList(catIds));

        return R.ok();
    }
}

ServiceImpl:

@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
    @Override
    public void removeMenuByIds(List<Long> asList) {
        //TODO 检查当前删除的菜单,是否被别的地方引用
        
        //逻辑删除
        baseMapper.deleteBatchIds(asList);
    }
}

文件存储

普通上传

在这里插入图片描述

在这里插入图片描述
如果下一次负载均衡到了其他服务器怎么办?

云存储

在这里插入图片描述

阿里云OSS

https://www.aliyun.com/product/oss
https://help.aliyun.com/product/31815.html

简介

阿里云对象存储OSS(Object Storage Service)为您提供基于网络的数据存取服务。使用OSS,您可以通过网络随时存储和调用包括文本、图片、音视频在内的各类数据文件。
OSS视频简介:https://static-aliyun-doc.oss-cn-hangzhou.aliyuncs.com/file-manage-files/zh-CN/20220727/ttob/OSS%E6%96%B0%E7%89%88%E4%BA%A7%E5%93%81%E4%BB%8B%E7%BB%8D%EF%BC%88%E4%B8%AD%E6%96%87%EF%BC%89.mp4

  • 阿里云对象存储OSS(Object Storage Service)是一款海量、安全、低成本、高可靠的云存储服务,可提供99.9999999999%(12个9)的数据持久性,99.995%的数据可用性。多种存储类型供选择,全面优化存储成本。

  • OSS具有与平台无关的RESTful API接口,您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。

  • 您可以使用阿里云提供的API、SDK接口或者OSS迁移工具轻松地将海量数据移入或移出阿里云OSS。数据存储到阿里云OSS以后,您可以选择标准存储(Standard)作为移动应用、大型网站、图片分享或热点音视频的主要存储方式,也可以选择成本更低、存储期限更长的低频访问存储(Infrequent Access)、归档存储(Archive)、冷归档存储(Cold Archive)作为不经常访问数据的存储方式。

术语

https://help.aliyun.com/zh/oss/product-overview/terms-2

中文英文说明
存储空间Bucket存储空间是您用于存储对象(Object)的容器,所有的对象都必须隶属于某个存储空间。
对象/文件Object对象是 OSS 存储数据的基本单元,也被称为OSS的文件。对象由元信息(Object Meta)、用户数据(Data)和文件名(Key)组成。对象由存储空间内部唯一的Key来标识。
地域Region地域表示 OSS 的数据中心所在物理位置。您可以根据费用、请求来源等综合选择数据存储的地域。详情请查看OSS已经开通的Region。
访问域名EndpointEndpoint 表示OSS对外服务的访问域名。OSS以HTTP RESTful API的形式对外提供服务,当访问不同地域的时候,需要不同的域名。通过内网和外网访问同一个地域所需要的域名也是不同的。具体的内容请参见各个Region对应的Endpoint。
访问密钥AccessKeyAccessKey,简称 AK,指的是访问身份验证中用到的AccessKeyId 和AccessKeySecret。OSS通过使用AccessKeyId 和AccessKeySecret对称加密的方法来验证某个请求的发送者身份。AccessKeyId用于标识用户,AccessKeySecret是用户用于加密签名字符串和OSS用来验证签名字符串的密钥,其中AccessKeySecret 必须保密。

建议:一个项目创建一个Bucket

简单使用

https://help.aliyun.com/zh/oss/developer-reference/getting-started?spm=a2c4g.11186623.0.i9#concept-32011-zh

  • 创建一个AccessKey(使用子账户AccessKey),只用于“OpenAPI 调用访问”,并分配“AliyunOSSFullAccess”权限
    在这里插入图片描述

  • 创建一个Bucket,存储类型为“低频访问存储”,读写权限为“公共读”
    在这里插入图片描述

注意:

在这里插入图片描述

  • 简单的文件上传:

            <dependency>
                <groupId>com.aliyun.oss</groupId>
                <artifactId>aliyun-sdk-oss</artifactId>
                <version>3.5.0</version>
            </dependency>
    
        @Test
        public void testUpload() throws Exception {
            // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
            String endpoint = "xxx";
            // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
            String accessKeyId = "xxx";
            String accessKeySecret = "xxx";
            // 填写Bucket名称,例如examplebucket。
            String bucketName = "xxx";
            // 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
            String objectName = "mengwa.jpg";
            // 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
            // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
            String filePath = "C:\\Users\\lpruoyu\\Pictures\\Saved Pictures\\mengwa.jpg";
    
            // 创建OSSClient实例。
            OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
    
            try {
                InputStream inputStream = new FileInputStream(filePath);
                // 创建PutObject请求。
                ossClient.putObject(bucketName, objectName, inputStream);
            } catch (OSSException oe) {
                System.out.println("Caught an OSSException, which means your request made it to OSS, "
                        + "but was rejected with an error response for some reason.");
                System.out.println("Error Message:" + oe.getErrorMessage());
                System.out.println("Error Code:" + oe.getErrorCode());
                System.out.println("Request ID:" + oe.getRequestId());
                System.out.println("Host ID:" + oe.getHostId());
            } catch (ClientException ce) {
                System.out.println("Caught an ClientException, which means the client encountered "
                        + "a serious internal problem while trying to communicate with OSS, "
                        + "such as not being able to access the network.");
                System.out.println("Error Message:" + ce.getMessage());
            } finally {
                if (ossClient != null) {
                    ossClient.shutdown();
                }
            }
        }
    

使用SpringCloudAlibaba—oss

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
        </dependency>
spring:
  cloud:
    alicloud:
      access-key: xxx
      secret-key: xxx
      oss:
        endpoint: xxx
    @Autowired
    OSSClient ossClient;

    @Test
    public void testUpload() throws Exception {
        String bucketName = "xxx";
        String objectName = "chumen.png";
        String filePath = "C:\\Users\\lpruoyu\\Pictures\\Saved Pictures\\chumen.png";
        try {
            InputStream inputStream = new FileInputStream(filePath);
            // 创建PutObject请求。
            ossClient.putObject(bucketName, objectName, inputStream);
        } catch (Exception e) {
            System.out.println("Exception:" + e);
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }

服务端签名后直传

普通上传方式:

浪费资源,耗费时间,上传文件还得过我们自己的服务器

在这里插入图片描述

服务端签名后直传:

我们要上传给OSS,肯定是有账号密码的,这些肯定不能暴露出去;

所以,使用第一种普通上传方式的方式是比较安全的

我们不能把账号密码写在前端中,这样会暴露隐私。

我们可以使用服务端签名后上传:(Policy是利用账号密码等生成的防伪签名)
在这里插入图片描述

在这里插入图片描述

好处就是文件不用占用我们服务器的资源和带宽了!因为文件处理是很慢的,会影响服务器处理别的请求。

在这里插入图片描述

服务端签名直传无需将访问密钥暴露在前端页面,具有更高的安全性。

在这里插入图片描述

实现:

创建跨域规则:

在这里插入图片描述

阿里云OSS配置:

spring:
  cloud:
    alicloud:
      access-key: xxxxxxxxxxxxx
      secret-key: yyyyyyyyyyyy
      oss:
        endpoint: oss-cn-hangzhou.aliyuncs.com
        bucket: gulimall-hellolp

#  用户登录名称 gulimall-lpruoyu@1043669636695115.onaliyun.com
# AccessKey ID xxxxxxxxxxxxx
# AccessKey Secret yyyyyyyyyyyy

网关配置:

server:
  port: 88
spring:
  application:
    name: gulimall-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
      config:
        server-addr: 127.0.0.1:8848
        namespace: 52102b61-d205-44ee-bf6a-0249dc55dd73
        group: dev
        file-extension: yml
# nacos中不同命名空间的服务不能直接调用,可以使用其他方法
#        namespace: 52102b61-d205-44ee-bf6a-0249dc55dd73
#        group: dev
#        file-extension: yml
    gateway:
      routes:
        - id: product_route
          uri: lb://gulimall-product
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}
          #需要把精确的路由放到上面【/api/product/**比/api/**更精确】

        - id: third_party_route
          uri: lb://gulimall-third-party
          predicates:
            - Path=/api/thirdparty/**
          filters:
            - RewritePath=/api/thirdparty/(?<segment>.*),/$\{segment}

        - id: admin_route
          uri: lb://renren-fast
          predicates:
            - Path=/api/**
          # 规则:前端项目都带上/api/前缀
          # 默认是这样子转发的 http://localhost:88/api/captcha.jpg  ==》 http://renrenfast/api/captcha.jpg
          #renrenfast 代表 renren-fast的url+port 也就是:localhost:8080
          # 希望这样子转发 http://renrenfast/renren-fast/captcha.jpg
          filters:
            - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}

#spring:
#  cloud:
#    gateway:
#      routes:
#        - id: test_route
#          uri: https://www.baidu.com
#          predicates:
#            - Query=url,baidu
#        - id: qq_route
#          uri: https://www.qq.com
#          predicates:
#            - Query=url,qq


package com.atguigu.gulimall.thirdparty.controller;

import com.aliyun.oss.OSS;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.MatchMode;
import com.aliyun.oss.model.PolicyConditions;
import com.atguigu.common.utils.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;

@RestController
public class OssController {
    @Autowired
    public OSS ossClient;

    @Value("${spring.cloud.alicloud.oss.endpoint}")
    private String endpoint;
    @Value("${spring.cloud.alicloud.oss.bucket}")
    private String bucket;
    @Value("${spring.cloud.alicloud.access-key}")
    private String accessId;

    @RequestMapping("/oss/policy")
    public R policy() {
        //https://gulimall-hellolp.oss-cn-hangzhou.aliyuncs.com/chumen.png
        String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
        // callbackUrl为 上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
//        String callbackUrl = "http://88.88.88.88:8888";
        String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
         /*
            以日期为名字,搞个目录,每一天都可以产生一个新目录
         */
        String dir = format + "/"; // 用户上传文件时指定的前缀。

        Map<String, String> respMap = null;
        try {
            long expireTime = 30;
            long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
            Date expiration = new Date(expireEndTime);
            PolicyConditions policyConds = new PolicyConditions();
            policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
            policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);

            String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
            byte[] binaryData = postPolicy.getBytes("utf-8");
            String encodedPolicy = BinaryUtil.toBase64String(binaryData);
            String postSignature = ossClient.calculatePostSignature(postPolicy);

            respMap = new LinkedHashMap<>();
            respMap.put("accessid", accessId);
            respMap.put("policy", encodedPolicy);
            respMap.put("signature", postSignature);
            respMap.put("dir", dir);
            respMap.put("host", host);
            respMap.put("expire", String.valueOf(expireEndTime / 1000));
            // respMap.put("expire", formatISO8601Date(expiration));
        } catch (Exception e) {
            // Assert.fail(e.getMessage());
            System.out.println(e.getMessage());
        }

        return R.ok().put("data", respMap);
    }
}

访问签名:http://localhost:88/api/thirdparty/oss/policy

后端校验—JSR303

前后端都需要校验

BindingResult

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

在这里插入图片描述

分组校验(多场景的复杂校验)

对于同一个字段,需要校验的规则有可能是不一样的。

以brandId为例子:

  • 在新增的情况下:新增不能携带id,是自增的
  • 在修改的情况下:必须指定品牌id,否则不知道要修改哪个品牌

再比如logo:

  • 在新增的情况下:必须是一个合法的URL
  • 在修改的情况下:可以不用提交
// 是接口就行(用于分组校验)
public interface AddGroup {
}
public interface UpdateGroup {
}
@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,UpdateGroup.class})
	private String name;
	/**
	 * 品牌logo地址
	 */
	@NotBlank(groups = {AddGroup.class})
	@URL(message = "logo必须是一个合法的url地址",groups={AddGroup.class,UpdateGroup.class})
	private String logo;

	/**
	 * 介绍
	 */
	private String descript;

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

	/**
	 * 检索首字母
	 */
	@NotEmpty(groups={AddGroup.class})
	@Pattern(regexp="^[a-zA-Z]$",message = "检索首字母必须是一个字母",groups={AddGroup.class, UpdateGroup.class})
	private String firstLetter;

	/**
	 * 排序
	 */
	@NotNull(groups={AddGroup.class}) 
	@Min(value = 0,message = "排序必须大于等于0",groups={AddGroup.class,UpdateGroup.class})
	private Integer sort;

}
    @RequestMapping("/save")
    //@RequiresPermissions("product:brand:save")
    // 给校验的bean后紧跟一个BindingResult,就可以获取到校验的结果
    public R save(/*@Valid*/ @Validated({AddGroup.class}) @RequestBody BrandEntity brand/*,BindingResult result*/){
//        if(result.hasErrors()){
//            Map<String,String> map = new HashMap<>();
//            //1、获取校验的错误结果
//            result.getFieldErrors().forEach((item)->{
//                //FieldError (item)
                  // 获取到错误提示
//                String message = item.getDefaultMessage();
//                //获取错误的属性的名字
//                String field = item.getField();
//                map.put(field,message);
//            });
//
//            return R.error(400,"提交的数据不合法").put("data",map);
//        }else {
//
//        }

        brandService.save(brand);
        return R.ok();
    }
    
    /**
     * 修改
     */
    @RequestMapping("/update")
    //@RequiresPermissions("product:brand:update")
    public R update(@Validated(UpdateGroup.class) @RequestBody BrandEntity brand){
        brandService.updateDetail(brand);
        return R.ok();
    }

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

自定义校验

        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
        </dependency>
@Documented
@Constraint(validatedBy = { l.p.common.valid.ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
    String message() default "{l.p.common.valid.ListValue.message}";

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

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

    int[] vals() default { };
}
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {

    private Set<Integer> set = new HashSet<>();
    //初始化方法
    @Override
    public void initialize(ListValue constraintAnnotation) {

        int[] vals = constraintAnnotation.vals();
        for (int val : vals) {
            set.add(val);
        }

    }

    //判断是否校验成功

    /**
     *
     * @param value 需要校验的值
     * @param context
     * @return
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {

        return set.contains(value);
    }
}

ValidationMessages.properties:

l.p.common.valid.ListValue.message=必须提交指定的值

使用:

    @NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
    @ListValue(vals = {0, 1}, groups = {AddGroup.class, UpdateStatusGroup.class})
    private Integer showStatus;
    @RequestMapping("/xx")
    public R xx(@Validated(AddGroup.class) @RequestBody BrandEntity brand){
		// xxxx 
        return R.ok();
    }

统一异常处理

/***
 * 错误码和错误信息定义类
 * 1. 错误码定义规则为5位数字
 * 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。
 *      10:通用             000:系统未知异常
 * 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
 * 错误码列表:
 *  10: 通用
 *      001:参数格式校验
 *  11: 商品
 *  12: 订单
 *  13: 购物车
 *  14: 物流
 */
public enum BizCodeEnume {
    UNKNOW_EXCEPTION(10000,"系统未知异常"),
    VAILD_EXCEPTION(10001,"参数格式校验失败");

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

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}
package com.atguigu.gulimall.product.exception;

import com.atguigu.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;

/**
 * @Slf4j:日志 (Lombok中的)
 * bootstrap.yml:
     logging:
       level:
         com.atguigu.gulimall: debug
 */
@Slf4j
//@ResponseBody
//@ControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {
    // 需要特殊处理的异常
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public R handleValidException(MethodArgumentNotValidException e) {
        log.error("数据校验出现问题{},异常类型:{}", e.getMessage(), e.getClass());
        BindingResult bindingResult = e.getBindingResult();

        Map<String, String> errorMap = new HashMap<>();
        bindingResult.getFieldErrors().forEach((fieldError) -> {
            errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
        });
        return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(), BizCodeEnume.VAILD_EXCEPTION.getMsg()).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.getMsg());
    }
}

参考

雷丰阳: Java项目《谷粒商城》Java架构师 | 微服务 | 大型电商项目).


本文完,感谢您的关注支持!


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值