# 上传头像

image-20210218213332745

前端代码这里不就贴了,后端代码处理流程讲一讲:

大体流程如下:

  1. 构建固定的文件名 face-{userid}.png

    而文件后缀只支持 png、jpg、jpeg ,如果不是这些则抛出异常给前端,如果是,则从文件名中取出文件后缀,评出固定的文件名。 换句话说:一个用户最多有 3 张头像,就是上面的三个后缀

  2. 构建图片存储到服务器哪一个位置的路径

    现阶段,图片保存在项目所在服务器硬盘上。

    所以以 userid 为每个用户创建一个文件夹来保存头像,{userid}/face-{userid}.png

    比如这个地址:http://localhost:8088/foodie/faces/190815GTKCBSS7MW/face-190815GTKCBSS7MW.png

    其中 http://localhost:8088/foodie/faces/ 作为头像的公共地址。

  3. 数据库中存储的是完整的图片路径,这里就使用了 spring 提供的静态资源映射功能,将静态资源映射成一个 web 服务,可以通过 url 访问到该静态资源

那么实现如下

# 图片上传接口

 @ApiOperation(value = "用户头像修改", notes = "用户头像修改", httpMethod = "POST")
    @PostMapping("uploadFace")
    public JSONResult uploadFace(
            @ApiParam(name = "userId", value = "用户id", required = true)
            @RequestParam String userId,
            @ApiParam(name = "file", value = "用户头像", required = true)
                    MultipartFile file,
            HttpServletRequest request, HttpServletResponse response) {

        // .sh .php

        // 定义头像保存的地址
        //        String fileSpace = IMAGE_USER_FACE_LOCATION;
        // 通过配置文件的形式来配置,本地绝对路径  和 服务器静态资源路径
        String fileSpace = fileUpload.getImageUserFaceLocation();
        // 在路径上为每一个用户增加一个 userid,用于区分不同用户上传
        String uploadPathPrefix = File.separator + userId;

        // 开始文件上传
        if (file != null) {
            FileOutputStream fileOutputStream = null;
            try {
                // 获得文件上传的文件名称
                String fileName = file.getOriginalFilename();

                if (StringUtils.isNotBlank(fileName)) {

                    // 文件重命名  imooc-face.png -> ["imooc-face", "png"]
                    String fileNameArr[] = fileName.split("\\.");

                    // 获取文件的后缀名
                    String suffix = fileNameArr[fileNameArr.length - 1];

                    if (!suffix.equalsIgnoreCase("png") &&
                            !suffix.equalsIgnoreCase("jpg") &&
                            !suffix.equalsIgnoreCase("jpeg")) {
                        return JSONResult.errorMsg("图片格式不正确!");
                    }

                    // face-{userid}.png
                    // 文件名称重组 覆盖式上传,增量式:额外拼接当前时间
                    String newFileName = "face-" + userId + "." + suffix;

                    // 上传的头像最终保存的位置
                    String finalFacePath = fileSpace + uploadPathPrefix + File.separator + newFileName;
                    // 用于提供给 web 服务访问的地址
                    uploadPathPrefix += ("/" + newFileName);

                    File outFile = new File(finalFacePath);
                    if (outFile.getParentFile() != null) {
                        // 创建文件夹
                        outFile.getParentFile().mkdirs();
                    }

                    // 文件输出保存到目录
                    fileOutputStream = new FileOutputStream(outFile);
                    InputStream inputStream = file.getInputStream();
                    IOUtils.copy(inputStream, fileOutputStream);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (fileOutputStream != null) {
                        fileOutputStream.flush();
                        fileOutputStream.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        } else {
            return JSONResult.errorMsg("文件不能为空!");
        }

        // 获取图片服务地址
        String imageServerUrl = fileUpload.getImageServerUrl();

        // 由于浏览器可能存在缓存的情况,所以在这里,我们需要加上时间戳来保证更新后的图片可以及时刷新
        String finalUserFaceUrl = imageServerUrl + uploadPathPrefix
                + "?t=" + DateUtil.getCurrentDateString(DateUtil.DATE_PATTERN);

        // 更新用户头像到数据库
        Users userResult = centerUserService.updateUserFace(userId, finalUserFaceUrl);

        userResult = setNullProperty(userResult);
        CookieUtils.setCookie(request, response, "user",
                JsonUtils.objectToJson(userResult), true);

        // TODO 后续要改,增加令牌token,会整合进redis,分布式会话

        return JSONResult.ok();
    }
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

service

 @ApiOperation(value = "用户头像修改", notes = "用户头像修改", httpMethod = "POST")
    @PostMapping("uploadFace")
    public JSONResult uploadFace(
            @ApiParam(name = "userId", value = "用户id", required = true)
            @RequestParam String userId,
            @ApiParam(name = "file", value = "用户头像", required = true)
                    MultipartFile file,
            HttpServletRequest request, HttpServletResponse response) {

        // .sh .php

        // 定义头像保存的地址
        //        String fileSpace = IMAGE_USER_FACE_LOCATION;
        // 通过配置文件的形式来配置,本地绝对路径  和 服务器静态资源路径
        String fileSpace = fileUpload.getImageUserFaceLocation();
        // 在路径上为每一个用户增加一个 userid,用于区分不同用户上传
        String uploadPathPrefix = File.separator + userId;

        // 开始文件上传
        if (file != null) {
            FileOutputStream fileOutputStream = null;
            try {
                // 获得文件上传的文件名称
                String fileName = file.getOriginalFilename();

                if (StringUtils.isNotBlank(fileName)) {

                    // 文件重命名  imooc-face.png -> ["imooc-face", "png"]
                    String fileNameArr[] = fileName.split("\\.");

                    // 获取文件的后缀名
                    String suffix = fileNameArr[fileNameArr.length - 1];

                    if (!suffix.equalsIgnoreCase("png") &&
                            !suffix.equalsIgnoreCase("jpg") &&
                            !suffix.equalsIgnoreCase("jpeg")) {
                        return JSONResult.errorMsg("图片格式不正确!");
                    }

                    // face-{userid}.png
                    // 文件名称重组 覆盖式上传,增量式:额外拼接当前时间
                    String newFileName = "face-" + userId + "." + suffix;

                    // 上传的头像最终保存的位置
                    String finalFacePath = fileSpace + uploadPathPrefix + File.separator + newFileName;
                    // 用于提供给 web 服务访问的地址
                    uploadPathPrefix += ("/" + newFileName);

                    File outFile = new File(finalFacePath);
                    if (outFile.getParentFile() != null) {
                        // 创建文件夹
                        outFile.getParentFile().mkdirs();
                    }

                    // 文件输出保存到目录
                    fileOutputStream = new FileOutputStream(outFile);
                    InputStream inputStream = file.getInputStream();
                    IOUtils.copy(inputStream, fileOutputStream);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (fileOutputStream != null) {
                        fileOutputStream.flush();
                        fileOutputStream.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        } else {
            return JSONResult.errorMsg("文件不能为空!");
        }

        // 获取图片服务地址
        String imageServerUrl = fileUpload.getImageServerUrl();

        // 由于浏览器可能存在缓存的情况,所以在这里,我们需要加上时间戳来保证更新后的图片可以及时刷新
        String finalUserFaceUrl = imageServerUrl + uploadPathPrefix
                + "?t=" + DateUtil.getCurrentDateString(DateUtil.DATE_PATTERN);

        // 更新用户头像到数据库
        Users userResult = centerUserService.updateUserFace(userId, finalUserFaceUrl);

        userResult = setNullProperty(userResult);
        CookieUtils.setCookie(request, response, "user",
                JsonUtils.objectToJson(userResult), true);

        // TODO 后续要改,增加令牌token,会整合进redis,分布式会话

        return JSONResult.ok();
    }
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

后面分别说:fileUpload 这个配置类 和 静态资源映射

数据库中存储的是 web 服务的头像地址,这样做的好处是,后续使用图片存储、文件存储中间件或则服务器时,就可以兼容了,不用修改过多的代码

# 属性资源文件

fileUpload 类实现如下

package cn.mrcode.foodiedev.resource;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "file")
@PropertySource("classpath:file-upload-prod.properties")
public class FileUpload {

    private String imageUserFaceLocation;
    private String imageServerUrl;

    public String getImageServerUrl() {
        return imageServerUrl;
    }

    public void setImageServerUrl(String imageServerUrl) {
        this.imageServerUrl = imageServerUrl;
    }

    public String getImageUserFaceLocation() {
        return imageUserFaceLocation;
    }

    public void setImageUserFaceLocation(String imageUserFaceLocation) {
        this.imageUserFaceLocation = imageUserFaceLocation;
    }
}

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

classpath 文件内容如下

file.imageUserFaceLocation=/workspaces/images/foodie/faces
file.imageServerUrl=http://localhost:8088/foodie/faces
1
2

可以看到在这里 @PropertySource("classpath:file-upload-prod.properties") 绑定死了文件名称,在不同环境之间切换是个麻烦事情

所以笔者并不推荐这样使用,直接使用 boot 的环境配置文件不是更好?

# 静态资源映射

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    // 实现静态资源的映射
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**")
                // 这里拦截之后,之前能访问的 swagger2 文档就无法访问到了
                // 因为它也是第三方 jar 包提供的 html 静态文件,所以这里也需要映射下
                .addResourceLocations("classpath:/META-INF/resources/")  // 映射 swagger2
                .addResourceLocations("file:/workspaces/images/");  // 映射本地静态资源
    }
1
2
3
4
5
6
7
8
9
10
11

上传后的图片可以通过如下地址访问到

http://localhost:8088/foodie/faces/190815GTKCBSS7MW/face-190815GTKCBSS7MW.png
1

# 限制图片格式以防后门

if (!suffix.equalsIgnoreCase("png") &&
    !suffix.equalsIgnoreCase("jpg") &&
    !suffix.equalsIgnoreCase("jpeg")) {
  return JSONResult.errorMsg("图片格式不正确!");
}
1
2
3
4
5

前端限制了能选择的图片类型,但是后端如果不限制的话,就有可能导致上传不是图片的文件,这个就是一个小漏洞。

# 图片大小限制 和 自定义异常

这个是 spring.servlet 提供的功能配置,可以限制上传文件的大小

spring.
  servlet:
    multipart:
      max-file-size: 512000     # 文件上传大小限制为 500kb
      max-request-size: 512000  # 请求大小限制为 500kb
1
2
3
4
5

再次上传大于 500kb 的文件就会抛出一个异常

严重: Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.web.multipart.MaxUploadSizeExceededException: Maximum upload size exceeded; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (1918017) exceeds the configured maximum (512000)] with root cause
org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (1918017) exceeds the configured maximum (512000)
	at org.apache.tomcat.util.http.fileupload.FileUploadBase$FileItemIteratorImpl.<init>(FileUploadBase.java:808)
	at org.apache.tomcat.util.http.fileupload.FileUploadBase.getItemIterator(FileUploadBase.java:256)
	at org.apache.tomcat.util.http.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:280)
1
2
3
4
5

但是此时,前端则收到了一个 500 异常,也没有响应的友好提示。

我们可以使用 spring 提供的全局异常捕获来实现拦截指定异常类型

package cn.mrcode.foodiedev.exception;


import cn.mrcode.foodiedev.common.util.JSONResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

@RestControllerAdvice
public class CustomExceptionHandler {

    // 上传文件超过500k,捕获异常:MaxUploadSizeExceededException
    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public JSONResult handlerMaxUploadFile(MaxUploadSizeExceededException ex) {
        return JSONResult.errorMsg("文件上传大小不能超过 500k,请压缩图片或者降低图片质量再上传!");
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

拦截了 MaxUploadSizeExceededException 异常类型,在执行 controller 时,跑出来的 MaxUploadSizeExceededException 类型都会走这个处理器,然后把我们自定义的信息响应到前端