前言
传统上,上传文件时,前端先把文件发给后端,后端接收到文件后再落盘或上传至OOS。
随着云服务的发展,前端可以直接上传文件至OOS。但是前端作为第三方向OOS上传文件需要凭证(例如token和秘钥),直接将凭证放入请求体发送上传请求显然是极度危险的。
因此,这里总结一下如何安全地实现前端直传OOS。
传统场景
场景说明
首先复习一下最传统的文件上传场景,即前端发送文件至后端,后端接收文件后上传至OOS或落盘。
示例代码
POST请求
Controller
获取post请求体中的参数,包括文件和其他表单数据。
1 2 3 4 5 6 7
| @PostMapping("/upload") public void upload(@RequestParam("file") MultipartFile file, @RequestParam("uploader") String uploader, @RequestParam("uploadTime") String uploadTime) {
bpFileService.upload(file, uploader, uploadTime); }
|
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
| public void uploadToDisk(MultipartFile file, String uploader, String uploadTime) { String path = "D:\\temp\\bp"; File dir = new File(path); try { if (!dir.exists()) { if (!dir.mkdirs()) { throw new IOException("Failed to create directory: " + path); } }
File diskFile = new File(path, file.getOriginalFilename() + ""); if (!diskFile.createNewFile()) { throw new IOException("Failed to create file: " + diskFile.getAbsolutePath()); }
file.transferTo(diskFile); } catch (IOException e) { e.printStackTrace(); }
BPFile sqlFile = new BPFile(file.getOriginalFilename(), file.getSize()+"", uploader, uploadTime, path); bpFileDao.insert(sqlFile); }
|
STS上传
阿里云STS(Security Token Service)是阿里云提供的一种临时访问权限管理服务。RAM提供RAM用户和RAM角色两种身份。其中,RAM角色不具备永久身份凭证,而只能通过STS获取可以自定义时效和访问权限的临时身份凭证,即安全令牌(STS Token)。
场景说明
在典型的Client/Server架构中,服务器端负责接收并处理客户端的请求,并将OSS作为后端的存储服务。客户端将要上传的文件发送给服务器端,然后服务器端再将数据转发上传到OSS。在这个过程中,一份数据需要在网络上传输两次,分别为从客户端到服务器端,再从服务器端到OSS。当访问量较大时,服务器端需要有足够的带宽资源来满足多个客户端同时上传的需求,这对架构的伸缩性提出了挑战。
为了解决以上场景带来的挑战,OSS提供了授权给第三方上传的功能。使用该功能,每个客户端可以直接将文件上传到OSS而不是通过服务器端转发,不仅节省了自建服务器的成本,而且充分利用了OSS的海量数据处理能力,无需考虑带宽和并发限制等,可以让客户专心于业务处理。
示例代码
POST请求
Controller
获取post请求体中的参数,调用生成临时凭证的API并返回结果。
1 2 3 4 5 6 7 8
| @PostMapping("/upload") public Response<STSCredentials> upload(@RequestParam("file") MultipartFile file, @RequestParam("uploader") String uploader, @RequestParam("uploadTime") String uploadTime) {
STSCredentials credentials = bpFileService.upload(file, uploader, uploadTime); return new Response<>(Code.OK, credentials); }
|
POJO
封装返回给前端的临时凭证。
1 2 3 4 5 6 7 8 9
| @Data public class STSCredentials { private String expiration; private String accessKeyId; private String accessKeySecret; private String securityToken; private String requestId; }
|
Service
调用STSService生成临时凭证。
1 2 3 4 5 6 7
| @Autowired private STSService sts;
@Override public STSCredentials upload(MultipartFile file, String uploader, String uploadTime) { return sts.getTempAccessCredentials(); }
|
通过RAM用户的AccessKeyId
和AccessKeySecret
生成临时访问凭证。
临时访问凭证包括安全令牌(SecurityToken)、临时访问密钥(AccessKeyId和AccessKeySecret)以及过期时间(Expiration)。
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| @Configuration public class STSService {
@Value("${OSS_ACCESS_KEY_ID}") private String accessKeyId; @Value("${OSS_ACCESS_KEY_SECRET}") private String accessKeySecret; @Value("${OSS_STS_ROLE_ARN}") private String roleArn; @Value("${BUCKET_NAME}") private String bucketName;
public STSCredentials getTempAccessCredentials() {
System.out.println(accessKeyId); System.out.println(accessKeySecret); System.out.println(roleArn); System.out.println(bucketName);
String endpoint = "sts.cn-beijing.aliyuncs.com"; String roleSessionName = "ArccSessionTest"; String policy = "{\n" + " \"Version\": \"1\", \n" + " \"Statement\": [\n" + " {\n" + " \"Action\": [\n" + " \"oss:PutObject\"\n" + " ], \n" + " \"Resource\": [\n" + " \"acs:oss:*:*:" + bucketName + "/src/*\" \n" + " ], \n" + " \"Effect\": \"Allow\"\n" + " }\n" + " ]\n" + "}"; Long durationSeconds = 3600L; try { String regionId = "cn-beijing"; DefaultProfile.addEndpoint(regionId, "Sts", endpoint); IClientProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret); DefaultAcsClient client = new DefaultAcsClient(profile); final AssumeRoleRequest request = new AssumeRoleRequest(); request.setSysMethod(MethodType.POST); request.setRoleArn(roleArn); request.setRoleSessionName(roleSessionName); request.setPolicy(policy); request.setDurationSeconds(durationSeconds); final AssumeRoleResponse response = client.getAcsResponse(request); STSCredentials credentials = new STSCredentials(); credentials.setExpiration(response.getCredentials().getExpiration()); credentials.setAccessKeyId(response.getCredentials().getAccessKeyId()); credentials.setAccessKeySecret(response.getCredentials().getAccessKeySecret()); credentials.setSecurityToken(response.getCredentials().getSecurityToken()); credentials.setRequestId(response.getRequestId()); return credentials; } catch (ClientException e) { e.printStackTrace(); System.out.println("Failed:"); System.out.println("Error code: " + e.getErrCode()); System.out.println("Error message: " + e.getErrMsg()); System.out.println("RequestId: " + e.getRequestId()); } return null; } }
|
表单上传
场景说明
OSS表单上传允许网页应用通过标准HTML表单直接将文件上传至OSS。这种方式下,在前端页面选择文件后,浏览器发起POST请求直接将文件传输到OSS服务器,而无需经过网站服务器中转,减轻了服务器的压力,提高了文件上传的效率和稳定性。
注意:通过表单上传的方式上传的Object大小不能超过5 GB。
使用场景
表单上传广泛应用于Web应用程序,包括但不限于以下几个方面:
- 用户资料上传:注册账号时上传头像、身份证照片或其他身份验证材料。在个人中心修改信息时上传新的头像或背景图。
- 文件分享与存储:在网盘服务、协同办公平台等通过表单上传各种格式的文件,如文档、图片、音频、视频等至云端进行存储和共享。
- 内容创作与发布:在博客、论坛、问答社区等平台编写文章并通过表单上传图片、附件等作为内容补充。
- 电商管理:商家在电商平台后台上传商品图片、详细描述文件、资质证书等;买家在购买过程中上传发票需求或其他证明材料。
- 在线教育平台:学生在提交作业或项目时上传文档、PPT、视频等作业文件;教师在课程建设时上传教学资料、课件等。
- 求职招聘网站:求职者上传简历、作品集等求职材料;企业发布职位时上传公司LOGO、招聘信息文件等。
- 问卷调查与反馈:填写在线调查问卷时上传附加的证据文件或说明文档。
- 软件开发协作:在代码托管平台如GitHub、GitLab等上传代码文件或项目文档。
方案概述
在服务端生成PostObject所需的Post签名、PostPolicy等信息,然后客户端可以凭借这些信息,在一定的限制下不依赖OSS SDK直接上传文件。PostPolicy可以用于限制客户端上传的文件,例如限制文件大小、文件类型。
注意:此方案适用于通过HTML表单上传的方式上传文件,不支持基于分片上传大文件、基于分片断点续传的场景。
代码示例
配置类
通过RAM用户密钥获取OOS实例。
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
| @Configuration public class OssConfig { @Value("${END_POIINT}") private String endpoint = "oss-cn-beijing.aliyuncs.com"; @Value("${BUCKET_NAME}") private String bucket ;
private String dir = "arcc/";
private long expireTime = 3600;
private String host = "http://" + bucket + "." + endpoint; @Value("${OSS_ACCESS_KEY_ID}") private String accessKeyId; @Value("${OSS_ACCESS_KEY_SECRET}") private String accessKeySecret;
private OSS ossClient; @Bean public OSS getOssClient() { ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); return ossClient; } @Bean public String getHost() { return host; } @Bean public String getAccessKeyId() { return accessKeyId; } @Bean public long getExpireTime() { return expireTime; } @Bean public String getDir() { return dir; }
@PreDestroy public void onDestroy() { ossClient.shutdown(); } }
|
Controller
生成Post签名和Post Policy等信息返回给前端。
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
| @RestController public class PostSignatureController { @Autowired private OSS ossClient;
@Autowired private OssConfig ossConfig;
@GetMapping("/get_post_signature_for_oss_upload") @ResponseBody public String generatePostSignature() { JSONObject response = new JSONObject(); try { long expireEndTime = System.currentTimeMillis() + ossConfig.getExpireTime() * 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, ossConfig.getDir()); String postPolicy = ossClient.generatePostPolicy(expiration, policyConds); byte[] binaryData = postPolicy.getBytes("utf-8"); String encodedPolicy = BinaryUtil.toBase64String(binaryData); String postSignature = ossClient.calculatePostSignature(postPolicy); response.put("ossAccessKeyId", ossConfig.getAccessKeyId()); response.put("policy", encodedPolicy); response.put("signature", postSignature); response.put("dir", ossConfig.getDir()); response.put("host", ossConfig.getHost()); } 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("HTTP Status Code: " + oe.getRawResponseError()); 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(); } return response.toString(); } } }
|
两者异同
STS和表单上传都能做到权限控制与定时销毁。
不同的是,表单上传在客户端通过JavaScript代码完成签名,然后通过表单直传数据到OSS,期间完全不需要服务端的参与。当然,如若设计更加复杂的权限控制、签名管理或设置上传回调等功能,就需要服务端的参与。
详细见:如何在Web端通过表单上传数据到OSS_对象存储(OSS)-阿里云帮助中心 (aliyun.com)
因此,表单上传适用于简单的文件上传场景,特别是在需要向公众开放上传功能时比较常见。
而STS授权更适用于需要更严格的权限控制或者需要将上传任务分发给第三方的场景,例如,将上传任务分发给移动端应用或者其他服务器。相比于表单上传有更高的灵活性和安全性,但是也更复杂一点。
其实还是搞不太清楚……姑且先这么理解吧QࡇQ
参考链接
问题记录
Q:我在Spring boot项目中定义了一个类,类中使用@Value
注解注入了application.yml
中的环境变量,类本身没有任何注解。我在其他类中调用该类时,使用new语句创建出的该类的示例拿不到环境变量,但是使用@Autowired
注解注入该类的示例可以拿到环境变量,这是为什么?
A:在 Spring Boot 项目中,使用 @Value
注解注入的属性是由 Spring 容器管理的 bean 属性。当你使用 new
关键字手动创建一个类的实例时,该实例并不是 Spring 容器所管理的 bean,因此无法进行依赖注入。
而当你使用 @Autowired
注解注入该类的示例时,Spring 容器会负责创建该类的实例,并且在实例化过程中注入依赖(比如 @Value
注解注入的属性),因此你能够获取到环境变量的值。
简而言之,使用 @Autowired
注解注入的示例是由 Spring 容器管理的 bean,可以享受到 Spring 提供的各种特性,包括依赖注入。而使用 new
关键字手动创建的示例则不受 Spring 容器的管理,因此无法实现依赖注入。