SpringBoot图片上传的奇妙冒险

S3和OSS的核心区别

  • 价格:OSS在国内的价格通常比S3低20%-30%(特别是流量费用)
  • 速度:OSS在国内的访问速度明显快于S3(毕竟服务器在国内)
  • 合规性:OSS更符合中国的数据合规要求
  • 集成:OSS与阿里云其他服务(如CDN)的集成更顺畅

与本地上传相比的优势

  1. 不用操心服务器磁盘空间问题
  2. 天生具备高可用性和灾备能力
  3. 轻松实现CDN加速
  4. 无需自己处理图片缩略图等常见需求

下面就来开启我们的”本土化”奇妙冒险吧!

第一步:导入依赖

1. 创建Bucket

登录阿里云控制台,进入OSS服务,创建一个Bucket。记住这几个关键配置:

  • 地域:选离你用户最近的(如华东1)
  • 存储类型:标准存储就行
  • 读写权限:先设私有,后面用STS临时令牌更安全

2. 获取AccessKey

在RAM访问控制中创建用户,记得保存:

  • AccessKey ID
  • AccessKey Secret

第二步:SpringBoot装备库

1. 添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- OSS官方SDK -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.13.0</version>
</dependency>

<!-- 方便处理图片 -->
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.14</version>
</dependency>

2. 配置参数

application.yml中加入:

1
2
3
4
5
6
7
8
9
aliyun:
oss:
endpoint: https://oss-cn-hangzhou.aliyuncs.com # 你的Endpoint
bucket-name: your-bucket-name
access-key-id: your-access-key-id
access-key-secret: your-access-key-secret
folder: images/ # 存储目录
expire-time: 3600 # 临时URL有效期(秒)
cdn-domain: https://your-cdn-domain.com # 如果有CDN加速域名

第三步:编写代码

1. 配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class OssConfig {

@Value("${aliyun.oss.endpoint}")
private String endpoint;

@Value("${aliyun.oss.access-key-id}")
private String accessKeyId;

@Value("${aliyun.oss.access-key-secret}")
private String accessKeySecret;

@Bean
public OSS ossClient() {
return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
}
}

2. 服务类

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
90
91
92
93
94
95
96
97
98
99
100
@Service
@Slf4j
public class OssService {

@Autowired
private OSS ossClient;

@Value("${aliyun.oss.bucket-name}")
private String bucketName;

@Value("${aliyun.oss.folder}")
private String folder;

@Value("${aliyun.oss.expire-time}")
private Integer expireTime;

@Value("${aliyun.oss.cdn-domain}")
private String cdnDomain;

/**
* 上传文件到OSS
* @param file 文件
* @return 访问URL
*/
public String upload(MultipartFile file) {
if (file.isEmpty()) {
throw new RuntimeException("上传文件不能为空");
}

// 生成文件名
String originalFilename = file.getOriginalFilename();
String fileExt = originalFilename.substring(originalFilename.lastIndexOf("."));
String fileName = folder + UUID.randomUUID() + fileExt;

try {
// 上传到OSS
ossClient.putObject(bucketName, fileName, file.getInputStream());

// 返回CDN地址或OSS地址
return StringUtils.isNotBlank(cdnDomain)
? cdnDomain + "/" + fileName
: getFileUrl(fileName);
} catch (IOException e) {
log.error("文件上传失败", e);
throw new RuntimeException("文件上传失败");
}
}

/**
* 生成缩略图并上传
* @param file 原文件
* @param width 宽度
* @param height 高度
* @return 缩略图URL
*/
public String uploadThumbnail(MultipartFile file, int width, int height) {
try {
// 生成缩略图
BufferedImage thumbnail = Thumbnails.of(file.getInputStream())
.size(width, height)
.asBufferedImage();

ByteArrayOutputStream os = new ByteArrayOutputStream();
ImageIO.write(thumbnail, "jpg", os);
byte[] bytes = os.toByteArray();

// 上传缩略图
String originalFilename = file.getOriginalFilename();
String fileName = folder + "thumb_" + UUID.randomUUID() + ".jpg";

ossClient.putObject(bucketName, fileName, new ByteArrayInputStream(bytes));

return StringUtils.isNotBlank(cdnDomain)
? cdnDomain + "/" + fileName
: getFileUrl(fileName);
} catch (IOException e) {
log.error("缩略图生成失败", e);
throw new RuntimeException("缩略图生成失败");
}
}

/**
* 获取文件URL(带签名)
* @param fileName 文件名
* @return 访问URL
*/
public String getFileUrl(String fileName) {
Date expiration = new Date(System.currentTimeMillis() + expireTime * 1000);
URL url = ossClient.generatePresignedUrl(bucketName, fileName, expiration);
return url.toString();
}

/**
* 删除文件
* @param fileName 文件名
*/
public void delete(String fileName) {
ossClient.deleteObject(bucketName, fileName);
}
}

