阿里云OSS
阿里的OSS就是一个文件云存储方案:
简介:
阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。其数据设计持久性不低于99.999999999%,服务设计可用性不低于99.99%。具有与平台无关的RESTful API接口,您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
您可以使用阿里云提供的API、SDK接口或者OSS迁移工具轻松地将海量数据移入或移出阿里云OSS。数据存储到阿里云OSS以后,您可以选择标准类型(Standard)的阿里云OSS服务作为移动应用、大型网站、图片分享或热点音视频的主要存储方式,也可以选择成本更低、存储期限更长的低频访问类型(Infrequent Access)和归档类型(Archive)的阿里云OSS服务作为不经常访问数据的备份和归档。
2.2.1 开通oss访问
首先登陆阿里云,然后找到对象存储的产品:
点击进入后,开通服务:
随后即可进入管理控制台:
2.2.2.基本概念
OSS中包含一些概念,我们来认识一下:
存储类型(Storage Class)
OSS提供标准、低频访问、归档三种存储类型,全面覆盖从热到冷的各种数据存储场景。其中标准存储类型提供高可靠、高可用、高性能的对象存储服务,能够支持频繁的数据访问;低频访问存储类型适合长期保存不经常访问的数据(平均每月访问频率1到2次),存储单价低于标准类型;归档存储类型适合需要长期保存(建议半年以上)的归档数据,在三种存储类型中单价最低。详情请参见存储类型介绍。
存储空间(Bucket)
存储空间是您用于存储对象(Object)的容器,所有的对象都必须隶属于某个存储空间。您可以设置和修改存储空间属性用来控制地域、访问权限、生命周期等,这些属性设置直接作用于该存储空间内所有对象,因此您可以通过灵活创建不同的存储空间来完成不同的管理功能。
对象/文件(Object)
对象是 OSS 存储数据的基本单元,也被称为OSS的文件。对象由元信息(Object Meta),用户数据(Data)和文件名(Key)组成。对象由存储空间内部唯一的Key来标识。对象元信息是一组键值对,表示了对象的一些属性,比如最后修改时间、大小等信息,同时您也可以在元信息中存储一些自定义的信息。
地域(Region)
地域表示 OSS 的数据中心所在物理位置。您可以根据费用、请求来源等综合选择数据存储的地域。详情请参见OSS已开通的Region。
访问域名(Endpoint
)
Endpoint 表示OSS对外服务的访问域名。OSS以HTTP RESTful API的形式对外提供服务,当访问不同地域的时候,需要不同的域名。通过内网和外网访问同一个地域所需要的域名也是不同的。具体的内容请参见各个Region对应的Endpoint。
访问密钥(AccessKey)
AccessKey,简称 AK,指的是访问身份验证中用到的AccessKeyId 和AccessKeySecret。OSS通过使用AccessKeyId 和AccessKeySecret对称加密的方法来验证某个请求的发送者身份。AccessKeyId用于标识用户,AccessKeySecret是用户用于加密签名字符串和OSS用来验证签名字符串的密钥,其中AccessKeySecret 必须保密。
以上概念中,跟我们开发中密切相关的有三个:
- 存储空间(Bucket)
- 访问域名(Endpoint)
- 访问密钥(AccessKey):包含了AccessKeyId 和AccessKeySecret。
2.2.3.创建一个bucket
在控制台的右侧,可以看到一个新建Bucket
按钮:
点击后,弹出对话框,填写基本信息:
注意点:
- bucket:存储空间名称,名字只能是字母、数字、中划线
- 区域:即服务器的地址,这里选择了上海
- Endpoint:选中区域后,会自动生成一个Endpoint地址,这将是我们访问OSS服务的域名的组成部分
- 存储类型:默认
- 读写权限:这里我们选择公共读,否则每次访问都需要额外生成签名并校验,比较麻烦。敏感数据不要请都设置为私有!
- 日志:不开通
2.2.4.创建AccessKey
有了bucket就可以进行文件上传或下载了。不过,为了安全考虑,我们给阿里云账户开通一个子账户,并设置对OSS的读写权限。
点击屏幕右上角的个人图像,然后点击访问控制:
在跳转的页面中,选择用户,并新建一个用户:
然后填写用户信息:
然后会为你生成用户的AccessKeyID和AccessKeySecret:
妥善保管,不要告诉任何人!
接下来,我们需要给这个用户添加对OSS的控制权限。
进入这个新增的用户详情页面:
点击添加权限,会进入权限选择页面,输入oss进行搜索,然后选择管理对象存储服务(OSS)
权限:
2.3.上传文件最佳实践
在控制台的右侧,点击开发者指南
按钮,即可查看帮助文档:
然后在弹出的新页面的左侧菜单中找到开发者指南:
可以看到上传文件中,支持多种上传方式,并且因为提供的Rest风格的API,任何语言都可以访问OSS实现上传。
我们可以直接使用java代码来实现把图片上传到OSS,不过这样以来文件会先从客户端浏览器上传到我们的服务端tomcat,然后再上传到OSS,效率较低,如图:
以上方法有三个缺点:
- 上传慢。先上传到应用服务器,再上传到OSS,网络传送比直传到OSS多了一倍。如果直传到OSS,不通过应用服务器,速度将大大提升,而且OSS采用BGP带宽,能保证各地各运营商的速度。
- 扩展性差。如果后续用户多了,应用服务器会成为瓶颈。
- 费用高。需要准备多台应用服务器。由于OSS上传流量是免费的,如果数据直传到OSS,不通过应用服务器,那么将能省下几台应用服务器。
在阿里官方的最佳实践中,推荐了更好的做法:
阿里官方文档中,对于web前端直传又给出了3种不同方案:
各自有一些优缺点。
2.3.1.web前端签名后直传
客户端通过JavaScript代码完成签名,然后通过表单直传数据到OSS。无需访问应用服务器,对应用服务器压力较低。
流程图如下:
- JavaScript客户端签名直传:
- 优点:在客户端通过JavaScript代码完成签名,无需过多配置,即可实现直传,非常方便。
- 问题:客户端通过JavaScript把AccesssKeyID 和AccessKeySecret写在代码里面有泄露的风险
这里我们选择第二种,因为我们并不需要了解用户上传的文件的情况。
2.3.2.服务端签名后直传流程
服务端签名后直传的原理如下:
- 用户发送上传Policy请求到应用服务器(我们的微服务)。
- 应用服务器返回上传Policy和签名给用户。
- 用户直接上传数据到OSS。
流程图:
- 服务端签名,JavaScript客户端直传:
- 优点:Web端向服务端请求签名,然后直接上传,不会对服务端产生压力,而且安全可靠
- 问题:服务端无法实时了解用户上传了多少文件,上传了什么文件
在页面点击上传的按钮,可以看到请求已经发出:
这正是在向服务端申请签名,接下来我们需要在服务端接收请求,生成签名并返回。
我们要做的事情包括:
- 搭建微服务
- 在微服务中,提供一个接口,生成文件上传需要的签名
- 分析接口声明,分析请求方式、请求路径、请求参数、返回值类型
- 实现业务,生成签名
- 把一些常量配置到yml文件
- 编写类,读取这些属性
- 把OSS客户端注入到spring容器
- 编写业务,实现签名的生成
- 解决跨域问题
- 前端,调用我们的接口,获取签名(已完成)
- 前端,携带签名,完成上传(已完成)
2.4.搭建授权签名微服务
文件上传并不是商品微服务独有的业务,以后的其它业务也可能用到。而且阿里的AccessKey授权也会在多个地方用到。因此我们把签名授权功能封装到一个独立的微服务中,专门做各种授权功能。
为了方便其它微服务调用,我们依然搭建成聚合工程。
2.4.1.创建父工程
项目坐标
存放目录
pom文件
修改打包方式为POM即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>leyou</artifactId> <groupId>com.leyou</groupId> <version>1.0.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion>
<artifactId>ly-auth</artifactId> <packaging>pom</packaging>
</project>
|
2.4.2.实体类模块
项目坐标
存放位置
pom文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>ly-auth</artifactId> <groupId>com.leyou</groupId> <version>1.0.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion>
<artifactId>ly-auth-pojo</artifactId>
</project>
|
2.4.3.API模块
坐标:
存储位置:
pom文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>ly-auth</artifactId> <groupId>com.leyou</groupId> <version>1.0.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion>
<artifactId>ly-auth-api</artifactId>
<dependencies> <dependency> <groupId>com.leyou</groupId> <artifactId>ly-auth-pojo</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> </dependencies> </project>
|
2.4.4.业务模块
项目坐标
存放位置
pom文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>ly-auth</artifactId> <groupId>com.leyou</groupId> <version>1.0.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion>
<artifactId>ly-auth-service</artifactId>
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> </dependency> <dependency> <groupId>com.leyou</groupId> <artifactId>ly-auth-pojo</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.leyou</groupId> <artifactId>ly-common</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
|
启动类
在ly-auth-service
的com.leyou.auth
包下,新建一个启动类:
1 2 3 4 5 6 7 8 9 10 11 12 13
| package com.leyou.auth;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = {"com.leyou.auth", "com.leyou.common.advice"}) public class LyAuthApplication { public static void main(String[] args) { SpringApplication.run(LyAuthApplication.class, args); } }
|
配置文件
在ly-auth-service
的resources
目录下,新建一个application.yml
文件:
1 2 3 4 5 6 7 8 9 10 11 12
| server: port: 8082 spring: application: name: auth-service eureka: client: service-url: defaultZone: http://ly-registry:10086/eureka logging: level: com.leyou: debug
|
网关路由
在ly-gateway
的application.yml
中,添加对auth-service
的路由:
1 2 3 4 5 6 7 8 9 10 11
| spring: cloud: gateway: routes: - id: auth-service uri: lb://auth-service predicates: - Path=/auth/**
|
2.5.前端请求签名
在品牌新增的表单中,点击图片上传:
发现请求已经发出:
2.5.1.请求分析
请求分析:
- 请求方式:Get
- 请求路径:/auth/ali/oss/signature
- 请求参数:无(如果有登录用户,会携带登录用户信息)
- 返回值:这个需要参考阿里云的文档介绍
2.5.2.签名返回值
有关签名直传的文档部分:https://help.aliyun.com/document_detail/31927.html?spm=a2c4g.11186623.2.13.34f16e285th61w#concept-qp2-g4y-5db
其中,服务端签名返回给服务端的内容如下:
1 2 3 4 5 6 7 8
| { "accessId":"6MKO******4AUk44", "host":"http://post-test.oss-cn-hangzhou.aliyuncs.com", "policy":"eyJleHBpcmF0aW9uIjoiMjAxNS0xMS0wNVQyMDo1Mjoy******Jjdb25kaXRpb25zIjpbWyJjdb250ZW50LWxlbmd0aC1yYW5nZSIsMCwxMDQ4NTc2MDAwXSxbInN0YXJ0cy13aXRoIiwiJGtleSIsInVzZXItZGlyXC8iXV19", "signature":"VsxOcOudx******z93CLaXPz+4s=", "expire":1446727949, "dir":"user-dirs/" }
|
详细解释:
accessId
:用户的AccessKeyIdhost
:申请的阿里OSS的bucket访问地址policy
:文件上传的策略,主要包含对上传文件的要求,利用Base64加密后返回,说明文档signature
:生成的签名expire
:本次签名的过期时间,客户端可以换成签名,在有效期内无需再次签名dir
:要上传到bucket中的哪个目录
我们在项目中定义个DTO用来封装这些结果属性:
在ly-auth-pojo
的com.leyou.auth.dto
中,添加新的类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package com.leyou.auth.dto;
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;
@Data @AllArgsConstructor(staticName = "of") @NoArgsConstructor public class AliOssSignatureDTO { private String accessId; private String host; private String policy; private String signature; private long expire; private String dir; }
|
2.5.3.服务端接口声明
根据上面的请求分析,我们可以定义出一个web接口了:
首先是controller,在ly-auth-service
的com.leyou.auth.web
包中,新增一个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| package com.leyou.auth.web;
import com.leyou.auth.dto.AliOssSignatureDTO; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController @RequestMapping("ali") public class AliAuthController {
@GetMapping("/oss/signature") public ResponseEntity<AliOssSignatureDTO> getAliSignature(){ return ResponseEntity.ok(null); }
}
|
2.6.服务端生成签名
根据之前的分析,我们来编写服务端代码,接收前端请求,返回签名结果。这个要参考官方文档中的Demo。
文档地址:https://help.aliyun.com/document_detail/91868.html?spm=a2c4g.11186623.2.16.58cd7eaer5eXWw#concept-ahk-rfz-2fb
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String accessId = "<yourAccessKeyId>"; String accessKey = "<yourAccessKeySecret>"; String endpoint = "oss-cn-hangzhou.aliyuncs.com"; String bucket = "bucket-name"; String host = "https://" + bucket + "." + endpoint; String dir = "user-dir-prefix/"; OSS client = new OSSClient(endpoint, accessId, accessKey); 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 = client.generatePostPolicy(expiration, policyConds); byte[] binaryData = postPolicy.getBytes("utf-8"); String encodedPolicy = BinaryUtil.toBase64String(binaryData); String postSignature = client.calculatePostSignature(postPolicy); Map<String, String> respMap = new LinkedHashMap<String, String>(); 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)); JSONObject ja1 = JSONObject.fromObject(respMap); response(request, response, ja1.toString());
} catch (Exception e) { System.out.println(e.getMessage()); } }
|
2.6.1.配置属性
在刚刚的Demo中,有许多是值或者与服务器环境有关,需要定义到配置文件中,在application.yaml
中添加下面的属性:
1 2 3 4 5 6 7 8 9
| ly: oss: accessKeyId: LTAI4FhtSrGpB2mq4N36XbGb accessKeySecret: OEavFEiAyGm7OsGYff5TClHx88KJ28 host: http://ly-images.oss-cn-shanghai.aliyuncs.com endpoint: oss-cn-shanghai.aliyuncs.com dir: "heima01" expireTime: 1200000 maxFileSize: 5242880
|
然后,通过一个类来加载这些属性,在ly-auth-service
的com.leyou.auth.config
中定义类:
1 2 3 4 5 6 7 8 9 10 11 12
| @Data @Component @ConfigurationProperties("ly.oss") public class OSSProperties { private String accessKeyId; private String accessKeySecret; private String host; private String endpoint; private String dir; private long expireTime; private long maxFileSize; }
|
2.6.2.配置OSS客户端
OSS上传需要使用阿里提供的客户端API,其中核心是一个名为OSS的接口:
我们在ly-auth-service
的com.leyou.auth.config
定义一个配置类,将OSS注入到Spring容器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package com.leyou.auth.config;
import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;
@Configuration public class OSSConfig {
@Bean public OSS ossClient(OSSProperties prop){ return new OSSClientBuilder() .build(prop.getEndpoint(), prop.getAccessKeyId(), prop.getAccessKeySecret()); } }
|
2.6.3.service
定义业务代码,改造阿里提供的Demo,将结果封装为DTO返回.
在ly-auth-service
的com.leyou.auth.service
中定义Service接口:
1 2 3 4 5 6 7 8 9 10 11 12
| package com.leyou.auth.service;
import com.leyou.auth.dto.AliOssSignatureDTO;
public interface AliAuthService {
AliOssSignatureDTO getSignature(); }
|
在ly-auth-service
的com.leyou.auth.service.impl
中定义Service的实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| package com.leyou.auth.service;
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.leyou.auth.config.OSSProperties; import com.leyou.auth.dto.AliOssSignatureDTO; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service;
import java.util.Date;
@Slf4j @Service public class AliAuthServiceImpl implements AliAuthService{
private OSSProperties prop;
private OSS client;
public AliAuthServiceImpl(OSSProperties ossProperties, OSS client) { this.prop = ossProperties; this.client = client; }
public AliOssSignatureDTO getSignature() { try { long expireTime = prop.getExpireTime(); long expireEndTime = System.currentTimeMillis() + expireTime; Date expiration = new Date(expireEndTime);
PolicyConditions policyConds = new PolicyConditions(); policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, prop.getMaxFileSize()); policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, prop.getDir()); String postPolicy = client.generatePostPolicy(expiration, policyConds); byte[] binaryData = postPolicy.getBytes("utf-8"); String encodedPolicy = BinaryUtil.toBase64String(binaryData); String postSignature = client.calculatePostSignature(postPolicy);
return AliOssSignatureDTO.of(prop.getAccessKeyId(), prop.getHost(), encodedPolicy, postSignature, expireEndTime, prop.getDir()); } catch (Exception e) { log.error("上传文件失败,原因:{}", e.getMessage(), e); throw new RuntimeException("文件上传失败!", e); } } }
|
2.6.4.补全controller
我们给ly-auth-service
的com.leyou.auth.web
包中的AliAuthController
补全业务,调用AliAuthService
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| package com.leyou.auth.web;
import com.leyou.auth.dto.AliOssSignatureDTO; import com.leyou.auth.service.AliAuthService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController @RequestMapping("ali") public class AliAuthController {
private AliAuthService aliAuthService;
public AliAuthController(AliAuthService aliAuthService) { this.aliAuthService = aliAuthService; }
@GetMapping("/oss/signature") public ResponseEntity<AliOssSignatureDTO> getAliSignature() { return ResponseEntity.ok(aliAuthService.getSignature()); }
}
|
2.7.启动测试
启动ly-auth-service
,并重启ly-auth-gateway
,然后测试上传功能。
发现签名正确返回了:
结果:
但是上传失败了,返回了403的状态码:
控制台也报错了:
这是跨域问题。
2.8.解决跨域问题
我们在manage.leyou.com
访问aliyuncs.com
是跨域访问,需要设置跨域许可。
在阿里OSS的控制台看到这样的信息:
我们点击设置,进入设置页面:
点击创建规则,进入跨域请求填写表单:
填写完成后,再次测试