本文詳細記錄基於 CentOS 7、Java (Tomcat)、Nginx 搭建郵件代理服務的完整過程。Nginx 作為郵件代理前端,通過 HTTP 認證接口驗證用戶憑證,並從 LDAP 獲取郵件服務器地址,實現 POP3、IMAP、SMTP 協議的透明代理與負載均衡。
郵件客戶端 (Outlook/Thunderbird)
│
▼
Nginx Mail Proxy (110/143/25)
│
├─ auth_http → Java WebApp (Tomcat:8080) → 驗證用戶、查詢 LDAP
│
└─ 代理轉發 → 後端郵件服務器
# 1. 設置主機名
hostnamectl set-hostname mail-proxy.example.com
# 2. 關閉防火牆與 SELinux
systemctl stop firewalld && systemctl disable firewalld
sed -i 's/^SELINUX=.*/SELINUX=disabled/' /etc/selinux/config
# 3. 停用郵件衝突服務
systemctl stop postfix && systemctl disable postfix
# 4. 安裝 Nginx (官方倉庫)
yum install -y https://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm
yum install -y nginx
# 5. 安裝 JDK 8 + Tomcat 8
# 下載 JDK: https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html
tar -zxvf jdk-8uXXX-linux-x64.tar.gz -C /usr/java/
# 配置 /etc/profile: JAVA_HOME, PATH, CLASSPATH
source /etc/profile
# 下載 Tomcat
tar -zxvf apache-tomcat-8.5.xx.tar.gz -C /usr/
# 啟動: /usr/apache-tomcat/bin/startup.sh
/etc/nginx/nginx.conf
user nginx;
worker_processes auto;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
error_log /var/log/nginx/error.log info;
mail {
server_name mail-proxy.example.com;
# 認證接口:Nginx 將用戶憑證 POST 到此 URL,應用返回 Auth-Status 等頭
auth_http https://localhost:8080/mail/auth;
imap_capabilities IMAP4rev1 UIDPLUS IDLE LITERAL+ QUOTA;
pop3_auth plain apop cram-md5;
pop3_capabilities LAST TOP USER PIPELINING UIDL;
smtp_auth login plain cram-md5;
smtp_capabilities "SIZE 10485760" ENHANCEDSTATUSCODES 8BITMIME DSN;
xclient off;
# POP3 代理
server {
listen 110;
protocol pop3;
proxy on;
proxy_pass_error_message on;
}
# IMAP 代理
server {
listen 143;
protocol imap;
proxy on;
}
# SMTP 代理
server {
listen 25;
protocol smtp;
proxy on;
}
}
systemctl enable nginx
systemctl start nginx
systemctl status nginx
# 檢查端口
netstat -anlpt | grep -E '110|143|25|8080'
Nginx 發送請求頭:
Auth-User:用戶名Auth-Pass:密碼Auth-Protocol:pop3/imap/smtpAuth-Login-Attempt:嘗試次數Client-IP:客戶端 IP應用需返回頭:
Auth-Status:OK / 錯誤訊息Auth-Server:後端郵件服務器 IP(關鍵)Auth-Port:端口 (110/143/25/993/995/465/587)Auth-User:轉發給後端的用戶名(可重寫)Auth-Pass:轉發給後端的密碼(可重寫)
@RestController
@RequestMapping("/mail")
public class MailAuthController {
private static final Logger log = LoggerFactory.getLogger(MailAuthController.class);
@Autowired
private LdapService ldapService;
@PostMapping("/auth")
public ResponseEntity<Void> auth(HttpServletRequest request, HttpServletResponse response) {
String user = request.getHeader("Auth-User");
String pass = request.getHeader("Auth-Pass");
String protocol = request.getHeader("Auth-Protocol");
String clientIp = request.getHeader("Client-IP");
log.info("郵件認證請求: user={}, protocol={}, ip={}", user, protocol, clientIp);
// 1. 驗證用戶憑證 (可對接 LDAP、數據庫、單點登入等)
if (!validateCredentials(user, pass)) {
response.setHeader("Auth-Status", "Invalid credentials");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return ResponseEntity.status(HttpServletResponse.SC_UNAUTHORIZED).build();
}
// 2. 從 LDAP 獲取用戶郵件服務器地址
String mailServer = ldapService.getMailServer(user);
if (mailServer == null) {
response.setHeader("Auth-Status", "Mail server not found for user");
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return ResponseEntity.status(HttpServletResponse.SC_NOT_FOUND).build();
}
// 3. 解析服務器地址為 IP (關鍵:Nginx 需要 IP,不支援域名)
String mailServerIp;
try {
mailServerIp = InetAddress.getByName(mailServer).getHostAddress();
} catch (UnknownHostException e) {
log.error("郵件服務器域名解析失敗: {}", mailServer, e);
response.setHeader("Auth-Status", "Mail server resolution failed");
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return ResponseEntity.status(HttpServletResponse.SC_INTERNAL_SERVER_ERROR).build();
}
// 4. 根據協議確定端口
int port = switch (protocol) {
case "pop3" -> 110;
case "imap" -> 143;
case "smtp" -> 25;
default -> 110;
};
// 5. 返回認證成功頭
response.setHeader("Auth-Status", "OK");
response.setHeader("Auth-Server", mailServerIp);
response.setHeader("Auth-Port", String.valueOf(port));
response.setHeader("Auth-User", user); // 透傳或重寫
response.setHeader("Auth-Pass", pass); // 透傳或重寫
return ResponseEntity.ok().build();
}
private boolean validateCredentials(String user, String pass) {
// 實現:LDAP Bind、數據庫查詢、OAuth 驗證等
// 示例:LDAP 簡單綁定驗證
try {
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://ldap.example.com:389");
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, "uid=" + user + ",ou=people,dc=example,dc=com");
env.put(Context.SECURITY_CREDENTIALS, pass);
new InitialDirContext(env).close();
return true;
} catch (NamingException e) {
return false;
}
}
}
@Service
public class LdapService {
@Value("${ldap.url}")
private String ldapUrl;
@Value("${ldap.base-dn}")
private String baseDn;
public String getMailServer(String username) {
try {
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, ldapUrl);
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, "cn=admin," + baseDn);
env.put(Context.SECURITY_CREDENTIALS, "admin_password");
DirContext ctx = new InitialDirContext(env);
// 查詢用戶條目
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
NamingEnumeration<SearchResult> results = ctx.search(
baseDn, "(uid=" + username + ")", controls);
if (results.hasMore()) {
SearchResult result = results.next();
Attributes attrs = result.getAttributes();
// 獲取 mailServer 屬性 (如: mailserver=mail1.example.com)
Attribute mailServerAttr = attrs.get("mailServer");
if (mailServerAttr != null) {
return (String) mailServerAttr.get();
}
}
} catch (NamingException e) {
log.error("LDAP 查詢失敗: {}", username, e);
}
return null;
}
}
現象:返回域名導致代理連接失敗
原因:Nginx mail proxy 的 Auth-Server 必須是 IP 地址
解決:Java 端用 InetAddress.getByName() 解析域名為 IP 後返回
現象:nginx: [emerg] bind() to 0.0.0.0:25 failed (98: Address already in use)
原因:系統預裝 Postfix 佔用 25 端口
解決:
systemctl stop postfix
systemctl disable postfix
systemctl restart nginx
排查:
curl https://localhost:8080/mail/authtail -f /var/log/nginx/error.logauth_http URL 路徑與 Controller mapping 一致
curl -i -H 'Auth-User: testuser' -H 'Auth-Pass: testpass' -H 'Auth-Protocol: pop3' \
https://localhost:8080/mail/auth
預期返回:200 OK,頭包含 Auth-Status: OK, Auth-Server: 192.168.x.x, Auth-Port: 110
# POP3
telnet mail-proxy.example.com 110
# 輸入:
USER testuser
PASS testpass
# IMAP
telnet mail-proxy.example.com 143
# 輸入:
a1 LOGIN testuser testpass
# SMTP
telnet mail-proxy.example.com 25
# 輸入:
EHLO test
AUTH LOGIN
# base64 編碼的 user/pass
mail-proxy.example.com (POP3: 110 / IMAP: 143 / 993 SSL)mail-proxy.example.com (SMTP: 25 / 587 / 465 SSL)| 項目 | 建議 |
|---|---|
| SSL/TLS | 配置 Nginx mail SSL (listen 993 ssl; ssl_certificate...),客戶端強制加密 |
| 高可用 | 多實例 Nginx + Keepalived/VIP,Tomcat 集群 + Nginx upstream |
| 認證安全 | 支援 OAuth2、雙因子認證,限制登錄失敗次數 |
| 監控告警 | Prometheus 監控 Nginx/Tomcat 指標,日誌接入 ELK |
| 速率限制 | Nginx limit_req_zone 限制認證接口頻率,防暴力破解 |
| 審計日誌 | 記錄所有登錄嘗試、郵件發送統計,滿足合規 |