文件上傳是業務系統高頻需求。本文記錄 Ubuntu 下 vsftpd 安裝配置(用戶、權限、被動模式)、Apache Commons Net FTPClient 上傳代碼、Nginx 反向代理靜態資源訪問全鏈路,重點解決上傳後文件權限導致 Nginx 403 的經典問題。
sudo apt-get update && sudo apt-get install vsftpd
sudo useradd -d /home/uftp -s /bin/bash -m uftp
sudo passwd uftp # 設置密碼
/etc/vsftpd.conf
# 基礎設置
listen=YES
anonymous_enable=NO
local_enable=YES
write_enable=YES # 允許上傳
local_umask=022 # 上傳文件權限 755 (777-022),解決 Nginx 403
dirmessage_enable=YES
use_localtime=YES
xferlog_enable=YES
connect_from_port_20=YES
secure_chroot_dir=/var/run/vsftpd/empty
pam_service_name=vsftpd
rsa_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
rsa_private_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
# 用戶白名單(僅允許 uftp 登錄)
userlist_enable=YES
userlist_deny=NO
userlist_file=/etc/allowed_users
seccomp_sandbox=NO # 兼容舊內核
# 被動模式(防火牆/雲服務器必配)
pasv_enable=YES
pasv_min_port=40000
pasv_max_port=40100
pasv_address=你的公網IP
echo "uftp" | sudo tee /etc/allowed_users
sudo systemctl restart vsftpd
# 測試
ftp localhost
# 或使用 FileZilla/Xftp 圖形化連接
commons-net
commons-net
3.8.0
org.apache.commons
commons-io
2.11.0
public class FtpUploadUtil {
public static boolean upload(String host, int port, String user, String pass,
String remoteDir, String localFilePath, String remoteFileName) {
FTPClient ftp = new FTPClient();
try {
// 1. 連接
ftp.connect(host, port);
int reply = ftp.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
ftp.disconnect();
return false;
}
// 2. 登錄
if (!ftp.login(user, pass)) return false;
// 3. 設置二進制模式、被動模式
ftp.setFileType(FTP.BINARY_FILE_TYPE);
ftp.enterLocalPassiveMode(); // 關鍵:客戶端發起數據連接
// 4. 切換遠程目錄(不存在則創建)
if (!ftp.changeWorkingDirectory(remoteDir)) {
// 遞歸創建多級目錄
String[] dirs = remoteDir.split("/");
StringBuilder path = new StringBuilder();
for (String dir : dirs) {
if (dir.isEmpty()) continue;
path.append("/").append(dir);
if (!ftp.changeWorkingDirectory(path.toString())) {
ftp.makeDirectory(path.toString());
}
}
}
// 5. 上傳文件
try (FileInputStream fis = new FileInputStream(localFilePath)) {
boolean success = ftp.storeFile(remoteFileName, fis);
return success;
}
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
try {
ftp.logout();
ftp.disconnect();
} catch (IOException ignored) {}
}
}
}
boolean ok = FtpUploadUtil.upload(
"192.168.148.128", 21, "uftp", "password",
"/home/uftp/images", "D:/123.jpg", "hello.jpg");
System.out.println(ok ? "上傳成功" : "上傳失敗");
server {
listen 80;
server_name your.domain.com;
location ~* \.(gif|jpg|jpeg|png|bmp|ico|txt|pdf)$ {
root /home/uftp/images; # 映射 FTP 根目錄
expires 7d; # 瀏覽器緩存 7 天
add_header Cache-Control "public";
access_log off;
}
}
nginx -t && nginx -s reload
curl https://your.domain.com/hello.jpg
# 或瀏覽器直接打開
| 問題 | 原因 | 解決 |
|---|---|---|
| 上傳成功但 Nginx 返回 403 | vsftpd 默認 umask 077 → 文件權限 700,Nginx worker 進程無讀權限 | 設置 local_umask=022 (文件 644/目錄 755) 或 file_open_mode=0644 |
| 上傳報 550 Permission denied | FTP 用戶無遠程目錄寫權限 | 確保 write_enable=YES、目錄擁有者為 ftp 用戶、SELinux/AppArmor 放行 |
| 連接超時/數據連接失敗 | 被動模式端口未開放 | 防火牆/安全組開放 pasv_min_port-pasv_max_port 範圍,設置 pasv_address 為公網 IP |
| 中文文件名亂碼 | FTP 協議默認 ISO-8859-1 | ftp.setControlEncoding("UTF-8"),vsftpd 配置 utf8_filesystem=YES |
| 大文件上傳中斷 | 網絡波動、超時設置過短 | ftp.setSoTimeout(0)、啟用斷點續傳 REST 指令(需服務端支持) |
PooledFTPClient (commons-pool2) 實現連接池,避免頻繁創建連接開銷。