项目实现的背景介绍,当后台采用Spring Security验证信息,用户登录token保存在header中
用户每次请求需要携带对应的header和用户角色的情况时,记录一下问题
前后端分离,后台使用SpringBoot 前端采用antd pro react

一、 后端编写业务代码过程中遇到的错误点

1. 配置SpringBoot文件上传

  
## ******************************* 配置文件上传 ***********************************
# 是否启用SpringMVC多分部上传功能
spring.servlet.multipart.enabled=true
# 将文件写入磁盘的阀值。值可以使用后缀“MB”或“KB”来表示兆字节或字节大小
spring.servlet.multipart.file-size-threshold=0
# 指定默认上传的文件夹
# spring.servlet.multipart.location=/upload
# 限制单个文件最大大小
spring.servlet.multipart.max-file-size=1MB
# 限制所有文件最大大小
spring.servlet.multipart.max-request-size=10MB
# 是否延迟多部件文件请求的参数和文件的解析
spring.servlet.multipart.resolve-lazily=false     
  
  
## ***********************  配置静态资源文件  读取静态文件的依赖   ***************************************
spring.resources.static-locations=classpath:/static/
# 以jar包发布项目时,我们存储的路径是与jar包同级的static目录,因此我们需要在jar包目录的  
# application.properties配置文件中设置静态资源路径,如下所示:
# 设置静态资源路径,多个以逗号分隔
# spring.resources.static-locations=classpath:static/,file:static/
spring.mvc.static-path-pattern=/static/**
  
  • 上面简单的配置实现了springboot上传文件的配置,避免了SpringMVC复杂的xml文件的配置
  • 以jar包发布项目时,我们存储的路径是与jar包同级的static目录,因此我们需要在jar包目录的application.properties配置文件中设置静态资源路径,如下所示:设置静态资源路径,多个以逗号分隔 spring.resources.static-locations=classpath:static/,file:static/

2. 后端采用SpringMVC的MultipartFile接受前端发过来的参数

@PostMapping("/modification-avatar")
    public Map<String, Object> update(@RequestParam(value = "file",required = false) MultipartFile file, HttpServletRequest request) throws FileNotFoundException {
        User user = getUserUtils.getUserByRequest(request);
        Map<String, Object> map = personalService.modifyUserAvatar(file, request, user);
        return map;
    }  
 
  • 使用@RequestParam注解是为了进行参数绑定,这个位置只要保证前端传回的文件名为file时,该注解可以去掉;
  • 添加required = false是标明该参数是非必要的条件,避免前端非法访问后台时报500 error ;

3. 编写service业务代码

  
 /**
     * @Param: file  前端上传过来的图片
     * @Param: request 用户发送的请求,主要包含了用户的登录信息以及权限
     * @Param: user 当前用户
     * @description:TODO
     * 修改用户头像
     * @Return
    */
    @Transactional(isolation = Isolation.REPEATABLE_READ,propagation = Propagation.REQUIRED,rollbackFor = {Exception.class,RuntimeException.class})
    public Map<String, Object> modifyUserAvatar(MultipartFile file, HttpServletRequest request, User user) throws FileNotFoundException {
        Map<String, Object> result = new HashMap<>(2);
        if (file!=null && user != null){
               // 上传到了当前服务器的相对路径下 但是当前服务器是内置tomcat  相对路径如下
               // String path = request.getServletContext().getRealPath("static/user/picture/" + user.getUserName());
            String path = ResourceUtils.getURL("classpath:").getPath()+"static/user/picture/" + user.getUserName();
            // 上传文件
            Map<String, Object> map = UploadPicUtils.uploadPicture(file, path);
            // 根据工具类中标识的返回状态码,判断是否上传成功
            if ((int)map.get("status")==0){
                // 返回值为0代表上传成功
                //修改数据库记录
                User user1 = new User();
                user1.setId(user.getId());
                user1.setUserName(user.getUserName());
                user1.setUserPic((String) map.get("url"));
                User updateUser = userService.updateUser(user1);
                if (updateUser!=null){
                    result.put("status","success");
                    //删除原来的头像
                    DeletePicUtils.delect(ResourceUtils.getURL("classpath:").getPath()+user.getUserPic());
                }else {
                    result.put("status","error");
                }
            }else {
                result.put("status","error");
            }
        }else {
            result.put("status","error");
        }

        return result;

    }  
  
  • 编写service业务代码中用到了自定义的图片上传工具,这里不是重点不重点介绍
  • 在传统的单点架构中,即SSM项目中,获取当前项目的相对路径是通过以下方式获取

    String path = request.getServletContext().getRealPath("static/user/picture/" + user.getUserName());  
  • 在SpringBoot的项目中通过以上方式是得到以下路径

  • 分析,springboot是使用的是内置的tomcat,因此通过ServletContext获取的并不是当前项目所在的相对路径,而是tomcat当前执行的相对路径
  • 解决: 采用 ResourceUtils.getURL(“classpath:“).getPath()这种方式即可获取当前项目的相对路径

二、 前端编写业务代码过程中遇到的错误点

1. 解决请求携带header

由于后端将用户的token 以及用户的权限角色保存在了header中,,因此在前端每一次请求时都需要加上相对应的header,根据这个需求编写每一次的相应和请求的前端拦截器
/**
 * request 网络请求工具
 * 更详细的 api 文档: https://github.com/umijs/umi-request
 */
