基于centeros + docker +fastDFS+SpringBoot的 文件服务器搭建

centerOS 环境下安装docker

yum install -y docker-io #安装docker
service docker start #启动docker 
docker -v # 查看docker版本

service docker start可能会报The service command supports only basic LSB actions (start, stop, restart, try-restart, reload...... 我们换成systemctl start docker 即可

拉取fastDFS镜像

拉去镜像

docker pull qbanxiaoli/fastdfs

启动fastDFS服务。

其中ip为你服务器ip,WEB_PORT为fast DFS访问的端口;其他的不动

docker run -d --restart=always --privileged=true --net=host --name=fastdfs -e IP=**.160.234.** -e WEB_PORT=8091 -v ${HOME}/fastdfs:/var/local/fdfs qbanxiaoli/fastdfs

测试是否启动成功

如下console输出所示,如果看到URL,则代表启动成功,复制url,浏览器输入url,输出Hello FastDFS 即表示启动成功;需要注意的是:启动FastDFS时,我修改过WEB_PORT的端口,所以url地址需要加上我们设置的WEB_PORT端口,默认是80端口。

在这里插入图片描述

[root@install install]# docker exec -it fastdfs /bin/bash
bash-5.0# echo "Hello FastDFS!">index.html
bash-5.0# fdfs_test /etc/fdfs/client.conf upload index.html
This is FastDFS client test program v5.12

Copyright (C) 2008, Happy Fish / YuQing

FastDFS may be copied only under the terms of the GNU General
Public License V3, which may be found in the FastDFS source kit.
Please visit the FastDFS Home Page http://www.csource.org/ 
for more detail.

[2021-10-25 17:16:20] DEBUG - base_path=/var/local/fdfs/storage, connect_timeout=30, network_timeout=60, tracker_server_count=1, anti_steal_token=0, anti_steal_secret_key length=0, use_connection_pool=0, g_connection_pool_max_idle_time=3600s, use_storage_id=0, storage server id count: 0

tracker_query_storage_store_list_without_group: 
	server 1. group_name=, ip_addr=**.160.234.**, port=23000

group_name=group1, ip_addr=**.160.234.**, port=23000
storage_upload_by_filename
group_name=group1, remote_filename=M00/00/00/PaDqDGF2deSADjP6AAAAD30CMqM98.html
source ip address: **.160.234.**
file timestamp=2021-10-25 17:16:20
file size=15
file crc32=2097296035
example file url: http://**.160.234.**/group1/M00/00/00/PaDqDGF2deSADjP6AAAAD30CMqM98.html
storage_upload_slave_by_filename
group_name=group1, remote_filename=M00/00/00/PaDqDGF2deSADjP6AAAAD30CMqM98_big.html
source ip address: **.160.234.**
file timestamp=2021-10-25 17:16:20
file size=15
file crc32=2097296035
example file url: http://**.160.234.**/group1/M00/00/00/PaDqDGF2deSADjP6AAAAD30CMqM98_big.html
bash-5.0# 

至此,我们在center OS上用docker搭载FastDFS 服务至此已经结束;接下来我们利用springboot搭载后台接口

Springboot 集成FastDFS Client 后台

新建一个springboot项目,引入依赖

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>


        <!--fastDFS Client-->
        <dependency>
            <groupId>com.github.tobato</groupId>
            <artifactId>fastdfs-client</artifactId>
            <version>1.26.2</version>
        </dependency>

<!--        web 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--hutool 工具包-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.4</version>
        </dependency>

        <!-- lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--  test -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>

Application启动类上添加FastDFSConfig引入注解

@Import(FdfsClientConfig.class)
@SpringBootApplication
public class FastDfsServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(FastDfsServerApplication.class, args);
    }

}

新增FastDfsClient类

添加@componet注解,

@component 目的是spring中声明这个类为bean交给IOC容器去管理。创建和依赖引入以及引用都交给IOC容器,

