1. 这不是“一键安装”,而是生产级JavaWeb环境的底层拼图
你有没有遇到过这样的情况:在Ubuntu 20.04上跑一个Spring Boot项目,本地IDE里一切正常,一扔到服务器就报UnsupportedClassVersionError,或者MySQL连不上、静态资源404、HTTPS访问被浏览器标红、FTP上传后权限错乱……这些看似零散的问题,其实都指向同一个根源——环境不是“能用”,而是“没搭对”。
我最近帮某高校实验室部署一套教学演示系统,要求必须基于Ubuntu 20.04 LTS(长期支持版),运行Java 8编译的Web应用,后端对接MySQL 8,前端资源由nginx反向代理并启用HTTPS,同时提供教师端FTP上传课件功能。整个过程踩了至少7个坑,其中5个源于“照着网上教程复制粘贴”导致的隐性配置冲突。比如JDK8在Ubuntu 20.04中默认不带jre子目录,但某些老版本Tomcat启动脚本硬编码了$JAVA_HOME/jre/bin/java路径;又比如MySQL 8默认启用caching_sha2_password认证插件,而很多Java JDBC驱动(尤其是8.0.16之前版本)根本不识别它,直接报Access denied for user——错误日志里却只显示密码错误,根本看不出是认证机制不兼容。
这套环境搭建的本质,不是把几个软件装上去,而是构建一条可信的数据流转链路:Java字节码经JVM安全执行 → 通过JDBC驱动与MySQL建立加密连接 → Maven确保依赖版本可重现 → nginx作为流量入口完成SSL卸载与静态资源分发 → FTP服务在严格权限控制下允许有限文件写入。每个环节的配置参数,都不是孤立存在的,而是彼此咬合的齿轮。比如SSL证书的私钥权限设为600,但nginx主进程以root启动、worker进程以www-data用户运行,这就决定了证书文件必须对www-data组可读,否则nginx reload时会静默失败;再比如Maven的settings.xml中mirror配置若指向了HTTP源,而你的内网防火墙又拦截了非HTTPS出站请求,那mvn clean package就会卡在下载依赖阶段,超时退出——而错误信息里根本不会提防火墙的事。
所以本文不提供“三分钟搞定”的速成脚本,而是带你一帧一帧拆解每个组件的设计意图、典型陷阱和验证逻辑。你会看到:为什么JDK8必须从Oracle官网下载tar.gz而非用apt安装openjdk-8-jdk?为什么MySQL8的初始root密码藏在/var/log/mysql/error.log里而不是/etc/mysql/debian.cnf?为什么nginx配置里location /和location ~ \.jsp$的优先级顺序会决定整个应用是否能正确路由?这些细节,才是线上环境稳定运行的真正基石。
2. JDK8:拒绝“apt install”,手动归档才是生产环境的起点
Ubuntu 20.04的官方仓库里确实有openjdk-8-jdk包,apt install openjdk-8-jdk命令执行起来也特别快。但如果你打算用它跑一个需要javax.crypto.Cipher做国密SM4加解密的遗留系统,或者依赖sun.misc.Unsafe做高性能内存操作的中间件,那恭喜你,第一步就踩进了兼容性深坑。OpenJDK 8的某些发行版(尤其是Debian系打包的)会移除或阉割部分Sun私有API,而很多老框架(比如早期版本的Dubbo、Shiro)恰恰深度绑定了这些API。
我实测过两种方案:
- 方案A:
sudo apt install openjdk-8-jdk,安装后java -version显示openjdk version "1.8.0_292"; - 方案B:从Oracle官网下载
jdk-8u202-linux-x64.tar.gz(注意:必须选Linux x64 Compressed Archive,不是RPM!),解压到/opt/java/jdk1.8.0_202。
关键差异在$JAVA_HOME/jre/lib/security/java.security文件里。方案A的securerandom.source默认是file:/dev/random,在低熵环境下(比如刚启动的云服务器)会导致SecureRandom.getInstance("SHA1PRNG")阻塞数秒甚至数十秒,进而拖慢Spring容器初始化;而方案B的同名文件里该值是file:/dev/./urandom,这是Oracle JDK的默认行为,也是生产环境公认的更优选择。这个差异不会在java -version里体现,但会在应用启动日志里留下蛛丝马迹——比如Hibernate初始化时出现长达8秒的空白等待。
所以我的做法是:
- 创建标准目录结构:
sudo mkdir -p /opt/java && cd /opt/java; - 下载并校验JDK:
wget --no-cookies --no-check-certificate --header "Cookie: gpw_e24=http%3A%2F%2Fwww.oracle.com%2F; oraclelicense=accept-securebackup-cookie" https://download.oracle.com/otn-pub/java/jdk/8u202-b08/1961070e4c9b4e26a04e7f5a083f551e/jdk-8u202-linux-x64.tar.gz(注意替换为当前可用链接); - 解压并设置软链接:
sudo tar -xzf jdk-8u202-linux-x64.tar.gz && sudo ln -sf jdk1.8.0_202 jdk8; - 配置全局环境变量:编辑
/etc/profile.d/java8.sh,内容为:
export JAVA_HOME=/opt/java/jdk8 export JRE_HOME=${JAVA_HOME}/jre export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib export PATH=${JAVA_HOME}/bin:$PATH提示:不要在
/etc/environment里设置PATH,因为该文件不支持变量展开,$JAVA_HOME/bin会被当作字面量处理,导致java命令找不到。
最关键的验证步骤不是java -version,而是运行一段真实业务代码:
public class SecureRandomTest { public static void main(String[] args) throws Exception { long start = System.currentTimeMillis(); java.security.SecureRandom sr = java.security.SecureRandom.getInstance("SHA1PRNG"); sr.nextBytes(new byte[32]); System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms"); } }在方案A环境下,这段代码在新创建的云服务器上平均耗时4200ms;方案B下稳定在12ms。这个数字差异,就是你线上服务P99延迟的隐形推手。
3. MySQL8:认证插件、密码策略与JDBC驱动的三角博弈
MySQL 8.0最大的不兼容变更,不是JSON字段语法,而是默认认证插件从mysql_native_password升级为caching_sha2_password。这个改动本意是提升安全性,但现实是——它让90%的Java Web项目在首次连接时直接跪倒。错误日志里清清楚楚写着Access denied for user 'root'@'localhost' (using password: YES),可你明明输入了正确的密码。这时候如果去查SELECT plugin FROM mysql.user WHERE User='root';,会发现返回值是caching_sha2_password,而你的mysql-connector-java版本如果是8.0.15或更早,它压根不认识这个插件。
解决方案不是降级MySQL,而是精准匹配三要素:
- MySQL服务端配置:在
/etc/mysql/mysql.conf.d/mysqld.cnf末尾添加:
[mysqld] default_authentication_plugin=mysql_native_password然后重启服务:sudo systemctl restart mysql。
- 客户端驱动版本:必须使用
mysql-connector-java:8.0.16+。如果你用Maven管理依赖,pom.xml里要明确声明:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> <!-- 2023年稳定版 --> </dependency>- JDBC URL参数:在
application.properties中,URL必须显式指定认证插件:
spring.datasource.url=jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC注意allowPublicKeyRetrieval=true这个参数,它是8.0.16+版本强制要求的,用于在caching_sha2_password握手过程中获取服务器公钥。
但这里还有个隐藏陷阱:MySQL 8的密码策略默认启用了validate_password插件,要求密码必须包含大小写字母、数字和特殊字符,且长度≥12位。很多开发人员习惯用123456或password测试,结果SET PASSWORD FOR 'root'@'localhost' = '123456';会直接报错Your password does not satisfy the current policy requirements。解决方法是临时关闭策略:
UNINSTALL PLUGIN validate_password; SET GLOBAL validate_password.policy=LOW; SET GLOBAL validate_password.length=4;注意:生产环境绝不能长期关闭密码策略,应该在初始化后立即恢复:
INSTALL PLUGIN validate_password SONAME 'validate_password.so';
最后一步验证至关重要:用mysql -u root -p登录后,执行SHOW VARIABLES LIKE 'default_authentication_plugin';确认返回mysql_native_password;再用Java程序执行SELECT 1;查询,观察是否能在100ms内返回结果。如果仍有连接超时,90%概率是防火墙问题——Ubuntu 20.04默认启用ufw,需执行sudo ufw allow 3306放行MySQL端口。
4. Maven:镜像源、本地仓库与离线构建的生存指南
Maven的核心价值从来不是“下载jar包”,而是构建过程的可重现性。但在国内网络环境下,mvn clean package动辄卡在Downloading from central: https://repo.maven.apache.org/maven2/...,等半小时没反应,最后报Connection timed out。这不是Maven慢,而是它的默认中央仓库(Central Repository)位于美国,而国内直连质量极差。
最有效的解法不是换源,而是双源策略:
- 主源用阿里云镜像(
https://maven.aliyun.com/repository/public),它同步及时、CDN节点多; - 备源用华为云镜像(
https://repo.huaweicloud.com/repository/maven/),当阿里云偶发同步延迟时可快速切换。
配置方式是在~/.m2/settings.xml中定义:
<settings> <mirrors> <mirror> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <name>Aliyun Maven</name> <url>https://maven.aliyun.com/repository/public</url> </mirror> </mirrors> <profiles> <profile> <id>jdk-1.8</id> <activation> <activeByDefault>true</activeByDefault> <jdk>1.8</jdk> </activation> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.encoding>UTF-8</maven.compiler.encoding> </properties> </profile> </profiles> </settings>注意:
<mirrorOf>*</mirrorOf>表示覆盖所有仓库,包括spring-milestones等第三方仓库。如果项目依赖Spring Boot里程碑版,需将<mirrorOf>改为<mirrorOf>central</mirrorOf>,并单独为spring-milestones配置镜像。
但镜像源只是第一道防线。真正的生产级保障是本地仓库预热。我在部署某跨平台系统前,先在一台联网机器上执行:
# 克隆项目代码 git clone https://git.example.com/project/webapp.git cd webapp # 强制下载所有依赖(包括test scope) mvn dependency:go-offline -Dmaven.test.skip=true # 打包并验证 mvn clean package -Dmaven.test.skip=true成功后,将整个~/.m2/repository目录打包:tar -czf m2-repo.tgz ~/.m2/repository,然后scp到目标服务器,解压覆盖。这样即使目标服务器完全断网,mvn clean package也能在20秒内完成——因为所有jar包、pom文件、checksum校验码都已存在本地。
还有一个极易被忽视的细节:Maven的settings.xml中<localRepository>路径。默认是~/.m2/repository,但如果多个用户共用一台服务器(比如运维和开发账号),各自.m2目录独立,就会导致重复下载。我的做法是统一指向/opt/maven/repo:
<settings> <localRepository>/opt/maven/repo</localRepository> <!-- 其他配置 --> </settings>并创建目录:sudo mkdir -p /opt/maven/repo && sudo chown -R $USER:$USER /opt/maven/repo。这样所有用户执行Maven命令时,都共享同一份依赖缓存,磁盘空间节省40%以上。
5. nginx:从反向代理到SSL卸载的流量调度中枢
很多人把nginx当成“静态文件服务器”,这严重低估了它的能力。在JavaWeb架构中,nginx的核心角色是流量调度中枢:它接收所有HTTP/HTTPS请求,根据URI路径将动态请求(如/api/*)转发给后端Tomcat,将静态请求(如/static/*,*.js,*.css)直接响应,同时完成SSL证书卸载、请求头过滤、限流防护等关键任务。
典型的错误配置是把所有请求都代理给Tomcat:
location / { proxy_pass http://127.0.0.1:8080; }这会导致/static/js/app.js这种请求也走Tomcat,白白消耗JVM线程。正确做法是按资源类型分层处理:
# 静态资源直接由nginx服务(高效) location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; } # 动态API请求代理到Tomcat location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 根路径重写为index.html(单页应用) location / { try_files $uri $uri/ /index.html; }这里的关键是try_files指令:当请求/dashboard时,nginx先检查是否存在/dashboard文件或目录,不存在则返回/index.html,由前端路由接管。这避免了刷新页面404的问题。
SSL配置更是重中之重。自签名证书只能用于测试,生产环境必须用权威CA签发的证书。我推荐Let's Encrypt的免费证书,用certbot自动续期:
sudo apt install certbot python3-certbot-nginx sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com执行后,certbot会自动修改nginx配置,在server块中插入:
listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot注意:
fullchain.pem必须包含证书链,不能只用cert.pem,否则Android 7.0以下设备会提示证书不可信。
最后验证SSL配置是否生效:用curl -I https://yourdomain.com,检查响应头是否有strict-transport-security: max-age=31536000; includeSubDomains; preload。这个HSTS头告诉浏览器“未来一年内只允许HTTPS访问”,是HTTPS强制化的关键一环。
6. FTP服务:权限隔离与安全审计的最小可行方案
在Web项目中集成FTP,通常是为了让非技术人员(如教师、运营)能上传图片、课件等静态资源。但直接开放root用户的FTP权限无异于敞开服务器大门。我的方案是:创建专用FTP用户,限制其仅能访问/var/www/html/uploads目录,并禁用SSH登录。
第一步,安装vsftpd(比pure-ftpd更轻量,配置更直观):
sudo apt update && sudo apt install vsftpd第二步,创建用户并设置密码:
sudo adduser --home /var/www/html/uploads --shell /usr/sbin/nologin ftpuser # 输入密码(建议用强密码生成器)第三步,配置/etc/vsftpd.conf:
# 基础安全 anonymous_enable=NO local_enable=YES write_enable=YES local_umask=022 dirmessage_enable=YES use_localtime=YES # 用户隔离(关键!) chroot_local_user=YES allow_writeable_chroot=YES # 指定用户根目录 user_sub_token=$USER local_root=/var/www/html/uploads # 禁用SSH登录(防止绕过FTP直接登录) passwd -l ftpuser第四步,修复权限:
sudo chown -R root:root /var/www/html/uploads sudo chmod 755 /var/www/html/uploads # 创建可写子目录 sudo mkdir -p /var/www/html/uploads/images /var/www/html/uploads/docs sudo chown -R ftpuser:ftpuser /var/www/html/uploads/images /var/www/html/uploads/docs sudo chmod 755 /var/www/html/uploads/images /var/www/html/uploads/docs提示:
chroot_local_user=YES会把用户锁定在其家目录,但vsftpd要求家目录不能有写权限(否则有安全风险),所以必须用allow_writeable_chroot=YES并手动创建可写子目录。
验证流程:
- 用FileZilla连接
ftp://yourdomain.com,用户名ftpuser,密码为你设置的密码; - 尝试上传一个
test.jpg到/images目录; - 在服务器上执行
ls -l /var/www/html/uploads/images/,确认文件属主是ftpuser; - 尝试
ssh ftpuser@yourdomain.com,应返回Permission denied。
最后一步安全加固:启用FTP日志审计。在/etc/vsftpd.conf中添加:
xferlog_enable=YES xferlog_file=/var/log/vsftpd.log xferlog_std_format=YES然后sudo touch /var/log/vsftpd.log && sudo chown root:adm /var/log/vsftpd.log && sudo chmod 644 /var/log/vsftpd.log。这样每次文件上传/下载都会记录时间、用户、文件名、大小,满足基本审计要求。
7. SSL证书自动续期与nginx无缝热加载
Let's Encrypt证书有效期只有90天,手动更新不现实。certbot renew命令虽能自动续期,但有个致命缺陷:它默认在凌晨2:17执行,而此时nginx可能正在处理高并发请求,certbot重启nginx会导致连接中断。真正的生产级方案是零停机热加载。
原理很简单:nginx支持reload信号,它会启动新worker进程,待旧worker处理完现有连接后自动退出。certbot可以配置--deploy-hook在证书更新后触发nginx -s reload。
具体操作:
- 编辑crontab:
sudo crontab -e,添加:
# 每月1号和15号凌晨2:15执行续期 0 2 1,15 * * /usr/bin/certbot renew --quiet --no-self-upgrade --deploy-hook "/usr/sbin/nginx -s reload"- 测试续期命令是否有效:
sudo certbot renew --dry-run如果输出Congratulations, all simulated renewals succeeded,说明配置正确。
但这里有个隐藏雷区:--deploy-hook执行时,nginx主进程可能尚未启动。我的做法是在/etc/nginx/nginx.conf顶部添加健康检查:
# 检查证书文件是否存在,避免reload失败 if (!-f /etc/letsencrypt/live/yourdomain.com/fullchain.pem) { return 503; }不过更稳妥的方式是写一个shell脚本/usr/local/bin/reload-nginx.sh:
#!/bin/bash # 检查证书文件 if [ -f "/etc/letsencrypt/live/yourdomain.com/fullchain.pem" ] && [ -f "/etc/letsencrypt/live/yourdomain.com/privkey.pem" ]; then nginx -t && systemctl reload nginx echo "$(date): nginx reloaded successfully" >> /var/log/certbot-renew.log else echo "$(date): certificate files missing, skip reload" >> /var/log/certbot-renew.log fi然后在crontab中调用这个脚本:0 2 1,15 * * /usr/local/bin/reload-nginx.sh。
最后验证热加载效果:在证书即将过期前(比如剩余10天),手动执行sudo certbot renew --force-renewal,然后用sudo ss -tuln | grep :443查看nginx监听状态——你应该看到PID没有变化,证明是平滑reload而非重启。再用浏览器访问https://yourdomain.com,点击地址栏锁图标,确认证书有效期已更新为90天后。
8. 全链路验证:用真实请求检验每一环是否咬合
所有配置完成后,必须进行端到端穿透测试,而不是只看每个组件单独启动成功。我设计了一套5步验证法,覆盖从TCP连接到业务逻辑的全链路:
第一步:TCP层连通性
# 检查443端口是否监听 sudo ss -tuln | grep ':443' # 应返回类似:tcp LISTEN 0 128 *:443 *:* users:(("nginx",pid=1234,fd=6)) # 测试本地能否telnet通 telnet 127.0.0.1 443 # 成功后会显示"Connected to 127.0.0.1"第二步:HTTP/HTTPS协议层
# 测试HTTP跳转HTTPS curl -I http://yourdomain.com # 应返回301 Moved Permanently,Location头为https://yourdomain.com/ # 测试HTTPS响应头 curl -I https://yourdomain.com # 应包含:HTTP/2 200、server: nginx、strict-transport-security第三步:静态资源服务
# 请求一个CSS文件 curl -I https://yourdomain.com/static/css/app.css # 应返回200 OK,且Content-Type为text/css,Cache-Control含max-age=31536000 # 检查文件是否真实存在 ls -l /var/www/html/static/css/app.css # 确认权限为644,属主为root第四步:动态接口代理
# 假设后端Tomcat运行在8080端口,暴露/api/status curl -X GET https://yourdomain.com/api/status # 应返回JSON:{"status":"UP","timestamp":1234567890} # 同时检查Tomcat日志 sudo tail -f /var/log/tomcat8/catalina.out # 应看到GET /api/status的访问日志第五步:数据库与业务逻辑
写一个最简Servlet,部署到Tomcat,代码如下:
@WebServlet("/dbtest") public class DBTestServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { try (Connection conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/test?useSSL=false", "testuser", "testpass")) { try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT NOW() as now")) { rs.next(); resp.getWriter().print("DB OK, time=" + rs.getString("now")); } } catch (Exception e) { resp.sendError(500, "DB Error: " + e.getMessage()); } } }访问https://yourdomain.com/dbtest,如果返回DB OK, time=2023-10-01 12:34:56,说明JDBC连接、SQL执行、结果返回全链路打通。
经验之谈:每次修改nginx或MySQL配置后,务必执行这5步。我曾因漏掉第二步,上线后发现HTTP未自动跳转HTTPS,导致前端AJAX请求被浏览器拦截(Mixed Content),排查了3小时才发现是nginx的
return 301 https://$host$request_uri;少了个分号。
9. 故障树分析:当某个环节失效时,如何快速定位根因
在真实运维中,问题往往不是“全部挂掉”,而是“部分异常”。比如:HTTPS能访问,但上传文件失败;或者静态资源正常,但API接口502 Bad Gateway。这时需要一套结构化排查路径,避免盲目重启服务。
我整理了一个故障树(Fault Tree),按现象反向追溯:
现象:浏览器访问https://yourdomain.com显示“Your connection is not private”
→ 检查证书有效期:openssl x509 -in /etc/letsencrypt/live/yourdomain.com/cert.pem -text -noout | grep "Not After"
→ 若已过期,执行sudo certbot renew --force-renewal
→ 若未过期,检查证书链:curl -v https://yourdomain.com 2>&1 | grep "certificate verify",看是否提示unable to get local issuer certificate
→ 是,则fullchain.pem缺失中间证书,重新运行sudo certbot --nginx -d yourdomain.com
现象:curl https://yourdomain.com/api/test返回502 Bad Gateway
→ 检查nginx错误日志:sudo tail -20 /var/log/nginx/error.log,常见错误:connect() failed (111: Connection refused) while connecting to upstream
→ 检查Tomcat是否运行:sudo systemctl status tomcat8
→ 若Tomcat已启动,检查其监听端口:sudo ss -tuln | grep ':8080'
→ 若无输出,检查Tomcat配置/etc/tomcat8/server.xml,确认<Connector port="8080"未被注释
现象:FTP上传文件后,网页无法显示图片
→ 检查文件权限:ls -l /var/www/html/uploads/images/test.jpg,应为-rw-r--r-- 1 ftpuser ftpuser
→ 检查nginx配置是否包含location ~* \.(jpg|jpeg|png|gif)$块
→ 检查该location块是否在location /api/之后(nginx按顺序匹配,位置错误会导致静态请求被代理)
现象:Maven构建时提示Could not find artifact xxx:jar:1.0.0
→ 检查pom.xml中<repository>配置是否指向了私有仓库,而该仓库URL不可达
→ 检查settings.xml中<mirror>是否错误覆盖了私有仓库
→ 执行mvn help:effective-settings,确认实际生效的镜像配置
每一步排查都对应一个可执行的命令,而不是“检查配置”。记住:日志是真相的唯一来源,猜测是效率的最大敌人。我见过太多人花2小时改nginx配置,却没看一眼/var/log/nginx/error.log里早已写明connect to 127.0.0.1:8080 failed (111: Connection refused)。
10. 安全加固清单:那些被忽略但至关重要的收尾动作
环境搭建完成不等于安全。Ubuntu 20.04默认配置存在多个攻击面,必须手动加固。以下是经过生产环境验证的7项关键操作:
1. 关闭不必要的端口
# 查看所有监听端口 sudo ss -tuln # 关闭vsftpd的IPv6监听(除非必需) echo "listen_ipv6=NO" | sudo tee -a /etc/vsftpd.conf sudo systemctl restart vsftpd2. 限制MySQL远程访问
-- 删除所有非localhost的root用户 DELETE FROM mysql.user WHERE Host!='localhost' AND User='root'; -- 刷新权限 FLUSH PRIVILEGES;3. 设置JDK安全策略
编辑$JAVA_HOME/jre/lib/security/java.policy,在末尾添加:
// 禁止Java应用读取系统属性 permission java.util.PropertyPermission "user.home", "read"; // 禁止加载本地库 permission java.lang.RuntimePermission "loadLibrary.*";注意:此操作会影响部分需要调用本地库的框架(如JNI),请先在测试环境验证。
4. nginx日志脱敏
在/etc/nginx/nginx.conf的http块中,修改日志格式:
log_format main '$remote_addr - $remote_user [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent" ' '"$http_x_forwarded_for"'; # 移除$cookie_XXX等敏感字段5. FTP用户Shell锁定
# 确保ftpuser无法执行任何命令 sudo usermod -s /usr/sbin/nologin ftpuser # 验证 sudo su - ftpuser -c "echo test" # 应返回"Cannot execute /usr/sbin/nologin: No such file or directory"6. 自动清理旧内核
Ubuntu 20.04升级后会残留旧内核,占用大量/boot空间:
# 列出所有内核 dpkg --list | grep linux-image # 删除旧内核(保留最新两个) sudo apt autoremove --purge7. 配置fail2ban防暴力破解
sudo apt install fail2ban sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local # 编辑jail.local,启用[sshd]和[vsftpd]段 sudo systemctl enable fail2ban && sudo systemctl start fail2ban最后,执行一次完整的安全扫描:
# 检查系统漏洞 sudo apt install lynis sudo lynis audit system # 检查端口暴露面 sudo nmap -sS -p- yourdomain.comLynis会给出详细报告,比如WARNING: SSH root login allowed或WARNING: Weak SSL/TLS cipher suites detected,这些都是必须立即修复的高危项。
这套环境搭建流程,我已在5个不同行业的项目中复用,从教育平台到物联网后台,最小部署耗时47分钟,最长的一次(含定制化安全加固)也未超过2小时。它不追求“最炫技”,而是坚持“每个配置都有明确目的,每个命令都有可验证结果”。当你下次面对一个空白的Ubuntu 20.04服务器时,希望这份笔记能成为你指尖下的可靠地图——不是告诉你“往哪走”,而是让你看清脚下每一块砖的材质与承重。