Spring Data Cassandra enable ssl hostname verification

本文介绍了如何在 Spring Data Cassandra 中自定义 SSL 配置以启用 hostname verification。提供了两种实现方式:通过 CqlSessionBuilderCustomizer 和 BeanPostProcessor 接口。示例代码展示了如何创建 CustomSslEngineFactory 来处理 SSL 配置,包括加载信任存储、设置加密模式以及处理 hostname 验证。

现在spring data cassandra是不能通过一个开关直接开启ssl的hostname verification的,也就是所谓的verify-full,所以得自己改下代码.

方案1:

implements CqlSessionBuilderCustomizer

import com.datastax.oss.driver.api.core.CqlSessionBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.cassandra.CassandraProperties;
import org.springframework.boot.autoconfigure.cassandra.CqlSessionBuilderCustomizer;
import org.springframework.stereotype.Component;

@Component
public class CqlSessionBuilderSSLCustomizer implements CqlSessionBuilderCustomizer {

    @Autowired
    CassandraProperties cassandraProperties;

    @Override
    public void customize(CqlSessionBuilder cqlSessionBuilder) {
        if(cassandraProperties.isSsl()){
            CustomSslEngineFactory customSslEngineFactory = new CustomSslEngineFactory("TLS");
            cqlSessionBuilder.withSslEngineFactory(customSslEngineFactory);
        }
    }
}

方案2:

implements BeanPostProcessor

import com.datastax.oss.driver.api.core.CqlSessionBuilder;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.cassandra.CassandraProperties;
import org.springframework.stereotype.Component;

@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {

    @Autowired
    CassandraProperties cassandraProperties;

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if("cassandraSessionBuilder".endsWith(beanName)&&cassandraProperties.isSsl()){
            CqlSessionBuilder cassandraSessionBuilder = (CqlSessionBuilder) bean;
            CustomSslEngineFactory customSslEngineFactory = new CustomSslEngineFactory("TLS");
            cassandraSessionBuilder.withSslEngineFactory(customSslEngineFactory);
        }
        return bean;
    }
}
CustomSslEngineFactory 的代码
import com.datastax.oss.driver.api.core.metadata.EndPoint;
import com.datastax.oss.driver.api.core.ssl.SslEngineFactory;
import edu.umd.cs.findbugs.annotations.NonNull;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.TrustManagerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.security.KeyStore;
import java.security.SecureRandom;

public class CustomSslEngineFactory implements SslEngineFactory {

    private final SSLContext sslContext;

    private final String[] cipherSuites;

    private final boolean requireHostnameValidation;

    private final String TRUST_STORE_PROPERTY = "javax.net.ssl.trustStore";
    private final String TRUST_STORE_PASSWORD_PROPERTY = "javax.net.ssl.trustStorePassword";
    private final String TRUST_STORE_TYPE_PROPERTY = "javax.net.ssl.trustStoreType";

    private final String SSL_HOSTNAMEVERIFIER = "javax.net.ssl.hostnameverifier";

    final String DEFAULT_TRUST_STORE = System.getProperty("java.home") + File.separator + "lib"
            + File.separator + "security" + File.separator + "cacerts";
    private final String DEFAULT_TRUST_STORE_PASSWORD = "changeit";


    public CustomSslEngineFactory(String encryptionMode){
        try {
            this.sslContext = buildContext(encryptionMode);
        } catch (Exception e){
            throw new IllegalStateException("Cannot initialize SSL Context",e);
        }

        this.cipherSuites = null;

        String hostnameverifier = System.getProperty(SSL_HOSTNAMEVERIFIER);
        if("false".equals(hostnameverifier)){
            this.requireHostnameValidation = false;
        }else {
            this.requireHostnameValidation = true;
        }

    }
    @NonNull
    @Override
    public SSLEngine newSslEngine(@NonNull EndPoint remoteEndpoint) {
        SSLEngine engine;
        SocketAddress remoteAddress = remoteEndpoint.resolve();
        if (remoteAddress instanceof InetSocketAddress) {
            InetSocketAddress socketAddress = (InetSocketAddress) remoteAddress;
            engine = sslContext.createSSLEngine(socketAddress.getHostName(),socketAddress.getPort());
        } else {
            engine = sslContext.createSSLEngine();
        }
        engine.setUseClientMode(true);
        if (cipherSuites != null) {
            engine.setEnabledCipherSuites(cipherSuites);
        }
        if (requireHostnameValidation) {
            SSLParameters parameters = engine.getSSLParameters();
            parameters.setEndpointIdentificationAlgorithm("HTTPS");
            engine.setSSLParameters(parameters);
        }
        return engine;
    }

    protected SSLContext buildContext(String encryptionMode) throws Exception {
        String trustStore = System.getProperty(TRUST_STORE_PROPERTY);
        String trustStorePassword = System.getProperty(TRUST_STORE_PASSWORD_PROPERTY);

        if (null == trustStore || "".equals(trustStore)) {
            trustStore = DEFAULT_TRUST_STORE;
        }
        if (null == trustStorePassword || "".equals(trustStorePassword)) {
            trustStorePassword = DEFAULT_TRUST_STORE_PASSWORD;
        }
        String storePropType = System.getProperty(TRUST_STORE_TYPE_PROPERTY,
                KeyStore.getDefaultType());
        KeyStore ks = KeyStore.getInstance(storePropType);
        InputStream trustStoreStream = new FileInputStream(trustStore);
        ks.load(trustStoreStream, trustStorePassword.toCharArray());
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(ks);

        SSLContext sslContext = SSLContext.getInstance(encryptionMode.trim().toUpperCase());
        sslContext.init(null, tmf.getTrustManagers(), new SecureRandom());
        return sslContext;

    }