import { extend } from 'umi-request';
import { notification } from 'antd';


const codeMessage = {
  200: '服务器成功返回请求的数据。',
  201: '新建或修改数据成功。',
  202: '一个请求已经进入后台排队(异步任务)。',
  204: '删除数据成功。',
  400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
  401: '用户没有权限(令牌、用户名、密码错误)。',
  403: '用户访问成功,但是未授权,访问是被禁止的。',
  404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
  406: '请求的格式不可得。',
  410: '请求的资源被永久删除,且不会再得到的。',
  422: '当创建一个对象时,发生一个验证错误。',
  500: '服务器发生错误,请检查服务器。',
  502: '网关错误。',
  503: '服务不可用,服务器暂时过载或维护。',
  504: '网关超时。',
};
/**
 * 异常处理程序
 */

const errorHandler = (error) => {
  const { response } = error;

  if (response && response.status) {
    const errorText = codeMessage[response.status] || response.statusText;
    const { status, url } = response;
    notification.error({
      message: `请求错误 ${status}: ${url}`,
      description: errorText,
    });
  } else if (!response) {
    notification.error({
      description: '您的网络发生异常,无法连接服务器',
      message: '网络异常',
    });
  }

  return response;
};


/**
 * 配置request请求时的默认参数
 */

const request = extend({
  errorHandler,
  // 默认错误处理
  credentials: 'include', // 默认请求是否带上cookie
});
// request拦截器, 改变url 或 options.
request.interceptors.request.use(async (url, options) => {

  // eslint-disable-next-line @typescript-eslint/naming-convention
  const c_token = localStorage.getItem("Authorization");
  const authority = localStorage.getItem("authority");
  if (c_token) {
    const headers = {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      'Authorization': c_token,
      'authority':authority
    };
    return (
      {
        url: url,
        options: { ...options, headers: headers },
      }
    );
  } else {
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const headers = {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      'Authorization': c_token,
      'authority':authority
    };
    return (
      {
        url: url,
        options: { ...options },
      }
    );
  }

})

// response拦截器, 处理response
request.interceptors.response.use((response, options) => {
  const token = response.headers.get("Authorization");
  const authority = response.headers.get("authority");
  if (token) {
    localStorage.setItem("Authorization", token);
    localStorage.setItem("authority", authority);
  }
  return response;
});

export default request;
  
  • 以上是request完整的封装代码,在前端每一次service中请求后端时必须使用上面封装的request进行请求
  • 通过上面请求拦截器和相应拦截器将会将用户token和权限保存在header中,满足每次请求携带header的业务需求

2.实现图片上传

分析如下:
  • 使用antd组件Upload可以方便实现文件的上传功能
  • 上传头像时这里考虑两种情况: 一种采用Upload默认的表单提交,,但是表单提交时没有携带header,因此考虑解决携带header;另外一种是采用自定义提交规则,但是这个位置有两个考虑的点,1. 提交的Content-Type类型必须为 multipart/form-data 2. 携带header
  • 另外还有一个主意点,采用自定义提交时不能使用antd封装的request进行请求,因为antd封装的request封装的contentType是application/json类型。

  • 下面介绍采用默认表单提交的方式进行提交

     
    const AvatarView = ({ avatar ,_this}) => (
    <>
    <div className={styles.avatar_title}>头像</div>
    <div className={styles.avatar}>
       <img src={_this.state.imageUrl===undefined?avatar:_this.state.imageUrl}  alt="头像" />
    </div>
    <Upload name="file"
            showUploadList={false}
           // listType="picture-card"
            action="/api/user/modification-avatar"
            accept="image/png,image/gif,image/jpeg"
            beforeUpload={beforeUpload}
            onChange={_this.handleChange}
            headers={{
              Authorization: localStorage.getItem("Authorization"),
              authority: localStorage.getItem("authority"),
            }}
    >
    
      <div className={styles.button_view}>
        <Button>
          <UploadOutlined />
          更换头像
        </Button>
      </div>
    </Upload>
    </>
    );  
  • 在上面Upload组件中设置了对应的header相应头,解决了上面携带header的问题

  • 下面介绍一下简单的上传前的验证

      
    const getBase64 = (img, callback) => {
    const reader = new FileReader();
    reader.addEventListener('load', () => callback(reader.result));
    reader.readAsDataURL(img);
    }
    const beforeUpload = (file) => {
    const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'|| file.type === 'image/gif';
    if (!isJpgOrPng) {
    message.error('You can only upload JPG/PNG file!');
    }
    const isLt2M = file.size / 1024 / 1024 < 2;
    if (!isLt2M) {
    message.error('Image must smaller than 2MB!');
    }
    return isJpgOrPng && isLt2M;
    }   
       
      
    handleChange = info => {
    if (info.file.status === 'uploading') {
      this.setState({ loading: true });
      return;
    }
    if (info.file.status === 'done') {
      getBase64(info.file.originFileObj, imageUrl =>
        this.setState({
          imageUrl,
          loading: false,
        }),
      );
    }
    };  
      
    

3. 至此前后端分离项目实现简单头像修改的功能就实现了,做一个简单记录