控制器

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
@RestController
@RequestMapping("/api/oss")
public class OssController {

@Autowired
private OssService ossService;

@PostMapping("/upload")
public Result<String> upload(@RequestParam("file") MultipartFile file) {
String url = ossService.upload(file);
return Result.success(url);
}

@PostMapping("/upload-thumbnail")
public Result<String> uploadThumbnail(@RequestParam("file") MultipartFile file,
@RequestParam(defaultValue = "200") int width,
@RequestParam(defaultValue = "200") int height) {
String url = ossService.uploadThumbnail(file, width, height);
return Result.success(url);
}

@DeleteMapping
public Result<Void> delete(@RequestParam String fileName) {
ossService.delete(fileName);
return Result.success();
}
}

第五步:前端小贴士

前端上传可以使用阿里云提供的Plupload或直接表单上传:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 简单表单上传 -->
<form action="/api/oss/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<button type="submit">上传</button>
</form>

<!-- 缩略图上传 -->
<form action="/api/oss/upload-thumbnail" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="number" name="width" value="200">
<input type="number" name="height" value="200">
<button type="submit">上传缩略图</button>
</form>

高级技巧:更安全的STS令牌方式

上面的方式直接用了AccessKey,生产环境更推荐使用STS临时令牌:

1. 添加依赖

1
2
3
4
5
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-sts</artifactId>
<version>3.0.0</version>
</dependency>

2. STS服务类

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
@Service
public class StsService {

@Value("${aliyun.oss.access-key-id}")
private String accessKeyId;

@Value("${aliyun.oss.access-key-secret}")
private String accessKeySecret;

@Value("${aliyun.oss.role-arn}")
private String roleArn;

@Value("${aliyun.oss.bucket-name}")
private String bucketName;

@Value("${aliyun.oss.expire-time}")
private Integer expireTime;

public StsToken getStsToken() {
// 配置STS
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
IAcsClient client = new DefaultAcsClient(profile);

// 构造请求
AssumeRoleRequest request = new AssumeRoleRequest();
request.setRoleArn(roleArn);
request.setRoleSessionName("oss-upload-session");
request.setDurationSeconds(expireTime.longValue());

try {
AssumeRoleResponse response = client.getAcsResponse(request);
return new StsToken(
response.getCredentials().getAccessKeyId(),
response.getCredentials().getAccessKeySecret(),
response.getCredentials().getSecurityToken(),
expireTime,
bucketName
);
} catch (ClientException e) {
throw new RuntimeException("获取STS令牌失败", e);
}
}

@Data
@AllArgsConstructor
public static class StsToken {
private String accessKeyId;
private String accessKeySecret;
private String securityToken;
private Integer expireTime;
private String bucketName;
}
}

3. 前端直传OSS

有了STS令牌后,前端可以直接上传到OSS,减轻服务器压力:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 先获取STS令牌
fetch('/api/oss/sts-token')
.then(res => res.json())
.then(token => {
const client = new OSS({
region: 'oss-cn-hangzhou',
accessKeyId: token.accessKeyId,
accessKeySecret: token.accessKeySecret,
stsToken: token.securityToken,
bucket: token.bucketName
});

// 上传文件
client.put('filename.jpg', file).then(result => {
console.log('上传成功', result);
});
});

性能优化小技巧

  1. 使用CDN加速:OSS可以无缝对接阿里云CDN
  2. 图片处理:OSS原生支持图片处理参数,如?x-oss-process=image/resize,w_200
  3. 分片上传:大文件使用分片上传API
  4. 客户端直传:使用STS令牌让客户端直接上传到OSS