@Slf4j
@Component
public class FastDfsClient {
注入FastFileStorageClient 以及FdfsWebServer

依赖注入有三种方式,注解注入,也就是@autowired 或者@Resource;Set注入;构造器注入,spring推荐使用构造器注入,这可以在一定程度上解决依赖为空的问题。那么为什么那么多人都是用注解注入呢?方便呀!

 /**
     * 注解注入
     */
    @Resource
    FastFileStorageClient fastFileStorageClient;

    /**
     * 注解注入
     */
    @Resource
    FdfsWebServer fdfsWebServer;
增加上传和删除以及下载相关方法

    /**
     * @param multipartFile 文件对象
     * @return 返回文件地址
     
     * @description 上传文件
     */
    public String uploadFile(MultipartFile multipartFile) {
        try {
            StorePath storePath = fastFileStorageClient.uploadFile(multipartFile.getInputStream(), multipartFile.getSize(), FilenameUtils.getExtension(multipartFile.getOriginalFilename()), null);
            return storePath.getFullPath();
        } catch (IOException e) {
            log.error(e.getMessage());
            return null;
        }
    }

    /**
     * @param multipartFile 图片对象
     * @return 返回图片地址
     
     * @description 上传缩略图
     */
    public String uploadImageAndCrtThumbImage(MultipartFile multipartFile) {
        try {
            StorePath storePath = fastFileStorageClient.uploadImageAndCrtThumbImage(multipartFile.getInputStream(), multipartFile.getSize(), FilenameUtils.getExtension(multipartFile.getOriginalFilename()), null);
            return storePath.getFullPath();
        } catch (Exception e) {
            log.error(e.getMessage());
            return null;
        }
    }

    /**
     * @param file 文件对象
     * @return 返回文件地址
     
     * @description 上传文件
     */
    public String uploadFile(File file) {
        try {
            FileInputStream inputStream = new FileInputStream(file);
            StorePath storePath = fastFileStorageClient.uploadFile(inputStream, file.length(), FilenameUtils.getExtension(file.getName()), null);
            return storePath.getFullPath();
        } catch (Exception e) {
            log.error(e.getMessage());
            return null;
        }
    }

    /**
     * @param file 图片对象
     * @return 返回图片地址
     
     * @description 上传缩略图
     */
    public String uploadImageAndCrtThumbImage(File file) {
        try {
            FileInputStream inputStream = new FileInputStream(file);
            StorePath storePath = fastFileStorageClient.uploadImageAndCrtThumbImage(inputStream, file.length(), FilenameUtils.getExtension(file.getName()), null);
            return storePath.getFullPath();
        } catch (Exception e) {
            log.error(e.getMessage());
            return null;
        }
    }

    /**
     * @param bytes         byte数组
     * @param fileExtension 文件扩展名
     * @return 返回文件地址
     
     * @description 将byte数组生成一个文件上传
     */
    public String uploadFile(byte[] bytes, String fileExtension) {
        ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
        StorePath storePath = fastFileStorageClient.uploadFile(stream, bytes.length, fileExtension, null);
        return storePath.getFullPath();
    }

    /**
     * @param fileUrl 文件访问地址
     * @param file    文件保存路径
     
     * @description 下载文件
     */
    public boolean downloadFile(String fileUrl, File file) {
        try {
            StorePath storePath = StorePath.parseFromUrl(fileUrl);
            byte[] bytes = fastFileStorageClient.downloadFile(storePath.getGroup(), storePath.getPath(), new DownloadByteArray());
            FileOutputStream stream = new FileOutputStream(file);
            stream.write(bytes);
        } catch (Exception e) {
            log.error(e.getMessage());
            return false;
        }
        return true;
    }

    /**
     * @param fileUrl 文件访问地址  /group1/M00/00/00/PaDqDGF4-iGAO0AEABKAelGkW34109.png
     
     * @description 删除文件
     */
    public boolean deleteFile(String fileUrl) {
        if (ObjectUtil.isEmpty(fileUrl)) {
            return false;
        }
        try {
            StorePath storePath = StorePath.parseFromUrl(fileUrl);
            fastFileStorageClient.deleteFile(storePath.getGroup(), storePath.getPath().substring(4, storePath.getPath().length()));
        } catch (Exception e) {
            log.error(e.getMessage());
            return false;
        }
        return true;
    }