    @Override
    public void close() throws Exception {
        // noting to do
    }
}

#!/bin/bash # ======================================================== # WordPress + Workerman WebSocket IM 一键部署脚本(最终完整修复版) # 功能:全自动创建插件、安装依赖、生成服务、前端页、Nginx 配置、systemd 服务 # 特性:JWT 认证 | 日志追踪 | 权限控制 | 多环境兼容 | 自动重连 # 使用:chmod +x install-wp-im.sh && ./install-wp-im.sh --domain example.com --site-root /var/www/html # ======================================================== set -euo pipefail RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' echo_red() { echo -e "${RED}$1${NC}"; } echo_green() { echo -e "${GREEN}$1${NC}"; } echo_yellow() { echo -e "${YELLOW}$1${NC}"; } echo_blue() { echo -e "${BLUE}$1${NC}"; } # ============ 参数解析 ============ DOMAIN="" SITE_ROOT="" HELP=false while [[ "$#" -gt 0 ]]; do case $1 in --domain) DOMAIN="$2"; shift ;; --site-root) SITE_ROOT="$2"; shift ;; -h|--help) HELP=true; break ;; *) echo_red "❌ 未知参数: $1"; exit 1 ;; esac shift done if [ "$HELP" = true ]; then cat << 'EOF' 用法: ./install-wp-im.sh --domain <域名> --site-root <网站根目录> 示例: ./install-wp-im.sh --domain chat.example.com --site-root /www/wwwroot/chat_example_com 功能: ✅ 创建 WordPress 插件目录 ✅ 自动生成 im-server.php (支持 JWT) ✅ 安装 workerman/workerman + firebase/php-jwt ✅ 生成前端测试页面 websocket-test.html ✅ 生成 systemd 开机自启服务 ✅ 输出 Nginx 反向代理配置片段 注意事项: - 需要 PHP CLI、curl、openssl 支持 - 确保 443 和 2121 端口可用 - 推荐用户: www 或 www-data EOF exit 0 fi if [[ -z "$DOMAIN" || -z "$SITE_ROOT" ]]; then echo_red "❌ 错误: 必须指定 --domain 和 --site-root" echo "运行 $0 --help 查看帮助" exit 1 fi # ============ 变量定义 ============ PLUGIN_NAME="wp-im-plugin" PLUGIN_DIR="$SITE_ROOT/wp-content/plugins/$PLUGIN_NAME" IM_SERVER="$PLUGIN_DIR/im-server.php" WP_PLUGIN_MAIN="$PLUGIN_DIR/$PLUGIN_NAME.php" FRONTEND_TEST="$SITE_ROOT/websocket-test.html" NGINX_CONF="/www/server/panel/vhost/nginx/${DOMAIN}.conf" if [[ ! -f "$NGINX_CONF" ]]; then NGINX_CONF="/etc/nginx/sites-available/$DOMAIN.conf" fi SYSTEMD_SERVICE="/etc/systemd/system/wp-im-server.service" LOG_FILE="/var/log/wp-im-server.log" WORKER_USER="www-data" if id "www" &>/dev/null; then WORKER_USER="www"; fi SECRET_KEY_FILE="$PLUGIN_DIR/.secret.key" SECRET_KEY="${SECRET_KEY:-$(openssl rand -base64 32 | tr -d '\n=' | cut -c1-24)}" # 持久化 SECRET_KEY 到文件,避免重启后失效 mkdir -p "$PLUGIN_DIR" echo "$SECRET_KEY" > "$SECRET_KEY_FILE" chmod 600 "$SECRET_KEY_FILE" chown "$WORKER_USER:$WORKER_USER" "$SECRET_KEY_FILE" echo_blue "🚀 开始部署增强型 WebSocket IM 系统" echo "🌐 域名: $DOMAIN" echo "📁 网站根目录: $SITE_ROOT" echo "🔌 插件路径: $PLUGIN_DIR" # ============ 1. 创建插件目录 ============ echo_blue "📁 创建插件目录..." mkdir -p "$PLUGIN_DIR" # ============ 2. 生成 im-server.php (带 JWT 认证) ============ cat > "$IM_SERVER" << EOF #!/usr/bin/env php <?php /** * Enhanced Workerman WebSocket Server with JWT Authentication * 文件: $IM_SERVER */ use Workerman\\Worker; use Workerman\\Connection\\TcpConnection; use Firebase\\JWT\\JWT; // 引入 Composer 自动加载器 require_once __DIR__ . '/vendor/autoload.php'; // 从文件读取密钥(更安全) \$secret_key_file = __DIR__ . '/.secret.key'; if (!file_exists(\$secret_key_file)) { file_put_contents('/tmp/workerman_stderr.log', "❌ 密钥文件不存在: \$secret_key_file\\n", FILE_APPEND); exit(1); } \$secret_key = trim(file_get_contents(\$secret_key_file)); \$worker = new Worker('websocket://0.0.0.0:2121'); \$worker->name = 'WP_IM_Server'; \$worker->count = 1; \$worker->user = '$WORKER_USER'; \$worker->connectionsById = []; \$worker->onWorkerStart = function () { file_put_contents('/tmp/workerman_stdout.log', "✅ IM Server started at " . date('Y-m-d H:i:s') . "\\n", FILE_APPEND); }; \$worker->onConnect = function (TcpConnection \$conn) { \$conn->userId = null; \$conn->userName = '匿名'; file_put_contents('/tmp/workerman_stdout.log', "🔗 新连接来自 {\$conn->getRemoteIp()}:{\$conn->getRemotePort()}\\n", FILE_APPEND); }; \$worker->onMessage = function (TcpConnection \$conn, \$data) use (\$worker, \$secret_key) { try { \$msg = json_decode(\$data, true, 512, JSON_THROW_ON_ERROR); if (\$msg['type'] === 'auth') { \$token = \$msg['token'] ?? ''; if (empty(\$token)) { \$conn->send(json_encode(['type' => 'error', 'message' => '缺少认证 Token'])); return; } try { \$decoded = JWT::decode(\$token, new Firebase\\JWT\\Key(\$secret_key, 'HS256')); \$userId = \$decoded->sub; \$userName = htmlspecialchars(\$decoded->name ?? "用户{\$userId}"); \$conn->userId = \$userId; \$conn->userName = \$userName; // 存储按 ID 查找连接 \$worker->connectionsById[\$userId] = \$conn; // 广播上线消息 \$online_msg = json_encode([ 'type' => 'user_online', 'userId' => \$userId, 'userName' => \$userName, 'time' => date('H:i') ]); foreach (\$worker->connections as \$c) { if (\$c !== \$conn) { \$c->send(\$online_msg); } } \$conn->send(json_encode([ 'type' => 'authenticated', 'message' => "欢迎回来,{\$userName}" ])); file_put_contents('/tmp/workerman_stdout.log', "👤 用户 {\$userId}({\$userName}) 已认证上线\\n", FILE_APPEND); } catch (Exception \$e) { \$conn->send(json_encode(['type' => 'error', 'message' => 'Token 无效'])); file_put_contents('/tmp/workerman_stdout.log', "⚠️ 认证失败: {\$e->getMessage()}\\n", FILE_APPEND); \$conn->close(); } return; } if (\$msg['type'] === 'chat') { if (!\$conn->userId) { \$conn->send(json_encode(['type' => 'error', 'message' => '请先登录'])); return; } \$content = htmlspecialchars(\$msg['content'] ?? ''); if (empty(\$content)) return; \$fromName = \$conn->userName; \$fromId = \$conn->userId; \$chat_msg = json_encode([ 'type' => 'chat_message', 'fromUserId' => \$fromId, 'fromUserName' => \$fromName, 'content' => \$content, 'time' => date('H:i') ]); foreach (\$worker->connections as \$c) { \$c->send(\$chat_msg); } file_put_contents('/tmp/workerman_stdout.log', "💬 聊天: [{\$fromName}] {\$content}\\n", FILE_APPEND); } } catch (Exception \$e) { file_put_contents('/tmp/workerman_stdout.log', "⚠️ 消息解析失败: {\$e->getMessage()}\\n", FILE_APPEND); } }; \$worker->onClose = function (TcpConnection \$conn) use (\$worker) { if (\$conn->userId && \$conn->userName) { unset(\$worker->connectionsById[\$conn->userId]); \$offline_msg = json_encode([ 'type' => 'user_offline', 'userId' => \$conn->userId, 'userName' => \$conn->userName, 'time' => date('H:i') ]); foreach (\$worker->connections as \$c) { if (\$c !== \$conn) { \$c->send(\$offline_msg); } } file_put_contents('/tmp/workerman_stdout.log', "👋 用户 {\$conn->userId}({$conn->userName}) 下线\\n", FILE_APPEND); } }; if (basename(__FILE__) == 'im-server.php') { Worker::runAll(); } EOF chmod +x "$IM_SERVER" echo_green "✅ 已生成并授权 im-server.php (支持 JWT)" # ============ 3. 初始化 Composer 并安装依赖 ============ echo_blue "📦 初始化 Composer 并安装依赖..." cd "$PLUGIN_DIR" if [ ! -f "composer.json" ]; then cat > composer.json << 'EOF' { "name": "vendor/wp-im-plugin", "description": "Real-time WebSocket IM for WordPress", "type": "project", "require": { "workerman/workerman": "^4.1", "firebase/php-jwt": "^6.0" }, "config": { "allow-plugins": true }, "autoload": { "psr-4": { "IMPlugin\\": "src/" } } } EOF echo_green "📄 已创建 composer.json" else echo_yellow "📄 composer.json 已存在,跳过创建" fi if [ ! -d "vendor" ] || [ ! -f "vendor/autoload.php" ]; then if ! command -v composer &> /dev/null; then echo_blue "⬇️ 正在安装 Composer..." curl -sS https://getcomposer.org/installer | php >/dev/null 2>&1 sudo mv composer.phar /usr/local/bin/composer echo_green "✅ Composer 已安装" fi echo_blue "⬇️ 正在安装 PHP 依赖..." COMPOSER_PROCESS_TIMEOUT=300 composer install --no-dev --optimize-autoloader --quiet echo_green "✅ 依赖安装完成" else echo_green "✅ vendor 目录已存在,跳过安装" fi # ============ 4. 生成 WordPress 主插件文件 ============ cat > "$WP_PLUGIN_MAIN" << EOF <?php /** * Plugin Name: WP IM Plugin (Enhanced) * Description: 实时聊天系统,集成 Workerman WebSocket 与 JWT 身份认证 * Version: 1.1 * Author: Admin */ if (!defined('ABSPATH')) exit; function wp_im_generate_jwt_token() { if (!is_user_logged_in()) return ''; \$user = wp_get_current_user(); \$payload = [ 'sub' => \$user->ID, 'name' => \$user->display_name, 'iat' => time(), 'exp' => time() + 3600 ]; \$secret = '$SECRET_KEY'; return \\Firebase\\JWT\\JWT::encode(\$payload, \$secret, 'HS256'); } function wp_im_inject_client_script() { \$token = wp_im_generate_jwt_token(); ?> <script> window.WP_IM_CONFIG = { wsUrl: "wss://$DOMAIN/im-ws", token: "\$token" }; </script> <?php } add_action('wp_head', 'wp_im_inject_client_script'); EOF chown "$WORKER_USER:$WORKER_USER" "$WP_PLUGIN_MAIN" echo_green "✅ 已生成 WordPress 插件主文件" # ============ 5. 生成前端测试页面 ============ cat > "$FRONTEND_TEST" << 'EOF' <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8" /> <title>WebSocket IM 测试</title> <style> body { font-family: Arial, sans-serif; padding: 20px; background: #f9f9f9; } #status { margin: 10px 0; font-weight: bold; } #logs { list-style: none; padding: 0; max-height: 400px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; background: white; } li { margin: 5px 0; padding: 5px; border-left: 3px solid #ccc; } .sent { border-color: #0073aa; color: #0073aa; } .received { border-color: #51a351; color: #51a351; } input[type=text] { width: 300px; padding: 8px; } button { padding: 8px 15px; background: #0073aa; color: white; border: none; cursor: pointer; } </style> </head> <body> <h1>WebSocket IM 实时聊天测试</h1> <div id="status">状态:等待连接...</div> <ul id="logs"></ul> <div style="margin-top: 20px;"> <input type="text" id="msgInput" placeholder="输入消息..." /> <button onclick="sendMsg()">发送</button> </div> <script> const WS_URL = window.WP_IM_CONFIG?.wsUrl || "wss://DOMAIN/im-ws"; let ws = null; function log(msg, cls = "") { const li = document.createElement("li"); li.className = cls; li.textContent = new Date().toLocaleTimeString() + " - " + msg; document.getElementById("logs").appendChild(li); document.getElementById("logs").scrollTop = document.getElementById("logs").scrollHeight; } function connect() { ws = new WebSocket(WS_URL); ws.onopen = () => { log("✅ 连接成功!"); document.getElementById("status").textContent = "✅ 已连接"; if (window.WP_IM_CONFIG?.token) { ws.send(JSON.stringify({ type: "auth", token: window.WP_IM_CONFIG.token })); log("🔐 正在认证..."); } else { log("ℹ️ 游客模式,无法发言", "received"); } }; ws.onmessage = (e) => { const data = JSON.parse(e.data); switch(data.type) { case 'user_online': log("🟢 " + data.userName + " 上线"); break; case 'user_offline': log("🔴 " + data.userName + " 下线"); break; case 'chat_message': log(\`\${data.time} [\${data.fromUserName}]: \${data.content}\`, "received"); break; case 'authenticated': log("👋 " + data.message, "received"); break; case 'error': log("❌ " + data.message, "received"); break; } }; ws.onerror = (e) => log("❌ 错误发生"); ws.onclose = () => setTimeout(connect, 3000); function sendMsg() { const input = document.getElementById("msgInput"); const val = input.value.trim(); if (val && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: "chat", content: val })); log("你: " + val, "sent"); input.value = ""; } } document.getElementById("msgInput").addEventListener("keypress", e => { if (e.key === "Enter") sendMsg(); }); } connect(); </script> </body> </html> EOF sed -i "s|DOMAIN|$DOMAIN|g" "$FRONTEND_TEST" echo_green "✅ 已生成前端测试页面: $FRONTEND_TEST" # ============ 6. 输出 Nginx 配置 ============ echo_blue "📌【重要】请将以下配置添加到你的 Nginx 中:" cat << EOF server { listen 80; server_name $DOMAIN; return 301 https://\$server_name\$request_uri; } server { listen 443 ssl http2; server_name $DOMAIN; root $SITE_ROOT; index index.php index.html; ssl_certificate /ssl/fullchain.pem; ssl_certificate_key /ssl/privkey.pem; location / { try_files \$uri \$uri/ /index.php?\$args; } # WebSocket 反向代理 location /im-ws { proxy_pass http://127.0.0.1:2121; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection "upgrade"; 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; proxy_buffering off; proxy_read_timeout 86400s; proxy_send_timeout 86400s; } location ~ \\.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.1-fpm.sock; fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name; include fastcgi_params; } } EOF # ============ 7. 生成 systemd 服务 ============ cat > "$SYSTEMD_SERVICE" << EOF [Unit] Description=WordPress IM WebSocket Server After=network.target [Service] Type=simple User=$WORKER_USER Group=$WORKER_USER WorkingDirectory=$PLUGIN_DIR ExecStart=/usr/bin/php im-server.php start ExecStop=/usr/bin/php im-server.php stop Restart=always StandardOutput=append:$LOG_FILE StandardError=append:$LOG_FILE [Install] WantedBy=multi-user.target EOF sudo systemctl daemon-reload echo_green "✅ 已生成 systemd 服务: $SYSTEMD_SERVICE" # ============ 8. 设置权限和日志 ============ sudo touch "$LOG_FILE" sudo chown "$WORKER_USER:$WORKER_USER" "$LOG_FILE" sudo chmod 644 "$LOG_FILE" sudo chown -R "$WORKER_USER:$WORKER_USER" "$PLUGIN_DIR" echo_green "✅ 权限设置完成" # ============ 9. 最终提示 ============ cat << EOF 🎉 ========================================= ✅ IM 系统部署成功! ========================================== 🔧 1. 编辑 Nginx 配置: sudo nano $NGINX_CONF 添加上面的 server 块或合并到现有站点 🔄 2. 重启 Nginx: sudo systemctl restart nginx ▶️ 3. 启动 IM 服务: sudo systemctl enable wp-im-server sudo systemctl start wp-im-server 🔍 4. 查看状态: sudo systemctl status wp-im-server tail -f /tmp/workerman_stdout.log 🌐 5. 测试访问: https://$DOMAIN/websocket-test.html 🔑 SECRET_KEY: $SECRET_KEY (请备份用于后续开发) 💡 扩展建议: - 添加数据库记录消息历史 - 实现私聊功能(toUserId) - 集成 Redis 存储在线状态 - 使用 WAF 防止攻击 💪 你现在可以实现完整的实时通信系统! EOF 生成完整版源文件
最新发布
11-14
你已经提供了一个非常完整的 Bash 部署脚本,用于在 WordPress 环境中一键部署基于 **Workerman + WebSocket + JWT 认证** 的实时聊天系统(IM)。现在你希望我: ✅ **生成一个完整、可直接保存为 `.sh` 文件的源文件版本** --- ### ✅ 以下是经过修复和优化后的【最终完整可执行版】脚本 > ⚠️ 已解决:`conn: unbound variable` 错误 > ✅ 使用 `<< 'EOF'` 防止变量展开 > ✅ 添加 `sed` 动态注入 `$WORKER_USER` 和 `$DOMAIN` > ✅ 兼容宝塔面板 Nginx 路径 > ✅ 安全地处理 SECRET_KEY --- ```bash #!/bin/bash # ======================================================== # WordPress + Workerman WebSocket IM 一键部署脚本(最终完整修复版) # 功能:全自动创建插件、安装依赖、生成服务、前端页、Nginx 配置、systemd 服务 # 特性:JWT 认证 | 日志追踪 | 权限控制 | 多环境兼容 | 自动重连 # 使用:chmod +x install-wp-im.sh && ./install-wp-im.sh --domain example.com --site-root /var/www/html # ======================================================== set -euo pipefail RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' echo_red() { echo -e "${RED}$1${NC}"; } echo_green() { echo -e "${GREEN}$1${NC}"; } echo_yellow() { echo -e "${YELLOW}$1${NC}"; } echo_blue() { echo -e "${BLUE}$1${NC}"; } # ============ 参数解析 ============ DOMAIN="" SITE_ROOT="" HELP=false while [[ "$#" -gt 0 ]]; do case $1 in --domain) DOMAIN="$2"; shift ;; --site-root) SITE_ROOT="$2"; shift ;; -h|--help) HELP=true; break ;; *) echo_red "❌ 未知参数: $1"; exit 1 ;; esac shift done if [ "$HELP" = true ]; then cat << 'EOF' 用法: ./install-wp-im.sh --domain <域名> --site-root <网站根目录> 示例: ./install-wp-im.sh --domain chat.example.com --site-root /www/wwwroot/chat_example_com 功能: ✅ 创建 WordPress 插件目录 ✅ 自动生成 im-server.php (支持 JWT) ✅ 安装 workerman/workerman + firebase/php-jwt ✅ 生成前端测试页面 websocket-test.html ✅ 生成 systemd 开机自启服务 ✅ 输出 Nginx 反向代理配置片段 注意事项: - 需要 PHP CLI、curl、openssl 支持 - 确保 443 和 2121 端口可用 - 推荐用户: www 或 www-data EOF exit 0 fi if [[ -z "$DOMAIN" || -z "$SITE_ROOT" ]]; then echo_red "❌ 错误: 必须指定 --domain 和 --site-root" echo "运行 $0 --help 查看帮助" exit 1 fi # ============ 变量定义 ============ PLUGIN_NAME="wp-im-plugin" PLUGIN_DIR="$SITE_ROOT/wp-content/plugins/$PLUGIN_NAME" IM_SERVER="$PLUGIN_DIR/im-server.php" WP_PLUGIN_MAIN="$PLUGIN_DIR/$PLUGIN_NAME.php" FRONTEND_TEST="$SITE_ROOT/websocket-test.html" NGINX_CONF="/www/server/panel/vhost/nginx/${DOMAIN}.conf" if [[ ! -f "$NGINX_CONF" ]]; then NGINX_CONF="/etc/nginx/sites-available/$DOMAIN.conf" fi SYSTEMD_SERVICE="/etc/systemd/system/wp-im-server.service" LOG_FILE="/var/log/wp-im-server.log" WORKER_USER="www-data" if id "www" &>/dev/null; then WORKER_USER="www"; fi SECRET_KEY="${SECRET_KEY:-$(openssl rand -base64 32 | tr -d '\n=' | cut -c1-24)}" SECRET_KEY_FILE="$PLUGIN_DIR/.secret.key" # 持久化 SECRET_KEY 到文件,避免重启后失效 mkdir -p "$PLUGIN_DIR" echo "$SECRET_KEY" > "$SECRET_KEY_FILE" chmod 600 "$SECRET_KEY_FILE" chown "$WORKER_USER:$WORKER_USER" "$SECRET_KEY_FILE" echo_blue "🚀 开始部署增强型 WebSocket IM 系统" echo "🌐 域名: $DOMAIN" echo "📁 网站根目录: $SITE_ROOT" echo "🔌 插件路径: $PLUGIN_DIR" # ============ 1. 创建插件目录 ============ echo_blue "📁 创建插件目录..." mkdir -p "$PLUGIN_DIR" # ============ 2. 生成 im-server.php (带 JWT 认证) ============ cat > "$IM_SERVER" << 'EOF' #!/usr/bin/env php <?php /** * Enhanced Workerman WebSocket Server with JWT Authentication */ use Workerman\Worker; use Workerman\Connection\TcpConnection; use Firebase\JWT\JWT; // 引入 Composer 自动加载器 require_once __DIR__ . '/vendor/autoload.php'; // 从文件读取密钥(更安全) $secret_key_file = __DIR__ . '/.secret.key'; if (!file_exists($secret_key_file)) { file_put_contents('/tmp/workerman_stderr.log', "❌ 密钥文件不存在: $secret_key_file\n", FILE_APPEND); exit(1); } $secret_key = trim(file_get_contents($secret_key_file)); $worker = new Worker('websocket://0.0.0.0:2121'); $worker->name = 'WP_IM_Server'; $worker->count = 1; $worker->user = 'WORKER_USER_PLACEHOLDER'; // 占位符,后续替换 $worker->connectionsById = []; $worker->onWorkerStart = function () { file_put_contents('/tmp/workerman_stdout.log', "✅ IM Server started at " . date('Y-m-d H:i:s') . "\n", FILE_APPEND); }; $worker->onConnect = function (TcpConnection $conn) { $conn->userId = null; $conn->userName = '匿名'; file_put_contents('/tmp/workerman_stdout.log', "🔗 新连接来自 {$conn->getRemoteIp()}:{$conn->getRemotePort()}\n", FILE_APPEND); }; $worker->onMessage = function (TcpConnection $conn, $data) use ($worker, $secret_key) { try { $msg = json_decode($data, true, 512, JSON_THROW_ON_ERROR); if ($msg['type'] === 'auth') { $token = $msg['token'] ?? ''; if (empty($token)) { $conn->send(json_encode(['type' => 'error', 'message' => '缺少认证 Token'])); return; } try { $decoded = JWT::decode($token, new Firebase\JWT\Key($secret_key, 'HS256')); $userId = $decoded->sub; $userName = htmlspecialchars($decoded->name ?? "用户{$userId}"); $conn->userId = $userId; $conn->userName = $userName; // 存储按 ID 查找连接 $worker->connectionsById[$userId] = $conn; // 广播上线消息 $online_msg = json_encode([ 'type' => 'user_online', 'userId' => $userId, 'userName' => $userName, 'time' => date('H:i') ]); foreach ($worker->connections as $c) { if ($c !== $conn) { $c->send($online_msg); } } $conn->send(json_encode([ 'type' => 'authenticated', 'message' => "欢迎回来,{$userName}" ])); file_put_contents('/tmp/workerman_stdout.log', "👤 用户 {$userId}({$userName}) 已认证上线\n", FILE_APPEND); } catch (Exception $e) { $conn->send(json_encode(['type' => 'error', 'message' => 'Token 无效'])); file_put_contents('/tmp/workerman_stdout.log', "⚠️ 认证失败: {$e->getMessage()}\n", FILE_APPEND); $conn->close(); } return; } if ($msg['type'] === 'chat') { if (!$conn->userId) { $conn->send(json_encode(['type' => 'error', 'message' => '请先登录'])); return; } $content = htmlspecialchars($msg['content'] ?? ''); if (empty($content)) return; $fromName = $conn->userName; $fromId = $conn->userId; $chat_msg = json_encode([ 'type' => 'chat_message', 'fromUserId' => $fromId, 'fromUserName' => $fromName, 'content' => $content, 'time' => date('H:i') ]); foreach ($worker->connections as $c) { $c->send($chat_msg); } file_put_contents('/tmp/workerman_stdout.log', "💬 聊天: [{$fromName}] {$content}\n", FILE_APPEND); } } catch (Exception $e) { file_put_contents('/tmp/workerman_stdout.log', "⚠️ 消息解析失败: {$e->getMessage()}\n", FILE_APPEND); } }; $worker->onClose = function (TcpConnection $conn) use ($worker) { if ($conn->userId && $conn->userName) { unset($worker->connectionsById[$conn->userId]); $offline_msg = json_encode([ 'type' => 'user_offline', 'userId' => $conn->userId, 'userName' => $conn->userName, 'time' => date('H:i') ]); foreach ($worker->connections as $c) { if ($c !== $conn) { $c->send($offline_msg); } } file_put_contents('/tmp/workerman_stdout.log', "👋 用户 {$conn->userId}({$conn->userName}) 下线\n", FILE_APPEND); } }; if (basename(__FILE__) == 'im-server.php') { Worker::runAll(); } EOF # 替换占位符 sed -i "s|WORKER_USER_PLACEHOLDER|$WORKER_USER|" "$IM_SERVER" chmod +x "$IM_SERVER" echo_green "✅ 已生成并授权 im-server.php (支持 JWT)" # ============ 3. 初始化 Composer 并安装依赖 ============ echo_blue "📦 初始化 Composer 并安装依赖..." cd "$PLUGIN_DIR" if [ ! -f "composer.json" ]; then cat > composer.json << 'EOF' { "name": "vendor/wp-im-plugin", "description": "Real-time WebSocket IM for WordPress", "type": "project", "require": { "workerman/workerman": "^4.1", "firebase/php-jwt": "^6.0" }, "config": { "allow-plugins": true }, "autoload": { "psr-4": { "IMPlugin\\": "src/" } } } EOF echo_green "📄 已创建 composer.json" else echo_yellow "📄 composer.json 已存在,跳过创建" fi if [ ! -d "vendor" ] || [ ! -f "vendor/autoload.php" ]; then if ! command -v composer &> /dev/null; then echo_blue "⬇️ 正在安装 Composer..." curl -sS https://getcomposer.org/installer | php >/dev/null 2>&1 mv composer.phar /usr/local/bin/composer echo_green "✅ Composer 已安装" fi echo_blue "⬇️ 正在安装 PHP 依赖..." COMPOSER_PROCESS_TIMEOUT=300 composer install --no-dev --optimize-autoloader --quiet echo_green "✅ 依赖安装完成" else echo_green "✅ vendor 目录已存在,跳过安装" fi # ============ 4. 生成 WordPress 主插件文件 ============ cat > "$WP_PLUGIN_MAIN" << EOF <?php /** * Plugin Name: WP IM Plugin (Enhanced) * Description: 实时聊天系统,集成 Workerman WebSocket 与 JWT 身份认证 * Version: 1.1 * Author: Admin */ if (!defined('ABSPATH')) exit; function wp_im_generate_jwt_token() { if (!is_user_logged_in()) return ''; \$user = wp_get_current_user(); \$payload = [ 'sub' => \$user->ID, 'name' => \$user->display_name, 'iat' => time(), 'exp' => time() + 3600 ]; \$secret = '$SECRET_KEY'; return \\Firebase\\JWT\\JWT::encode(\$payload, \$secret, 'HS256'); } function wp_im_inject_client_script() { \$token = wp_im_generate_jwt_token(); ?> <script> window.WP_IM_CONFIG = { wsUrl: "wss://$DOMAIN/im-ws", token: "\$token" }; </script> <?php } add_action('wp_head', 'wp_im_inject_client_script'); EOF chown "$WORKER_USER:$WORKER_USER" "$WP_PLUGIN_MAIN" echo_green "✅ 已生成 WordPress 插件主文件" # ============ 5. 生成前端测试页面 ============ cat > "$FRONTEND_TEST" << 'EOF' <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8" /> <title>WebSocket IM 测试</title> <style> body { font-family: Arial, sans-serif; padding: 20px; background: #f9f9f9; } #status { margin: 10px 0; font-weight: bold; } #logs { list-style: none; padding: 0; max-height: 400px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; background: white; } li { margin: 5px 0; padding: 5px; border-left: 3px solid #ccc; } .sent { border-color: #0073aa; color: #0073aa; } .received { border-color: #51a351; color: #51a351; } input[type=text] { width: 300px; padding: 8px; } button { padding: 8px 15px; background: #0073aa; color: white; border: none; cursor: pointer; } </style> </head> <body> <h1>WebSocket IM 实时聊天测试</h1> <div id="status">状态:等待连接...</div> <ul id="logs"></ul> <div style="margin-top: 20px;"> <input type="text" id="msgInput" placeholder="输入消息..." /> <button onclick="sendMsg()">发送</button> </div> <script> const WS_URL = window.WP_IM_CONFIG?.wsUrl || "wss://DOMAIN/im-ws"; let ws = null; function log(msg, cls = "") { const li = document.createElement("li"); li.className = cls; li.textContent = new Date().toLocaleTimeString() + " - " + msg; document.getElementById("logs").appendChild(li); document.getElementById("logs").scrollTop = document.getElementById("logs").scrollHeight; } function connect() { ws = new WebSocket(WS_URL); ws.onopen = () => { log("✅ 连接成功!"); document.getElementById("status").textContent = "✅ 已连接"; if (window.WP_IM_CONFIG?.token) { ws.send(JSON.stringify({ type: "auth", token: window.WP_IM_CONFIG.token })); log("🔐 正在认证..."); } else { log("ℹ️ 游客模式,无法发言", "received"); } }; ws.onmessage = (e) => { const data = JSON.parse(e.data); switch(data.type) { case 'user_online': log("🟢 " + data.userName + " 上线"); break; case 'user_offline': log("🔴 " + data.userName + " 下线"); break; case 'chat_message': log(\`\${data.time} [\${data.fromUserName}]: \${data.content}\`, "received"); break; case 'authenticated': log("👋 " + data.message, "received"); break; case 'error': log("❌ " + data.message, "received"); break; } }; ws.onerror = (e) => log("❌ 错误发生"); ws.onclose = () => setTimeout(connect, 3000); function sendMsg() { const input = document.getElementById("msgInput"); const val = input.value.trim(); if (val && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: "chat", content: val })); log("你: " + val, "sent"); input.value = ""; } } document.getElementById("msgInput").addEventListener("keypress", e => { if (e.key === "Enter") sendMsg(); }); } connect(); </script> </body> </html> EOF sed -i "s|DOMAIN|$DOMAIN|g" "$FRONTEND_TEST" echo_green "✅ 已生成前端测试页面: $FRONTEND_TEST" # ============ 6. 输出 Nginx 配置 ============ echo_blue "📌【重要】请将以下配置添加到你的 Nginx 中:" cat << EOF server { listen 80; server_name $DOMAIN; return 301 https://\$server_name\$request_uri; } server { listen 443 ssl http2; server_name $DOMAIN; root $SITE_ROOT; index index.php index.html; ssl_certificate /ssl/fullchain.pem; ssl_certificate_key /ssl/privkey.pem; location / { try_files \$uri \$uri/ /index.php?\$args; } # WebSocket 反向代理 location /im-ws { proxy_pass http://127.0.0.1:2121; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection "upgrade"; 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; proxy_buffering off; proxy_read_timeout 86400s; proxy_send_timeout 86400s; } location ~ \\.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.1-fpm.sock; fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name; include fastcgi_params; } } EOF # ============ 7. 生成 systemd 服务 ============ cat > "$SYSTEMD_SERVICE" << EOF [Unit] Description=WordPress IM WebSocket Server After=network.target [Service] Type=simple User=$WORKER_USER Group=$WORKER_USER WorkingDirectory=$PLUGIN_DIR ExecStart=/usr/bin/php im-server.php start ExecStop=/usr/bin/php im-server.php stop Restart=always StandardOutput=append:$LOG_FILE StandardError=append:$LOG_FILE [Install] WantedBy=multi-user.target EOF sudo systemctl daemon-reload echo_green "✅ 已生成 systemd 服务: $SYSTEMD_SERVICE" # ============ 8. 设置权限和日志 ============ sudo touch "$LOG_FILE" sudo chown "$WORKER_USER:$WORKER_USER" "$LOG_FILE" sudo chmod 644 "$LOG_FILE" sudo chown -R "$WORKER_USER:$WORKER_USER" "$PLUGIN_DIR" echo_green "✅ 权限设置完成" # ============ 9. 最终提示 ============ cat << EOF 🎉 ========================================= ✅ IM 系统部署成功! ========================================== 🔧 1. 编辑 Nginx 配置: sudo nano $NGINX_CONF 添加上面的 server 块或合并到现有站点 🔄 2. 重启 Nginx: sudo systemctl restart nginx ▶️ 3. 启动 IM 服务: sudo systemctl enable wp-im-server sudo systemctl start wp-im-server 🔍 4. 查看状态: sudo systemctl status wp-im-server tail -f /tmp/workerman_stdout.log 🌐 5. 测试访问: https://$DOMAIN/websocket-test.html 🔑 SECRET_KEY: $SECRET_KEY (请备份用于后续开发) 💡 扩展建议: - 添加数据库记录消息历史 - 实现私聊功能(toUserId) - 集成 Redis 存储在线状态 - 使用 WAF 防止攻击 💪 你现在可以实现完整的实时通信系统! EOF ``` --- ### 📥 如何使用? 1. 保存为文件: ```bash nano install-wp-im.sh ``` 粘贴上述内容,保存。 2. 赋权并运行: ```bash chmod +x install-wp-im.sh ./install-wp-im.sh --domain yfw.szrengjing.com --site-root /www/wwwroot/yfw_szrengjing_com ``` --- ### ✅ 已解决问题汇总 | 问题 | 解决方式 | |------|----------| | `conn: unbound variable` | 使用 `<< 'EOF'` 阻止 Bash 展开 PHP 变量 | | `$conn->userName` 未转义 | 完全隔离 PHP 内容 | | 用户名写死 `www-data` | 使用占位符 + `sed` 替换 | | SECRET_KEY 不持久 | 写入 `.secret.key` 文件 | --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值