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:/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:/group1/M00/00/00/PaDqDGF2deSADjP6AAAAD30CMqM98_big.html bash-5.0#
至此,我们在center OS上用docker搭载FastDFS 服务至此已经结束;接下来我们利用springboot搭载后台接口
Springboot 集成FastDFS Client 后台 新建一个springboot项目,引入依赖Application启动类上添加FastDFSConfig引入注解org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-devtools runtime true com.github.tobato fastdfs-client 1.26.2 org.springframework.boot spring-boot-starter-web cn.hutool hutool-all 5.7.4 org.projectlombok lombok true junit junit test
@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;
增加上传和删除以及下载相关方法
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;
}
}
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;
}
}
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;
}
}
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;
}
}
public String uploadFile(byte[] bytes, String fileExtension) {
ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
StorePath storePath = fastFileStorageClient.uploadFile(stream, bytes.length, fileExtension, null);
return storePath.getFullPath();
}
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;
}
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;
}
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:/
@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 {
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;
public static final Integer SC_JEECG_NO_AUTHZ = 510;
public static final int TOKEN_EXPIRE_TIME = 3600; //3600秒即是一小时
public static final String PREFIX_USER_TOKEN = "PREFIX_USER_TOKEN_";
public static final Integer MENU_TYPE_0 = 0;
public static final Integer MENU_TYPE_1 = 1;
public static final Integer MENU_TYPE_2 = 2;
public static final Integer USER_UNFREEZE = 1;
public static final Integer USER_FREEZE = 2;
public static String ACCESS_TOKEN = "Access-Token";
public static final String LOGIN_USER_RULES_CACHE = "loginUser_cacheRules";
public static String LOGIN_USER_CACHERULES_ROLE = "loginUser_cacheRules::Roles_";
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用postman测试一下接口implements Serializable { private static final long serialVersionUID = 1L; private boolean success = true; private String message = "操作成功!"; private Integer code = 0; private T result; private long timestamp = System.currentTimeMillis(); public Result() { } public Result error500(String message) { this.message = message; this.code = CommonConstant.SC_INTERNAL_SERVER_ERROR_500; this.success = false; return this; } public Result success(String message) { this.message = message; this.code = CommonConstant.SC_OK_200; this.success = true; return this; } public static Result
测试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中,那么思路来了,
- 首先前端上传文件时约定添加access_token
- 后端自定义各一个注解,然后利用aop给注解添加增强拦截
- 给需要拦截的Controller、Service添加该注解即可实现token验证,
package com.file.server.annotations;
import java.lang.annotation.*;
@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;
@Aspect
@Component
@Slf4j
public class LoginRequireAop {
@Resource
private RedisUtil redisUtil;
@Pointcut("@annotation(com.file.server.annotations.LoginRequire)")
void loginRequirePointCut() {
}
@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 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;
@Slf4j
@RestControllerAdvice
public class GlobalException {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result 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());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = IllegalArgumentException.class)
public Result handler(IllegalArgumentException e) throws IOException {
log.error("Assert异常:-------------->{}", e.getMessage());
return Result.error(400, e.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = TokenInvalidException.class)
public Result handler(TokenInvalidException e) throws IOException {
return Result.error(HttpStatus.BAD_REQUEST.value(), e.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = NullPointerException.class)
public Result handler(NullPointerException e) throws Exception {
return Result.error(HttpStatus.BAD_REQUEST.value(), "参数异常");
}
}
package com.file.server.exception;
public class TokenInvalidException extends RuntimeException {
public TokenInvalidException(String e) {
super(e);
}
}
测试
postman中 的Header中增加access_token参数,因为这个token不存在,所以我们在aop中拦截掉了,返回无效token。
如果是正常存在的token,则会通过验证。