    /**
     * 封装文件完整URL地址
     *
     * @param path
     * @return
     */
    public String getResAccessUrl(String path) {
        String url = fdfsWebServer.getWebServerUrl() + path;
        log.info("上传文件地址为:\n" + url);
        return url;
    }



增加yml配置类相关配置
spring:
  redis:
    host: localhost
    port: 6379
    database: 0
    connect-timeout: 3000
    jedis:
      pool:
        max-idle: 8
        max-active: 8
        max-wait: 1000
  devtools:
    restart:
      enabled: true
      additional-paths: src/main/java
  main:
    allow-bean-definition-overriding: true
  servlet:
    multipart:
      max-file-size: 5MB
      max-request-size: 10MB

fdfs:
  so-timeout: 1500
  connect-timeout: 600
  pool:
    max-total: 153
    max-wait-millis: 100
  thumb-image:
    height: 150
    width: 150
  tracker-list:
    - **.160.234.**:22122
  web-server-url: http://**.160.234.**:8091/

server:
  port: 8092

测试之前,我们先要开启:8091(application 的serverPort端口)、8092(WEB_PORT端口)、22122(trackerService端口)、2300(storageService端口)

添加测试类

 @Test
    public void Upload() {
        String fileUrl = "C:\\Users\\yugan\\Pictures\\Feedback\\Capture001.png";
        File file = new File(fileUrl);

        String str = fastDfsClient.uploadFile(file);
        log.info("str:{}",str);
        fastDfsClient.getResAccessUrl(str);
    }

运行测试方法:console打印日志:复制url至浏览器即可访问
在这里插入图片描述
在这里插入图片描述

写到这里,基本上可以满足需求了,但是问题来了,我们是写给前端来上传的,所以这里得提供一个对外的接口,用来调用fastDfsClient的上传;

添加Controller

package com.file.server.controller;

import com.file.server.handler.Result;
import com.file.server.utils.FastDfsClient;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.File;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * @author yugan
 */
@RestController
@RequestMapping(value = "/fastDfs")
@Slf4j
public class FastDfsController {

    @Resource
    FastDfsClient fastDfsClient;


    @RequestMapping(value = "/uploadImageAndCrtThumbImage", method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.OK)
    public Result uploadImageAndCrtThumbImage(MultipartFile file) {

        log.info("上传的文件:{}", file.getOriginalFilename());

        String url = fastDfsClient.uploadImageAndCrtThumbImage(file);
        log.info("返回的uri为:{}", url);
        String httpUrl = fastDfsClient.getResAccessUrl(url);

        Result result = new Result();
        result.setResult(httpUrl);
        result.success("上传成功");
        return result;
    }
}
附相关依赖

CommonConstant .java

package com.file.server.constant;

public interface CommonConstant {

    /**
     * 删除标志 1 未删除 0
     */
    public static final Integer DEL_FLAG_1 = 1;

    public static final Integer DEL_FLAG_0 = 0;

    public static final Integer SC_INTERNAL_SERVER_ERROR_500 = 500;

    public static final Integer SC_OK_200 = 200;

    /**
     * 访问权限认证未通过 510
     */
    public static final Integer SC_JEECG_NO_AUTHZ = 510;

    /**
     * 登录用户令牌缓存KEY前缀
     */
    public static final int TOKEN_EXPIRE_TIME = 3600; //3600秒即是一小时

    public static final String PREFIX_USER_TOKEN = "PREFIX_USER_TOKEN_";

    /**
     * 0:一级菜单
     */
    public static final Integer MENU_TYPE_0 = 0;

    /**
     * 1:子菜单
     */
    public static final Integer MENU_TYPE_1 = 1;

    /**
     * 2:按钮权限
     */
    public static final Integer MENU_TYPE_2 = 2;

    /**
     * 是否用户已被冻结 1(解冻)正常 2冻结
     */
    public static final Integer USER_UNFREEZE = 1;

    public static final Integer USER_FREEZE = 2;

    /**
     * token的key
     */
    public static String ACCESS_TOKEN = "Access-Token";

    /**
     * 登录用户规则缓存
     */
    public static final String LOGIN_USER_RULES_CACHE = "loginUser_cacheRules";

    /**
     * 登录用户拥有角色缓存KEY前缀
     */
    public static String LOGIN_USER_CACHERULES_ROLE = "loginUser_cacheRules::Roles_";

    /**
     * 登录用户拥有权限缓存KEY前缀
     */
    public static String LOGIN_USER_CACHERULES_PERMISSION = "loginUser_cacheRules::Permissions_";


    /**
     * 分页查询 当前页前端参数名
     */
    public static String CURRENT = "current";


    /**
     * 分页查询 每页显示条数 参数名
     */
    public static String SIZE = "size";
}

Result.java

package com.file.server.handler;

import com.file.server.constant.CommonConstant;
import lombok.Data;

import java.io.Serializable;

/**
 * 接口返回数据格式
 */
@Data
public class Result<T> implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 成功标志
     */
    private boolean success = true;

    /**
     * 返回处理消息
     */
    private String message = "操作成功!";

    /**
     * 返回代码
     */
    private Integer code = 0;

    /**
     * 返回数据对象 data
     */
    private T result;

    /**
     * 时间戳
     */
    private long timestamp = System.currentTimeMillis();

    public Result() {

    }

    public Result<T> error500(String message) {
        this.message = message;
        this.code = CommonConstant.SC_INTERNAL_SERVER_ERROR_500;
        this.success = false;
        return this;
    }

    public Result<T> success(String message) {
        this.message = message;
        this.code = CommonConstant.SC_OK_200;
        this.success = true;
        return this;
    }


    public static Result<Object> ok() {
        Result<Object> r = new Result<Object>();
        r.setSuccess(true);
        r.setCode(CommonConstant.SC_OK_200);
        r.setMessage("成功");
        return r;
    }

    public static Result<Object> ok(String msg) {
        Result<Object> r = new Result<Object>();
        r.setSuccess(true);
        r.setCode(CommonConstant.SC_OK_200);
        r.setMessage(msg);
        return r;
    }

    public static Result<Object> ok(Object data) {
        Result<Object> r = new Result<Object>();
        r.setSuccess(true);
        r.setCode(CommonConstant.SC_OK_200);
        r.setResult(data);
        return r;
    }

    public static Result<Object> error(String msg) {
        return error(CommonConstant.SC_INTERNAL_SERVER_ERROR_500, msg);
    }

    public static Result<Object> error(int code, String msg) {
        Result<Object> r = new Result<Object>();
        r.setCode(code);
        r.setMessage(msg);
        r.setSuccess(false);
        return r;
    }

    /**
     * 无权限访问返回结果
     */
    public static Result<Object> noauth(String msg) {
        return error(CommonConstant.SC_JEECG_NO_AUTHZ, msg);
    }
}

用postman测试一下接口

在这里插入图片描述

测试deleteFile接口时,报错误码:2,错误信息:找不到节点或文件,这里需要更改一下删除接口的代码;

 public boolean deleteFile(String fileUrl) {
        if (ObjectUtil.isEmpty(fileUrl)) {
            return false;
        }
        try {
            StorePath storePath = StorePath.parseFromUrl(fileUrl);
            fastFileStorageClient.deleteFile(storePath.getGroup(), storePath.getPath().substring(4, storePath.getPath().length()));
        } catch (Exception e) {
            log.error(e.getMessage());
            return false;
        }
        return true;
    }

问题又来了,这个对外接口不太安全,没有校验,一旦接口泄露,那岂不是.无法无天了。接下来,我们对接口进行安全校验

添加access_token校验

由于这是一个文件上传服务,所以设计之初就是给其他的系统使用的,既然涉及多个系统,那么用户肯定有一个access_token存在redis中,那么思路来了,

  1. 首先前端上传文件时约定添加access_token
  2. 后端自定义各一个注解,然后利用aop给注解添加增强拦截
  3. 给需要拦截的Controller、Service添加该注解即可实现token验证,
自定义注解
package com.file.server.annotations;

import java.lang.annotation.*;

/**
 * 登陆注解
 * @author yugan
 * @document 表示可以被javadoc此类的工具文档化,
 * @target 表示此注解使用范围,这里是方法和类 TYPE:类,接口或者枚举  METHOD:方法
 * @retention 表示这个注解存在的生命周期
 */
@Target({ElementType.METHOD,ElementType.TYPE})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequire {

}

给自定义注解添加aop增强拦截
package com.file.server.handler;

import cn.hutool.core.map.MapUtil;
import com.file.server.constant.CommonConstant;
import com.file.server.exception.TokenInvalidException;
import com.file.server.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.Objects;

/**
 * @author yugan
 */
@Aspect
@Component
@Slf4j
public class LoginRequireAop {
    @Resource
    private RedisUtil redisUtil;

    /**
     * 添加切点
     */
    @Pointcut("@annotation(com.file.server.annotations.LoginRequire)")
    void loginRequirePointCut() {
    }


    /**
     * 前置通知,检查token的有效性
     */
    @Before("loginRequirePointCut()")
    void checkTokenAvailability(JoinPoint joinPoint) {

        log.info("---------------------befor-------------------");

        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String access_token = request.getHeader(CommonConstant.ACCESS_TOKEN);
        //验证token的有效性,如果为null,则抛出NPE异常,便于GlobalException 来捕获到
        if (Objects.isNull(access_token)) {
            throw new NullPointerException();
        }

        //根据token 查询redis中是否有改用户的登陆信息,没有就拒绝请求
        Map<Object, Object> userInfo = redisUtil.hmget(access_token);
        if (MapUtil.isEmpty(userInfo)) {
            throw new TokenInvalidException("token过期");
        }
        log.info("ACCESS_TOKEN pass");
    }


}

给需要拦截的Controller、Service添加注解,拦截token

@RestController
@RequestMapping(value = "/fastDfs")
@Slf4j
@LoginRequire
public class FastDfsController {

附GlobalException 异常统一处理以及自定义异常TokenInvalidException

package com.file.server.exception;

import com.file.server.handler.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.io.IOException;

/**
 * @author yugan
 * RestControllerAdvice是Controller的加强,可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并应用到所有@RequestMapping中
 */
@Slf4j
@RestControllerAdvice
public class GlobalException {
    /**
     * @Validated 校验错误异常处理
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result<Object> handler(MethodArgumentNotValidException e) throws IOException {
        log.error("运行时异常:--------------", e);
        BindingResult bindingResult = e.getBindingResult();
        //这一步是把异常的信息最简化
        ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();
        return Result.error(HttpStatus.BAD_REQUEST.value(), objectError.getDefaultMessage());
    }


    /**
     * 处理Assert的异常
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = IllegalArgumentException.class)
    public Result<Object> handler(IllegalArgumentException e) throws IOException {
        log.error("Assert异常:-------------->{}", e.getMessage());
        return Result.error(400, e.getMessage());
    }

    /**
     * token 过期
     *
     * @param e
     * @return
     * @throws IOException
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = TokenInvalidException.class)
    public Result<Object> handler(TokenInvalidException e) throws IOException {
        return Result.error(HttpStatus.BAD_REQUEST.value(), e.getMessage());
    }


    /**
     * token 非法无效
     *
     * @param e
     * @return
     * @throws Exception
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = NullPointerException.class)
    public Result<Object> handler(NullPointerException e) throws Exception {
        return Result.error(HttpStatus.BAD_REQUEST.value(), "参数异常");
    }
}

package com.file.server.exception;

/**
 * 自定义个一个token无效异常
 *
 * @author yugan
 */
public class TokenInvalidException extends RuntimeException {
    public TokenInvalidException(String e) {
        super(e);
    }
}


测试

postman中 的Header中增加access_token参数,因为这个token不存在,所以我们在aop中拦截掉了,返回无效token。
在这里插入图片描述
如果是正常存在的token,则会通过验证。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值