<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>xqcherry站点</title><description>No description</description><link>https://blog.xqcherry.top/</link><language>zh_CN</language><item><title>Cloudflare 525 故障复盘：从回源握手失败到 Tunnel</title><link>https://blog.xqcherry.top/posts/cloudflare-525-postmortem/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/cloudflare-525-postmortem/</guid><description>一次 Cloudflare 525 排查：源站直连正常，但传统回源在 TLS 协商阶段被中断，最终通过 Cloudflare Tunnel 恢复访问。</description><pubDate>Thu, 24 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;博客接入 Cloudflare 后，访问一度稳定返回 &lt;code&gt;525 SSL handshake failed&lt;/code&gt;。最终确认：静态站点、Nginx 和证书都能正常工作，问题出在 Cloudflare 到源站公网 &lt;code&gt;443&lt;/code&gt; 的 TLS 协商链路。&lt;/p&gt;
&lt;h2&gt;现象&lt;/h2&gt;
&lt;p&gt;原始链路如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;浏览器 → Cloudflare → 源站公网 443 → Docker/Nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cloudflare 开启“完全（严格）”SSL 后，站点返回 525；但绕过 Cloudflare、使用 SNI 直连源站时，首页可以正常返回 &lt;code&gt;HTTP 200&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -k -I --resolve example.com:443:&amp;lt;源站 IP&amp;gt; https://example.com/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这说明问题不在静态站点构建、Nginx 基础配置或端口可达性。&lt;/p&gt;
&lt;h2&gt;排查结论&lt;/h2&gt;
&lt;p&gt;服务器抓包表明，Cloudflare 的请求已经到达源站网卡，并转发到 Nginx；Nginx 也正常返回了握手数据。&lt;/p&gt;
&lt;p&gt;握手日志中的关键现象是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Cloudflare ClientHello
→ Nginx ServerHello
→ 对端发送 RST
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TLS 1.3 中，&lt;code&gt;ServerHello&lt;/code&gt; 发生在证书发送之前。因此这次中断不足以证明是证书、私钥或域名校验的问题。&lt;/p&gt;
&lt;p&gt;后续也逐一排除了这些常见原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;源站安全组或 &lt;code&gt;443&lt;/code&gt; 端口未开放；&lt;/li&gt;
&lt;li&gt;Nginx 未正确转发；&lt;/li&gt;
&lt;li&gt;证书过期、私钥不匹配或域名不匹配；&lt;/li&gt;
&lt;li&gt;TLS 1.2 / TLS 1.3 不支持；&lt;/li&gt;
&lt;li&gt;服务器时间漂移；&lt;/li&gt;
&lt;li&gt;在 Cloudflare “完全”与“完全（严格）”之间切换。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;故障边界最终收敛为：&lt;strong&gt;Cloudflare 到源站公网 &lt;code&gt;443&lt;/code&gt; 的链路，在 TLS 参数协商阶段中断。&lt;/strong&gt; 这类问题无法仅靠站点代码或常规 Nginx 配置可靠解决。&lt;/p&gt;
&lt;h2&gt;解决方案：目前还是解决解决不了&lt;/h2&gt;
&lt;p&gt;目前还是不知道问题出在哪&lt;/p&gt;
&lt;p&gt;只能暂时采用别的方案，将公网回源改为 Tunnel：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;浏览器 → Cloudflare → Cloudflare Tunnel
→ cloudflared → Docker 内网 → Nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cloudflared&lt;/code&gt; 与 Nginx 加入同一个 Docker 网络，再在 Cloudflare Zero Trust 中为域名配置 Public Hostname。Tunnel 建立后，由服务器主动向 Cloudflare 发起连接，不再依赖 Cloudflare 访问源站公网 &lt;code&gt;443&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;切换完成后，站点恢复 &lt;code&gt;HTTP 200&lt;/code&gt;。现阶段保持 Tunnel 作为默认链路。&lt;/p&gt;
</content:encoded></item><item><title>LCR 110. 所有可能的路径：DFS 回溯枚举 DAG 路径</title><link>https://blog.xqcherry.top/posts/algorithms/all-paths-source-target/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/algorithms/all-paths-source-target/</guid><description>在有向无环图中维护当前路径，深度优先搜索到终点时收集结果，并在返回时撤销选择。</description><pubDate>Wed, 23 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目要求列出从节点 &lt;code&gt;0&lt;/code&gt; 到节点 &lt;code&gt;n - 1&lt;/code&gt; 的全部路径。图是 DAG，因此沿边递归不会遇到环；每次递归只需维护一条当前路径。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; g, res;
    vector&amp;lt;int&amp;gt; path;
    int n ;

    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; allPathsSourceTarget(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; graph) {

        g = graph;
        n = graph.size();

        path.push_back(0);
        dfs(0);

        return res;
    }

    void dfs(int u) {

        if(u == n - 1) {
            res.push_back(path);
            return ;
        }

        for(auto&amp;amp; v : g[u]) {
            path.push_back(v);
            dfs(v);
            path.pop_back();
        }
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;回溯的关键&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;push_back&lt;/code&gt; 代表选择下一节点，递归返回后的 &lt;code&gt;pop_back&lt;/code&gt; 代表撤销选择。遗漏撤销会让相邻分支共享错误路径。输出本身可能有指数级大小，因此总时间复杂度至少与答案规模成正比。&lt;/p&gt;
</content:encoded></item><item><title>141. 环形链表：哈希集合判圈</title><link>https://blog.xqcherry.top/posts/algorithms/linked-list-cycle-floyd/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/algorithms/linked-list-cycle-floyd/</guid><description>记录已经访问过的节点；再次遇到同一节点时，说明链表存在环。</description><pubDate>Wed, 23 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;遍历链表时，把每个访问过的节点地址加入集合。若当前节点已存在于集合中，说明沿 &lt;code&gt;next&lt;/code&gt; 指针回到了此前位置，链表存在环。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    typedef struct ListNode* PtrtoNode;
    bool hasCycle(ListNode *head) {
        
        set&amp;lt;PtrtoNode&amp;gt; s;

        while(head != NULL) {
            if(s.count(head)) return 1;
            s.insert(head);
            head = head-&amp;gt;next;
        }

        return 0;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没有环时会遍历到 &lt;code&gt;nullptr&lt;/code&gt;；有环时会再次访问某个节点。时间复杂度为 $O(n)$，额外空间为 $O(n)$。&lt;/p&gt;
</content:encoded></item><item><title>76. 最小覆盖子串：滑动窗口的扩张与收缩</title><link>https://blog.xqcherry.top/posts/algorithms/minimum-window-substring/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/algorithms/minimum-window-substring/</guid><description>用字符计数维护窗口是否覆盖目标串；窗口满足条件时尽可能收缩，以得到最短合法子串。</description><pubDate>Mon, 21 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;滑动窗口的核心不是枚举所有区间，而是维护一个不变量：窗口何时已经覆盖 &lt;code&gt;t&lt;/code&gt;。当前实现用 &lt;code&gt;nd&lt;/code&gt; 记录目标字符计数、&lt;code&gt;win&lt;/code&gt; 记录窗口计数，并用 &lt;code&gt;check()&lt;/code&gt; 判断是否满足覆盖条件；右指针负责扩张，左指针负责删除冗余字符并更新最优答案。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include&amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

map&amp;lt;char, int&amp;gt; nd, win;
string s, t;
string res;

int check() {
    for(auto&amp;amp; [c, cnt] : nd) {
        if(win[c] &amp;lt; cnt) return 0;
    }
    return 1;
}

int main() {

    cin &amp;gt;&amp;gt; s &amp;gt;&amp;gt; t;

    int m = s.size();
    int n = t.size();

    for(auto&amp;amp; c : t) nd[c] ++;

    int l = 0, r = 0;
    int resl = 0, mlen = 1e9;

    while(r &amp;lt; m) {
        win[s[r]] ++;

        while(check()) {

            if(r - l + 1 &amp;lt; mlen) {
                mlen = r - l + 1;
                resl = l;
            }

            win[s[l]] --;
            l ++;
        }

        r ++;
    }

    if(mlen == 1e9) res = &quot;&quot;;
    else res = s.substr(resl, mlen);

    cout &amp;lt;&amp;lt; res &amp;lt;&amp;lt; &quot;\n&quot;;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;边界&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;check()&lt;/code&gt; 会逐一确认 &lt;code&gt;nd&lt;/code&gt; 中所有字符的需求量，因此能正确处理 &lt;code&gt;t&lt;/code&gt; 中的重复字符。两个指针都只向右移动；单次检查需要遍历目标字符集合。&lt;/p&gt;
</content:encoded></item><item><title>785. 判断二分图：DFS 染色与冲突检测</title><link>https://blog.xqcherry.top/posts/algorithms/is-graph-bipartite/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/algorithms/is-graph-bipartite/</guid><description>将相邻顶点染成不同颜色；一旦边的两端颜色相同，图就不是二分图。</description><pubDate>Sun, 20 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;二分图的顶点可以分成两个集合，任意边只连接不同集合。把两个集合抽象为 &lt;code&gt;0&lt;/code&gt; 与 &lt;code&gt;1&lt;/code&gt; 两种颜色，就得到 DFS/BFS 染色问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:

    vector&amp;lt;int&amp;gt; color;
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; graphs;

    bool isBipartite(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; graph) {
        int n = graph.size();
        graphs = graph;

        color.assign(n, -1);

        for(int i = 0; i &amp;lt; n; i ++ ) {
            if(color[i] == -1) {
                if(!dfs(i, 0)) return 0;
            }
        }

        return 1;
    }

    int dfs(int u, int c) {
        color[u] = c;

        for(auto&amp;amp; v : graphs[u]) {
            if(color[v] == -1) {
                if(!dfs(v, 1 - c)) return 0;
            }
            else if(color[v] == color[u]) {
                return 0;
            }
        }

        return 1;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;图可能不连通，不能只从 &lt;code&gt;0&lt;/code&gt; 号节点开始。每个未染色节点都应作为一个新连通分量的起点。时间复杂度为 $O(V+E)$，空间复杂度为 $O(V)$。&lt;/p&gt;
</content:encoded></item><item><title>315. 计算右侧小于当前元素的个数：离散化与树状数组</title><link>https://blog.xqcherry.top/posts/algorithms/count-smaller-fenwick-tree/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/algorithms/count-smaller-fenwick-tree/</guid><description>从右向左扫描数组，用离散化压缩值域，并通过树状数组查询右侧较小元素的数量。</description><pubDate>Sun, 20 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;处理 &lt;code&gt;nums[i]&lt;/code&gt; 时，右侧元素已经加入数据结构；问题变成查询“已加入元素中有多少值小于 &lt;code&gt;nums[i]&lt;/code&gt;”。值域可能很大或包含负数，所以先离散化为连续排名。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include&amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

const int N = 1e5 + 10;

int n;
int a[N], tree[N];

int lowbit(int x) {
    return x &amp;amp; -x;
}

void add(int x, int v) {
    while(x &amp;lt;= n) {
        tree[x] += v;
        x += lowbit(x);
    }
}

int query(int x) {
    int res = 0;

    while(x) {
        res += tree[x];
        x -= lowbit(x);
    }

    return res;
}

int main() {

    cin &amp;gt;&amp;gt; n;
    vector&amp;lt;int&amp;gt; alls, res;

    for(int i = 0; i &amp;lt; n; i ++ ) {
        cin &amp;gt;&amp;gt; a[i];
        alls.push_back(a[i]);
    }
        
    sort(alls.begin(), alls.end());
    alls.erase(unique(alls.begin(), alls.end()), alls.end());

    for(int i = n - 1; i &amp;gt;= 0; i -- ) {
        int rk = lower_bound(alls.begin(), alls.end(), a[i]) - alls.begin() + 1;

        res.push_back(query(rk - 1));
        add(rk, 1);
    }

    reverse(res.begin(), res.end());

    for(auto&amp;amp; x : res) cout &amp;lt;&amp;lt; x &amp;lt;&amp;lt; &quot; &quot;;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;树状数组的 &lt;code&gt;query(rank - 1)&lt;/code&gt; 正好排除与当前值相等的元素，符合“严格小于”的要求。排序离散化与每次查询/更新的总复杂度均为 $O(n\log n)$。&lt;/p&gt;
</content:encoded></item><item><title>罗马数字转整数：识别特殊减法对</title><link>https://blog.xqcherry.top/posts/algorithms/roman-to-integer/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/algorithms/roman-to-integer/</guid><description>将基础符号和六种特殊减法组合分别映射为数值，通过线性扫描完成转换。</description><pubDate>Sun, 20 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;罗马数字通常从左到右累加；&lt;code&gt;IV&lt;/code&gt;、&lt;code&gt;IX&lt;/code&gt;、&lt;code&gt;XL&lt;/code&gt;、&lt;code&gt;XC&lt;/code&gt;、&lt;code&gt;CD&lt;/code&gt;、&lt;code&gt;CM&lt;/code&gt; 是需要整体处理的减法组合。代码用一个单字符映射和一个双字符映射分别处理它们。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int romanToInt(string s) {
        map&amp;lt;char, int&amp;gt; m = {
            {&apos;I&apos;, 1}, {&apos;V&apos;, 5}, {&apos;X&apos;, 10}, {&apos;L&apos;, 50},
            {&apos;C&apos;, 100}, {&apos;D&apos;, 500}, {&apos;M&apos;, 1000}
        };

        map&amp;lt;string, int&amp;gt; n = {
            {&quot;IV&quot;, 4}, {&quot;IX&quot;, 9},
            {&quot;XL&quot;, 40}, {&quot;XC&quot;, 90},
            {&quot;CD&quot;, 400}, {&quot;CM&quot;, 900}
        };

        int res = 0;

        for(int i = 0; i &amp;lt; s.size(); i ++ ) {
            if(i + 1 &amp;lt; s.size()) {
                string t = s.substr(i, 2);
                if(n.count(t)) {
                    res += n[t];
                    i ++;
                    continue;
                }
            }
            res += m[s[i]];
        }

        return res;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：$O(n)$。&lt;/li&gt;
&lt;li&gt;空间复杂度：$O(1)$；符号映射大小固定。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note[判断条件]
不必逐一枚举 &lt;code&gt;IV&lt;/code&gt;、&lt;code&gt;IX&lt;/code&gt; 等减法组合。比较当前字符与下一个字符，就能统一覆盖全部合法减法对。
:::&lt;/p&gt;
</content:encoded></item><item><title>三数之和：排序加双指针去重</title><link>https://blog.xqcherry.top/posts/algorithms/three-sum-two-pointers/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/algorithms/three-sum-two-pointers/</guid><description>固定第一个数后，用相向双指针寻找剩余两数，并在三个层次处理重复值。</description><pubDate>Sun, 20 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;目标是在数组中找到所有和为 &lt;code&gt;0&lt;/code&gt; 且不重复的三元组。暴力枚举需要 $O(n^3)$；排序后固定一个数，可将后两数搜索降为线性。&lt;/p&gt;
&lt;h2&gt;思路&lt;/h2&gt;
&lt;p&gt;排序后枚举 &lt;code&gt;i&lt;/code&gt;。在区间 &lt;code&gt;[i + 1, n - 1]&lt;/code&gt; 中维护 &lt;code&gt;left&lt;/code&gt;、&lt;code&gt;right&lt;/code&gt;：和太小就右移 &lt;code&gt;left&lt;/code&gt;，和太大就左移 &lt;code&gt;right&lt;/code&gt;，恰好为零时记录答案并跳过重复值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; threeSum(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
        int n = nums.size();
        vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; res;

        if(n == 3) {
            int x = nums[0] + nums[1] + nums[2];
            if(!x) {
                res.push_back(nums);
                return res;
            }
            else {};
        }

        sort(nums.begin(), nums.end());
        for(int i = 0; i &amp;lt; n - 2; i ++ ) {
            if(i &amp;gt; 0 &amp;amp;&amp;amp; nums[i] == nums[i - 1]) continue;
            int x = -nums[i];

            int l = i + 1, r = n - 1;
            while(l &amp;lt; r) {
                int t = nums[l] + nums[r];

                if(t == x) {
                    res.push_back({-x, nums[l], nums[r]});
                    while(l &amp;lt; r &amp;amp;&amp;amp; nums[l] == nums[l + 1]) l ++;
                    while(l &amp;lt; r &amp;amp;&amp;amp; nums[r] == nums[r - 1]) r --;
                    l ++, r --;
                } 
                else if(t &amp;gt; x) r --;
                else l ++;
            }
        }

        return res;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;易错点&lt;/h2&gt;
&lt;p&gt;去重不只发生在 &lt;code&gt;i&lt;/code&gt;：找到答案后，左右指针也必须越过相同元素，否则会重复收集同一三元组。时间复杂度为 $O(n^2)$，排序以外只使用常数额外空间。&lt;/p&gt;
</content:encoded></item><item><title>42. 接雨水：双指针维护两侧最高柱</title><link>https://blog.xqcherry.top/posts/algorithms/trapping-rain-water-two-pointers/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/algorithms/trapping-rain-water-two-pointers/</guid><description>从两侧向中间收缩，利用较低一侧的最高柱确定当前位置的积水量，在线性时间内求解接雨水。</description><pubDate>Sun, 20 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;第 &lt;code&gt;i&lt;/code&gt; 个位置能接的雨水由左右最高柱中较低的一侧决定：$\max(0, \min(L_i, R_i) - h_i)$。预处理两组最高值可解题，但会消耗 $O(n)$ 空间。&lt;/p&gt;
&lt;h2&gt;双指针不变量&lt;/h2&gt;
&lt;p&gt;使用 &lt;code&gt;left&lt;/code&gt;、&lt;code&gt;right&lt;/code&gt; 从两端收缩，分别维护 &lt;code&gt;leftMax&lt;/code&gt; 和 &lt;code&gt;rightMax&lt;/code&gt;。若 &lt;code&gt;leftMax &amp;lt;= rightMax&lt;/code&gt;，当前位置左侧的上界已经确定，右侧必然至少有 &lt;code&gt;rightMax&lt;/code&gt;，所以可以立即结算 &lt;code&gt;left&lt;/code&gt;；反之处理 &lt;code&gt;right&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int trap(vector&amp;lt;int&amp;gt;&amp;amp; height) {
        int n = height.size();

        int l = 0, r = n - 1;
        int lmax = 0, rmax = 0;

        int res = 0;

        while(l &amp;lt; r) {
            lmax = max(lmax, height[l]);
            rmax = max(rmax, height[r]);

            if(lmax &amp;lt; rmax) {
                res += lmax - height[l];
                l ++;
            }
            else {
                res += rmax - height[r];
                r --;
            }
        }

        return res;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度为 $O(n)$，额外空间为 $O(1)$。空数组或只有一个柱子时循环不会执行，答案自然为零。&lt;/p&gt;
</content:encoded></item><item><title>两数之和：哈希表查找补数</title><link>https://blog.xqcherry.top/posts/algorithms/two-sum-hash-table/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/algorithms/two-sum-hash-table/</guid><description>先用哈希表记录数值与下标，再逐个查询目标补数，将两数之和从枚举所有组合优化到线性时间。</description><pubDate>Sun, 20 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;题目要求在数组中找到两个不同下标，使其元素之和等于 &lt;code&gt;target&lt;/code&gt;。关键是把“寻找另一个数”转换为“查询补数”。&lt;/p&gt;
&lt;h2&gt;思路&lt;/h2&gt;
&lt;p&gt;先把每个数值与其下标写入映射；随后遍历数组，查询补数 &lt;code&gt;target - nums[i]&lt;/code&gt; 是否存在，且其下标不能与当前下标相同。&lt;/p&gt;
&lt;p&gt;:::tip[当前代码的处理方式]
这份实现采用“先建表、后查询”的两次遍历；&lt;code&gt;m[k] != i&lt;/code&gt; 用于排除同一个元素被重复使用的情况。
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    vector&amp;lt;int&amp;gt; twoSum(vector&amp;lt;int&amp;gt;&amp;amp; nums, int target) {
        int n = nums.size();

        map&amp;lt;int, int&amp;gt; m;
        vector&amp;lt;int&amp;gt; ans;

        for(int i = 0; i &amp;lt; n; i ++ ) m[nums[i]] = i;

        for(int i = 0; i &amp;lt; n; i ++ ) {
            auto x = nums[i];
            int k = target - x;
            if(m.count(k) &amp;amp;&amp;amp; m[k] != i) {
                ans.push_back(i);
                ans.push_back(m[k]);
                break;
            }
        }

        return ans;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复杂度与边界&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：$O(n)$，每次哈希查询均摊为常数时间。&lt;/li&gt;
&lt;li&gt;空间复杂度：$O(n)$。&lt;/li&gt;
&lt;li&gt;数组存在重复元素时仍然有效；题目保证唯一答案时可直接返回。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>1871. 跳跃游戏 VII：用滑动窗口优化可达性 DP</title><link>https://blog.xqcherry.top/posts/algorithms/jump-game-vii-sliding-window/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/algorithms/jump-game-vii-sliding-window/</guid><description>将每个位置的可达性转化为前一段区间内是否存在可达位置，并用滑动窗口把二次 DP 优化为线性。</description><pubDate>Sat, 19 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;令 &lt;code&gt;dp[i]&lt;/code&gt; 表示能否到达位置 &lt;code&gt;i&lt;/code&gt;。当 &lt;code&gt;s[i]&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt; 时，若区间 &lt;code&gt;[i - maxJump, i - minJump]&lt;/code&gt; 中任意位置可达，则 &lt;code&gt;dp[i]&lt;/code&gt; 为真。&lt;/p&gt;
&lt;h2&gt;暴力 DP&lt;/h2&gt;
&lt;p&gt;以下是原始的区间枚举版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    bool canReach(string s, int minJump, int maxJump) {
        int n = s.size();
        vector&amp;lt;int&amp;gt; f(n, 0);

        f[0] = 1;

        for(int i = 0; i &amp;lt; n; i ++ ) {
            if(!f[i]) continue;

            for(int j = i + minJump; j &amp;lt;= min(n - 1, i + maxJump); j ++ ) {
                if(s[j] == &apos;0&apos;) f[j] = 1;
            }
        }

        return f[n - 1];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接检查整个区间需要 $O(n^2)$。观察到该区间随 &lt;code&gt;i&lt;/code&gt; 右移，维护窗口内可达位置数 &lt;code&gt;count&lt;/code&gt; 即可。&lt;/p&gt;
&lt;h2&gt;滑动窗口优化&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    bool canReach(string s, int minJump, int maxJump) {
        int n = s.size();

        vector&amp;lt;int&amp;gt; f(n, 0);
        f[0] = 1;

        int cnt = 0;
        for(int i = 1; i &amp;lt; n; i ++ ) {

            int r = i - minJump;
            if(r &amp;gt;= 0 &amp;amp;&amp;amp; f[r]) cnt ++;

            int l = i - maxJump - 1;
            if(l &amp;gt;= 0 &amp;amp;&amp;amp; f[l]) cnt --;

            if(s[i] == &apos;0&apos; &amp;amp;&amp;amp; cnt) f[i] = 1;
        }

        return f[n - 1];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;enter&lt;/code&gt; 是新进入可跳区间的位置，&lt;code&gt;leave&lt;/code&gt; 是刚离开区间的位置；两者的下标是最容易写错的部分。时间复杂度 $O(n)$，空间复杂度 $O(n)$。&lt;/p&gt;
</content:encoded></item><item><title>52. N 皇后 II：回溯与方向检查</title><link>https://blog.xqcherry.top/posts/algorithms/n-queens-ii-backtracking/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/algorithms/n-queens-ii-backtracking/</guid><description>逐行放置皇后，在已有棋盘中检查同列和两条对角线是否冲突，统计所有合法布局。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;每一行只能放一个皇后，因此递归的第 &lt;code&gt;row&lt;/code&gt; 层只需尝试该行的各列。冲突只可能来自同列、主对角线或副对角线。&lt;/p&gt;
&lt;h2&gt;状态设计&lt;/h2&gt;
&lt;p&gt;由于按行递归，当前行之下尚未放置皇后。检查位置 &lt;code&gt;(r, c)&lt;/code&gt; 时，只需向上检查同列、左上对角线和右上对角线。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include&amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

const int N = 10;

int n, res;
int g[N][N];

int check(int r, int c) {

    for(int i = r - 1; i &amp;gt;= 0; i --) {
        if(g[i][c] == 1) return 0;
    }

    for(int i = r - 1, j = c - 1; i &amp;gt;= 0 &amp;amp;&amp;amp; j &amp;gt;= 0; i --, j --) {
        if(g[i][j] == 1) return 0;
    }

    for(int i = r - 1, j = c + 1; i &amp;gt;= 0 &amp;amp;&amp;amp; j &amp;lt; n; i --, j ++) {
        if(g[i][j] == 1) return 0;
    }

    return 1;
}

void dfs(int r) {

    if(r == n) {
        res ++;
        return ;
    } 

    for(int c = 0; c &amp;lt; n; c ++ ) {
        if(check(r, c)) {
            g[r][c] = 1;
            dfs(r + 1);
            g[r][c] = -1;
        }
    }
}

int main() {

    cin &amp;gt;&amp;gt; n;

    for(int i = 0; i &amp;lt; n; i ++)
        for(int j = 0; j &amp;lt; n; j ++)
            g[i][j] = -1;

    dfs(0);

    cout &amp;lt;&amp;lt; res &amp;lt;&amp;lt; &quot;\n&quot;;

    return res;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;回溯最重要的是撤销状态；忘记恢复任一数组都会污染下一条搜索分支。搜索树最坏规模为 $O(n!)$，剪枝显著减少实际分支。&lt;/p&gt;
</content:encoded></item><item><title>DES 与 Feistel 网络：理解经典分组密码的轮函数设计</title><link>https://blog.xqcherry.top/posts/cryptography/des-feistel-network/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/cryptography/des-feistel-network/</guid><description>以 DES 为例梳理 Feistel 网络、16 轮处理和密钥长度限制，并说明它为何只适合作为学习材料。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;DES（Data Encryption Standard）是研究分组密码结构的重要案例，但其 56 位有效密钥已无法抵抗现代穷举攻击，因此不应再用于新系统。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/cryptography/des-overview.png&quot; alt=&quot;DES 整体流程&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;参数速览&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;分组长度&lt;/td&gt;
&lt;td&gt;64 bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;标称密钥长度&lt;/td&gt;
&lt;td&gt;64 bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;有效密钥长度&lt;/td&gt;
&lt;td&gt;56 bit（其余为校验位）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;网络结构&lt;/td&gt;
&lt;td&gt;Feistel 网络&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;轮数&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;每轮子密钥&lt;/td&gt;
&lt;td&gt;48 bit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;DES 的处理过程&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;64 bit 明文
  → 初始置换 IP
  → 分为 L₀、R₀（各 32 bit）
  → 16 轮 Feistel 变换
  → 合并并执行逆初始置换 IP⁻¹
  → 64 bit 密文
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第 $i$ 轮可抽象为：&lt;/p&gt;
&lt;p&gt;$$L_i=R_{i-1}, \qquad R_i=L_{i-1}\oplus F(R_{i-1}, K_i)$$&lt;/p&gt;
&lt;p&gt;轮函数 $F$ 将右半部分与子密钥混合，经扩展、S 盒非线性代换和置换后输出 32 位结果。多轮迭代使局部差异逐步扩散到整个分组。&lt;/p&gt;
&lt;h2&gt;为什么 Feistel 结构重要&lt;/h2&gt;
&lt;p&gt;即使轮函数 $F$ 本身不可逆，整个 Feistel 网络仍可逆：解密时按相反顺序使用子密钥即可。这使结构设计更灵活，也影响了后续许多密码算法。&lt;/p&gt;
&lt;p&gt;:::caution[安全现状]
DES 已废弃；3DES 也正逐步退出。新系统应选择 AES-GCM 或 ChaCha20-Poly1305 等现代认证加密方案。
:::&lt;/p&gt;
</content:encoded></item><item><title>分组密码：混淆、扩散与 Feistel / SPN 结构</title><link>https://blog.xqcherry.top/posts/cryptography/block-ciphers/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/cryptography/block-ciphers/</guid><description>从混淆和扩散两项设计目标出发，理解分组密码的基本工作方式，以及 Feistel 和 SPN 两类典型结构。</description><pubDate>Thu, 17 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;分组密码以固定长度的数据块为单位进行变换，是对称密码的核心构件之一。DES 和 AES 都属于分组密码，但采用了不同的内部结构。&lt;/p&gt;
&lt;h2&gt;两个设计目标&lt;/h2&gt;
&lt;h3&gt;混淆（Confusion）&lt;/h3&gt;
&lt;p&gt;混淆让密钥、明文和密文之间的关系尽量复杂，降低攻击者从输入输出推导密钥的能力。典型组件是 &lt;strong&gt;S 盒&lt;/strong&gt;：通过非线性查表引入非线性。&lt;/p&gt;
&lt;h3&gt;扩散（Diffusion）&lt;/h3&gt;
&lt;p&gt;扩散让一个明文比特或密钥比特的变化影响更多输出位，形成雪崩效应。置换、线性变换和列混合等操作都服务于扩散。&lt;/p&gt;
&lt;p&gt;:::tip[理解方式]
可以把混淆理解为“关系难以预测”，把扩散理解为“影响尽可能传播开”。安全的分组密码需要两者多轮配合。
:::&lt;/p&gt;
&lt;h2&gt;两类经典结构&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;结构&lt;/th&gt;
&lt;th&gt;核心思路&lt;/th&gt;
&lt;th&gt;代表算法&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Feistel 网络&lt;/td&gt;
&lt;td&gt;分组左右拆分，轮函数只作用于一侧后再交换&lt;/td&gt;
&lt;td&gt;DES&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPN（代换—置换网络）&lt;/td&gt;
&lt;td&gt;多轮 S 盒代换与线性置换&lt;/td&gt;
&lt;td&gt;AES&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Feistel 的一个优点是加密和解密可以复用相近的结构，仅需调整子密钥顺序；SPN 则通过多轮非线性代换与扩散层累积安全性。&lt;/p&gt;
&lt;h2&gt;使用时的边界&lt;/h2&gt;
&lt;p&gt;分组密码描述的是底层原语，而不是完整安全方案。实际加密还需要安全的工作模式、随机数或 nonce，以及完整性保护。应使用 AES-GCM 等认证加密模式，而不要手动拼接加密流程。&lt;/p&gt;
</content:encoded></item><item><title>从置换到代换：古典密码的两种基本思路</title><link>https://blog.xqcherry.top/posts/cryptography/classical-ciphers/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/cryptography/classical-ciphers/</guid><description>通过密码棒、栅栏、凯撒、维吉尼亚和 Playfair 密码，理解密码设计中置换与代换两条基础路线。</description><pubDate>Thu, 17 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;古典密码的实现各不相同，但可以归结为两种操作：&lt;strong&gt;置换&lt;/strong&gt;改变符号的位置，&lt;strong&gt;代换&lt;/strong&gt;改变符号本身。现代分组密码中的扩散与混淆，也能看到这两种思想的影子。&lt;/p&gt;
&lt;p&gt;:::note[阅读目标]
这篇内容用于建立直觉，而非推荐实际使用这些算法。古典密码的密钥空间小、统计特征明显，不能用于现代安全通信。
:::&lt;/p&gt;
&lt;h2&gt;两条设计路线&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;改变的对象&lt;/th&gt;
&lt;th&gt;典型算法&lt;/th&gt;
&lt;th&gt;直观效果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;置换&lt;/td&gt;
&lt;td&gt;字符的位置&lt;/td&gt;
&lt;td&gt;密码棒、栅栏密码&lt;/td&gt;
&lt;td&gt;字符没变，顺序被打乱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;代换&lt;/td&gt;
&lt;td&gt;字符本身&lt;/td&gt;
&lt;td&gt;凯撒、维吉尼亚、Playfair&lt;/td&gt;
&lt;td&gt;顺序不变，符号被替换&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;置换：重新排列已有字符&lt;/h2&gt;
&lt;h3&gt;斯巴达密码棒&lt;/h3&gt;
&lt;p&gt;发送者把纸带缠在固定粗细的圆柱上，沿圆柱逐行写入明文，再沿纸带读出密文。接收者只有使用相同粗细的密码棒，才能恢复正确的排列顺序；因此密码棒的尺寸就是密钥。&lt;/p&gt;
&lt;h3&gt;栅栏密码&lt;/h3&gt;
&lt;p&gt;以两行为例，把 &lt;code&gt;HELLOWORLD&lt;/code&gt; 按“之”字形写入，再逐行读出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;H   L   O   O   L
  E   L   W   R   D

密文：HLOOLELWRD
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;栅栏行数（或深度）是密钥。行数有限且字符频率没有被改变，因此它很容易被分析破解。&lt;/p&gt;
&lt;h2&gt;代换：用另一个符号替换原符号&lt;/h2&gt;
&lt;h3&gt;凯撒密码&lt;/h3&gt;
&lt;p&gt;把每个字母在字母表中向后移动固定距离；例如偏移 &lt;code&gt;+3&lt;/code&gt; 时，&lt;code&gt;HELLO&lt;/code&gt; 变为 &lt;code&gt;KHOOR&lt;/code&gt;。密钥就是移动位数。&lt;/p&gt;
&lt;h3&gt;维吉尼亚密码&lt;/h3&gt;
&lt;p&gt;维吉尼亚密码使用重复的关键字提供多个偏移量。对 &lt;code&gt;HELLOWORLD&lt;/code&gt; 使用 &lt;code&gt;KEYKEYKEYK&lt;/code&gt;，每个位置分别按对应字母的偏移量移动，可得到 &lt;code&gt;RIJVSUYVJN&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;相比凯撒密码，它不再是单表代换；但重复密钥会留下周期性，仍可借助频率分析破解。&lt;/p&gt;
&lt;h3&gt;Playfair 密码&lt;/h3&gt;
&lt;p&gt;Playfair 使用密钥构造 &lt;code&gt;5×5&lt;/code&gt; 字母方阵（通常合并 &lt;code&gt;I/J&lt;/code&gt;），每两个字符作为一组替换，因此属于&lt;strong&gt;多字母组代换&lt;/strong&gt;。它比逐字符代换更难直接做频率分析，但并不具备现代密码学意义上的安全性。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;置换负责“打乱位置”，代换负责“隐藏符号”。现代密码通常会多轮组合非线性代换和线性置换，而不是单独依赖其中一种操作。&lt;/p&gt;
</content:encoded></item><item><title>密码学入门：安全目标、基本模型与攻击能力</title><link>https://blog.xqcherry.top/posts/cryptography/foundations/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/cryptography/foundations/</guid><description>从机密性、完整性、认证和不可否认性出发，梳理密码系统的基本模型、攻击类型与对称/非对称密码的分工。</description><pubDate>Thu, 17 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;密码学关注的不是“把信息变得看不懂”，而是在不可信的环境中提供可验证的安全属性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/cryptography/security-services.png&quot; alt=&quot;密码学安全服务&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;四项常见安全目标&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;目标&lt;/th&gt;
&lt;th&gt;要解决的问题&lt;/th&gt;
&lt;th&gt;常见技术&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;机密性&lt;/td&gt;
&lt;td&gt;未授权者不能读懂内容&lt;/td&gt;
&lt;td&gt;对称加密、公钥加密&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;完整性&lt;/td&gt;
&lt;td&gt;内容没有被修改&lt;/td&gt;
&lt;td&gt;哈希、MAC、数字签名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;认证性&lt;/td&gt;
&lt;td&gt;能确认消息来源&lt;/td&gt;
&lt;td&gt;MAC、数字签名、证书&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;不可否认性&lt;/td&gt;
&lt;td&gt;发送者不能事后否认&lt;/td&gt;
&lt;td&gt;数字签名&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::important[不要混淆概念]
加密主要解决机密性；哈希本身不提供身份认证；数字签名通常同时服务于完整性、认证和不可否认性。
:::&lt;/p&gt;
&lt;h2&gt;一个密码系统如何工作&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/cryptography/crypto-model.png&quot; alt=&quot;基本通信模型&quot; /&gt;&lt;/p&gt;
&lt;p&gt;设明文为 $m$、密文为 $c$、加密密钥为 $k_e$、解密密钥为 $k_d$：&lt;/p&gt;
&lt;p&gt;$$c = E(k_e, m), \qquad m = D(k_d, c)$$&lt;/p&gt;
&lt;p&gt;发送者 Alice 将密文通过不安全信道交给 Bob；窃听者 Eve 可以截获密文，但不应从中恢复明文。现代密码学遵循 Kerckhoffs 原则：&lt;strong&gt;安全性应依赖密钥保密，而不是算法保密&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;攻击者可能掌握什么&lt;/h2&gt;
&lt;p&gt;攻击模型按攻击者能力由弱到强排列：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;唯密文攻击&lt;/strong&gt;：只获得密文样本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;已知明文攻击&lt;/strong&gt;：掌握部分明文及其对应密文。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择明文攻击&lt;/strong&gt;：可以选择输入并获取加密结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择密文攻击&lt;/strong&gt;：可以选择密文并获得解密反馈。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;现代密码算法通常以能够抵抗选择明文攻击为基本要求；具体场景还会要求抵抗更强的选择密文攻击。&lt;/p&gt;
&lt;h2&gt;两类密码体制&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;体制&lt;/th&gt;
&lt;th&gt;密钥关系&lt;/th&gt;
&lt;th&gt;优势&lt;/th&gt;
&lt;th&gt;典型用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;对称密码&lt;/td&gt;
&lt;td&gt;加解密使用同一密钥或可互推密钥&lt;/td&gt;
&lt;td&gt;速度快&lt;/td&gt;
&lt;td&gt;大量数据加密&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;非对称密码&lt;/td&gt;
&lt;td&gt;公钥与私钥不同&lt;/td&gt;
&lt;td&gt;易于分发公钥&lt;/td&gt;
&lt;td&gt;密钥交换、签名、身份认证&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;实际系统通常采用混合方案：先用非对称机制协商会话密钥，再用对称密码加密业务数据。&lt;/p&gt;
</content:encoded></item><item><title>伪随机生成器与流密码：用短密钥保护长消息</title><link>https://blog.xqcherry.top/posts/cryptography/prg-and-stream-ciphers/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/cryptography/prg-and-stream-ciphers/</guid><description>理解 PRG 如何将短种子扩展为密钥流，以及流密码与一次一密在安全模型和使用约束上的差异。</description><pubDate>Thu, 17 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;一次一密要求密钥与消息等长，现实系统难以承担这种分发成本。伪随机生成器（PRG）提供了另一条路线：由短种子确定性地产生很长、计算上难以区分于随机数的序列。&lt;/p&gt;
&lt;h2&gt;PRG 的形式&lt;/h2&gt;
&lt;p&gt;$$G: {0,1}^{s} \rightarrow {0,1}^{n}, \qquad s \ll n$$&lt;/p&gt;
&lt;p&gt;其中 $s$ 是种子长度、$n$ 是输出长度。相同种子必然产生相同输出；它的安全要求不是“真正随机”，而是有效攻击者无法在计算上将输出与真正随机序列区分开。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;短种子 → PRG → 长伪随机密钥流
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;流密码如何加密&lt;/h2&gt;
&lt;p&gt;流密码将 PRG 生成的密钥流与明文逐位或逐字节异或：&lt;/p&gt;
&lt;p&gt;$$c = m \oplus \operatorname{keystream}$$&lt;/p&gt;
&lt;p&gt;解密使用同一密钥流再次异或即可恢复明文。它的结构与 OTP 相似，核心差异在于密钥流来自短密钥和算法，而非等长真随机密钥。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;一次一密&lt;/th&gt;
&lt;th&gt;流密码&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;密钥材料&lt;/td&gt;
&lt;td&gt;真随机、与消息等长&lt;/td&gt;
&lt;td&gt;短密钥与 nonce/IV&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;安全基础&lt;/td&gt;
&lt;td&gt;信息论安全&lt;/td&gt;
&lt;td&gt;计算安全&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;可扩展性&lt;/td&gt;
&lt;td&gt;很差&lt;/td&gt;
&lt;td&gt;很好&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::warning[密钥流不能复用]
同一密钥和 nonce/IV 组合绝不能加密两条消息。复用后会产生与 OTP 密钥复用相同的问题：攻击者可通过两个密文的异或得到两个明文的异或。
:::&lt;/p&gt;
&lt;h2&gt;实践建议&lt;/h2&gt;
&lt;p&gt;不要自行组合“PRG + XOR”实现加密。工程中优先使用带认证的成熟方案，例如 ChaCha20-Poly1305 或 AES-GCM，并由可靠库负责 nonce、标签验证与密钥管理。&lt;/p&gt;
</content:encoded></item><item><title>一次一密与完善保密：理论上最安全的加密方案</title><link>https://blog.xqcherry.top/posts/cryptography/perfect-secrecy-and-otp/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/cryptography/perfect-secrecy-and-otp/</guid><description>从异或运算出发，理解一次一密为何能够达到完善保密，以及它为什么难以在通用系统中落地。</description><pubDate>Thu, 17 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;一次一密（One-Time Pad，OTP）展示了密码学中一个重要结论：只要密钥满足严格条件，密文可以不泄露任何关于明文的信息。&lt;/p&gt;
&lt;h2&gt;从异或开始&lt;/h2&gt;
&lt;p&gt;异或满足 $x \oplus x = 0$，因此：&lt;/p&gt;
&lt;p&gt;$$ (m \oplus k) \oplus k = m $$&lt;/p&gt;
&lt;p&gt;OTP 使用同一条密钥流进行加密与解密：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;加密：c = m ⊕ k
解密：m = c ⊕ k
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如，&lt;code&gt;10110010 ⊕ 01101101 = 11011111&lt;/code&gt;；将结果再次与相同密钥异或，即可恢复原文。&lt;/p&gt;
&lt;h2&gt;达到完善保密的三个条件&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;密钥必须真正随机。&lt;/li&gt;
&lt;li&gt;密钥长度不能短于明文。&lt;/li&gt;
&lt;li&gt;每个密钥只能使用一次。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::caution[重复使用密钥会失效]
若 $c_1=m_1\oplus k$、$c_2=m_2\oplus k$，则 $c_1\oplus c_2=m_1\oplus m_2$。密钥被抵消，两个明文之间的关系暴露给攻击者。
:::&lt;/p&gt;
&lt;h2&gt;什么是完善保密&lt;/h2&gt;
&lt;p&gt;Shannon 对完善保密的表达是：&lt;/p&gt;
&lt;p&gt;$$P(M \mid C) = P(M)$$&lt;/p&gt;
&lt;p&gt;即攻击者观察到密文 $C$ 后，对明文 $M$ 的判断没有获得额外信息。对任意给定的 $m$ 与 $c$，OTP 都存在唯一的 $k=m\oplus c$；只要所有等长密钥等概率出现，任何候选明文都同样可能。&lt;/p&gt;
&lt;h2&gt;理论与现实的差距&lt;/h2&gt;
&lt;p&gt;OTP 的问题是密钥分发：加密 100 GB 数据，就需要预先安全地共享 100 GB 真随机且从不复用的密钥。这使它只适合少数具备专用密钥分发能力的场景。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;一次一密&lt;/th&gt;
&lt;th&gt;现代流密码&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;密钥来源&lt;/td&gt;
&lt;td&gt;等长真随机密钥&lt;/td&gt;
&lt;td&gt;短密钥生成伪随机密钥流&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;安全性质&lt;/td&gt;
&lt;td&gt;信息论安全&lt;/td&gt;
&lt;td&gt;计算安全&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;实际可用性&lt;/td&gt;
&lt;td&gt;密钥管理成本极高&lt;/td&gt;
&lt;td&gt;适合通用系统&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;OTP 是理解安全上界的好模型；现实中更常见的选择是经过验证的现代流密码或认证加密方案。&lt;/p&gt;
</content:encoded></item><item><title>区块链入门：数据可信、分布式共识与系统分层</title><link>https://blog.xqcherry.top/posts/blockchain/blockchain-fundamentals/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/blockchain/blockchain-fundamentals/</guid><description>从区块链的核心定义、部署形态和六层架构出发，理解哈希、P2P 网络、共识和智能合约如何共同构成分布式账本。</description><pubDate>Thu, 10 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;区块链可以理解为一种由多个参与者共同维护的、按时间顺序追加的数据账本。它把数据结构、密码学、点对点网络与共识规则组合在一起，使参与者即使不完全互信，也能对账本状态达成一致。&lt;/p&gt;
&lt;p&gt;:::note[先澄清一个常见误解]
“不可篡改”不等于数据绝对无法修改。更准确地说，篡改已确认记录需要重写后续链条，并取得网络规则所要求的控制权，因此成本高、容易被发现；具体安全性取决于链的设计与参与者分布。
:::&lt;/p&gt;
&lt;h2&gt;区块链提供了什么&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;th&gt;依赖的机制&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;共享账本&lt;/td&gt;
&lt;td&gt;多个节点保存并验证同一份状态&lt;/td&gt;
&lt;td&gt;分布式复制、P2P 网络&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;可验证历史&lt;/td&gt;
&lt;td&gt;记录按顺序关联，便于追溯来源&lt;/td&gt;
&lt;td&gt;区块结构、哈希链、Merkle Tree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;一致性&lt;/td&gt;
&lt;td&gt;节点对有效交易和区块顺序达成一致&lt;/td&gt;
&lt;td&gt;共识机制、验证规则&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;可编程规则&lt;/td&gt;
&lt;td&gt;将部分业务逻辑部署为链上代码&lt;/td&gt;
&lt;td&gt;智能合约、虚拟机&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;密码学在其中承担的是身份认证、完整性校验与所有权证明；它并不自动解决所有信任、隐私、性能和治理问题。&lt;/p&gt;
&lt;h2&gt;四种常见部署形态&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;形态&lt;/th&gt;
&lt;th&gt;参与和写入权限&lt;/th&gt;
&lt;th&gt;常见适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;公有链&lt;/td&gt;
&lt;td&gt;原则上任何人都可参与验证或使用&lt;/td&gt;
&lt;td&gt;开放式数字资产、公共应用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;私有链&lt;/td&gt;
&lt;td&gt;由单一组织控制参与者&lt;/td&gt;
&lt;td&gt;企业内部协作与审计&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;联盟链&lt;/td&gt;
&lt;td&gt;多个预先许可的组织共同维护&lt;/td&gt;
&lt;td&gt;跨机构供应链、金融协作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;混合链&lt;/td&gt;
&lt;td&gt;公开与受限数据/权限结合&lt;/td&gt;
&lt;td&gt;需要公开验证又需保护业务数据的系统&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;选择部署形态的关键不是“是否使用区块链”，而是参与方之间的信任关系、性能要求、数据合规性及治理方式。&lt;/p&gt;
&lt;h2&gt;六层架构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/blockchain/layers.png&quot; alt=&quot;区块链系统的六层架构&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数据层&lt;/strong&gt;：定义区块、交易、哈希、数字签名与 Merkle Tree 等基础数据结构。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络层&lt;/strong&gt;：通过 P2P 网络传播交易、区块和节点发现信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;共识层&lt;/strong&gt;：让节点依据统一规则决定哪些记录有效、以何种顺序写入账本；PoW 与 PoS 是两类常见思路。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;激励层&lt;/strong&gt;：通过手续费、区块奖励或质押惩罚等机制，鼓励参与者按协议行为。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;合约层&lt;/strong&gt;：提供智能合约和执行环境，把可验证的业务规则写入链上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应用层&lt;/strong&gt;：面向用户提供钱包、资产、供应链溯源、身份和其他具体产品。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以把这套结构记为：数据层决定“记录什么”，网络层决定“如何传播”，共识层决定“以谁为准”，合约层决定“规则怎样执行”，应用层决定“用户如何使用”。&lt;/p&gt;
&lt;h2&gt;进一步学习路径&lt;/h2&gt;
&lt;p&gt;建议按“密码学基础 → 哈希与数字签名 → P2P 网络 → 共识机制 → 智能合约安全”的顺序学习。理解每层的职责与边界，比单独记忆概念更重要。&lt;/p&gt;
</content:encoded></item><item><title>学习笔记：用 Spec Coding 让 AI 编程更可控</title><link>https://blog.xqcherry.top/posts/learning/spec-driven-development/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/learning/spec-driven-development/</guid><description>从意图、规格到验证，理解 Spec Coding 为什么能减少 AI 编程中的返工，并厘清 Spec、设计、计划与实现的边界。</description><pubDate>Sat, 22 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;AI 可以很快产出代码，但它无法自动知道需求中的隐含约束。Spec Coding（规格驱动开发）把“先对齐要做什么，再讨论怎么实现”变成显式步骤，从而减少猜测和返工。&lt;/p&gt;
&lt;h2&gt;从模糊需求到可验证行为&lt;/h2&gt;
&lt;p&gt;直接把一句需求交给 AI，通常会留下许多未决问题：边界条件是什么、接口如何变化、旧用户是否兼容、失败时如何处理、验收标准又是什么。&lt;/p&gt;
&lt;p&gt;以“为登录系统加入二次验证”为例，规格不应止步于功能名称，而应描述可观察行为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Requirement:
已开启二次验证的用户，必须完成第二因素验证后才能登录。

Scenario:
WHEN 用户提交正确的用户名和密码
AND 该用户已开启二次验证
THEN 系统要求输入一次性验证码
AND 验证通过后才返回登录会话
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::important[Spec 不是愿望清单]
好的规格要写清行为、边界和场景，并能转化为验收测试。它约束的是结果，而非单纯罗列实现方式。
:::&lt;/p&gt;
&lt;h2&gt;一个可复用的工作流&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Intent → Specification → Design / Plan → Implementation → Verification
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;阶段&lt;/th&gt;
&lt;th&gt;要回答的问题&lt;/th&gt;
&lt;th&gt;产出&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Intent&lt;/td&gt;
&lt;td&gt;为什么做、用户要解决什么问题？&lt;/td&gt;
&lt;td&gt;目标与范围&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Specification&lt;/td&gt;
&lt;td&gt;系统应表现成什么样？&lt;/td&gt;
&lt;td&gt;需求、场景、验收条件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design&lt;/td&gt;
&lt;td&gt;用什么方案满足规格？&lt;/td&gt;
&lt;td&gt;架构与关键取舍&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plan&lt;/td&gt;
&lt;td&gt;具体按什么顺序做？&lt;/td&gt;
&lt;td&gt;可执行任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Implementation&lt;/td&gt;
&lt;td&gt;如何改动代码？&lt;/td&gt;
&lt;td&gt;代码与配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verification&lt;/td&gt;
&lt;td&gt;如何证明实现正确？&lt;/td&gt;
&lt;td&gt;测试、检查与人工验证&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这里最容易混淆的是 Plan 和 Spec：Plan 是完成当前改动的临时执行路线；Spec 是系统行为的长期描述。前者回答“怎么做”，后者回答“应该做成什么样”。&lt;/p&gt;
&lt;h2&gt;规格应该包含什么&lt;/h2&gt;
&lt;p&gt;一份轻量但有效的 Spec 通常覆盖以下内容：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;范围与非目标&lt;/strong&gt;：本次要做什么，明确不做什么。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户可观察的行为&lt;/strong&gt;：正常路径、错误路径和边界条件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;兼容性约束&lt;/strong&gt;：已有接口、数据和用户流程是否受影响。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验收方式&lt;/strong&gt;：哪些自动化测试、手工步骤或指标能证明完成。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::tip[先写最小规格]
不需要为每个小改动写长文档。先列出能消除关键歧义的场景与验收条件；复杂需求再逐步补充分支和设计决策。
:::&lt;/p&gt;
&lt;h2&gt;工具与方法论的分工&lt;/h2&gt;
&lt;p&gt;在 AI 编程工作流中，规格管理工具和 Agent 工作流框架常被一起使用，但职责不同：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;关注点&lt;/th&gt;
&lt;th&gt;规格管理&lt;/th&gt;
&lt;th&gt;Agent 工作流&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;核心问题&lt;/td&gt;
&lt;td&gt;系统现在是什么、这次要改变什么？&lt;/td&gt;
&lt;td&gt;Agent 应如何分析、实现与验证？&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;典型产物&lt;/td&gt;
&lt;td&gt;当前规格、变更提案、设计、任务&lt;/td&gt;
&lt;td&gt;调研、计划、测试、评审、验证流程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;价值&lt;/td&gt;
&lt;td&gt;保留项目长期上下文&lt;/td&gt;
&lt;td&gt;提升一次开发过程的可重复性&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;例如 OpenSpec 一类工具适合沉淀“当前系统行为”和“待合并变更”；Superpowers 一类工作流适合规范 brainstorm、计划、TDD、评审与验证。它们可以组合，但都不能代替对需求本身的判断。&lt;/p&gt;
&lt;h2&gt;练习方式&lt;/h2&gt;
&lt;p&gt;下一次让 AI 修改代码前，先写下三件事：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. 用户最终能看到什么变化？
2. 哪些旧行为必须保持不变？
3. 用什么证据确认它真的完成了？
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这三问足以将多数模糊 Prompt 转化为可执行的规格，也是开始实践 Spec Coding 最小、最有价值的一步。&lt;/p&gt;
</content:encoded></item><item><title>从域名到应用：Route 53、CloudFront、Nginx 与 Next.js 的部署链路</title><link>https://blog.xqcherry.top/posts/operations/cloudfront-route53-nginx-architecture/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/operations/cloudfront-route53-nginx-architecture/</guid><description>用一个通用架构说明域名注册、DNS、CloudFront、源站域名、Nginx 与 Next.js 各自的职责，以及请求如何从浏览器抵达应用。</description><pubDate>Tue, 21 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;一个面向公网的 Web 应用通常由“域名注册、DNS 解析、CDN 入口、源站代理与应用进程”几层构成。把职责拆开，排查访问、缓存或证书问题时就能快速定位到正确的一层。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;浏览器
  → DNS（Route 53）
  → CDN（CloudFront）
  → 源站域名（origin.example.com）
  → Nginx
  → Next.js 应用
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;各组件的职责&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;组件&lt;/th&gt;
&lt;th&gt;负责什么&lt;/th&gt;
&lt;th&gt;不负责什么&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;域名注册商&lt;/td&gt;
&lt;td&gt;域名归属与续费&lt;/td&gt;
&lt;td&gt;不必然提供 DNS 解析&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Route 53&lt;/td&gt;
&lt;td&gt;DNS 托管与记录解析&lt;/td&gt;
&lt;td&gt;不提供应用服务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CloudFront&lt;/td&gt;
&lt;td&gt;HTTPS 入口、边缘缓存、回源转发&lt;/td&gt;
&lt;td&gt;不替代源站业务逻辑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nginx&lt;/td&gt;
&lt;td&gt;反向代理、请求限制、响应头&lt;/td&gt;
&lt;td&gt;不生成 Next.js 页面&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next.js&lt;/td&gt;
&lt;td&gt;页面渲染、API 和业务逻辑&lt;/td&gt;
&lt;td&gt;不管理 DNS 或 CDN 证书&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;DNS 与入口配置&lt;/h2&gt;
&lt;p&gt;常见的记录关系是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;example.com        A/AAAA Alias → CloudFront 分发
www.example.com    A/AAAA Alias → CloudFront 分发
origin.example.com A/AAAA          → 源站公网地址或负载均衡器
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主域名和 &lt;code&gt;www&lt;/code&gt; 交给 CloudFront，源站使用专门的子域名。这样迁移服务器时只需更新源站记录，不必改动 CDN 分发配置。&lt;/p&gt;
&lt;p&gt;:::note[证书位置]
为 CloudFront 自定义域名签发的 ACM 证书必须位于 &lt;code&gt;us-east-1&lt;/code&gt; 区域。源站若使用 HTTPS，还需要一张与 &lt;code&gt;origin.example.com&lt;/code&gt; 匹配且由 CloudFront 信任的证书。
:::&lt;/p&gt;
&lt;h2&gt;缓存策略要与请求类型匹配&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;路径类型&lt;/th&gt;
&lt;th&gt;推荐策略&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;带哈希的 JS/CSS/图片&lt;/td&gt;
&lt;td&gt;长缓存，文件名变化即更新&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;页面 HTML&lt;/td&gt;
&lt;td&gt;依据是否 SSR、ISR 或静态导出设置短缓存/不缓存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/api/*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;通常不缓存，转发必要请求头和查询参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/ws/*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不缓存，并确认 CDN 与源站均允许 WebSocket 升级&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/_next/image*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;结合 Next.js 图片优化和缓存策略配置&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;不要把“动态接口一律缓存”或“静态资源一律不缓存”作为默认策略；缓存键、TTL、Cookie 与查询参数都要随业务调整。&lt;/p&gt;
&lt;h2&gt;源站反向代理示意&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;server {
  listen 443 ssl http2;
  server_name origin.example.com;

  location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生产环境还应限制源站仅接受 CloudFront 或受信网络的访问、启用访问日志和健康检查，并避免把管理后台直接暴露在未保护的子域名上。&lt;/p&gt;
&lt;h2&gt;排查顺序&lt;/h2&gt;
&lt;p&gt;遇到访问故障时，按“DNS 记录 → CloudFront 分发与证书 → 源站可达性 → Nginx 日志 → 应用日志”逐层验证。每层只确认一个事实，可以避免在错误的组件上反复修改配置。&lt;/p&gt;
</content:encoded></item><item><title>WireGuard 组网实践：用 VPS 建立私有服务器网络</title><link>https://blog.xqcherry.top/posts/operations/wireguard-private-network/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/operations/wireguard-private-network/</guid><description>以一台公网 VPS 为中心，配置 WireGuard 服务端与多个节点，建立仅供内部服务访问的加密私有网段。</description><pubDate>Sun, 28 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;WireGuard 适合将多台分散的服务器连接到一个私有网段。以下示例以公网 VPS 为中心节点，两个内网节点通过加密隧道互通；示例地址请按自己的网络规划替换。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Internet
   │
VPS / WireGuard Server · 10.20.0.1
   ├── Node A · 10.20.0.2
   └── Node B · 10.20.0.3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning[密钥安全]
每个节点必须生成独立密钥对。私钥仅保存在节点本机，不能提交仓库、发送给他人或出现在截图中；配置示例中的占位符不是实际密钥。
:::&lt;/p&gt;
&lt;h2&gt;1. 安装并生成节点密钥&lt;/h2&gt;
&lt;p&gt;在每个节点执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update
sudo apt install -y wireguard
umask 077
wg genkey | tee privatekey | wg pubkey &amp;gt; publickey
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;记录每个节点的&lt;strong&gt;公钥&lt;/strong&gt;，并安全保存其私钥。VPS 需要开放 UDP &lt;code&gt;51820&lt;/code&gt;（或自定义端口）。&lt;/p&gt;
&lt;h2&gt;2. 配置中心 VPS&lt;/h2&gt;
&lt;p&gt;编辑 &lt;code&gt;/etc/wireguard/wg0.conf&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Interface]
Address = 10.20.0.1/24
ListenPort = 51820
PrivateKey = &amp;lt;VPS_PRIVATE_KEY&amp;gt;

[Peer]
PublicKey = &amp;lt;NODE_A_PUBLIC_KEY&amp;gt;
AllowedIPs = 10.20.0.2/32

[Peer]
PublicKey = &amp;lt;NODE_B_PUBLIC_KEY&amp;gt;
AllowedIPs = 10.20.0.3/32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;AllowedIPs&lt;/code&gt; 同时用于路由和对端身份约束；服务端应为每个 peer 精确指定单个地址或必要的私有子网，避免使用过宽的范围。&lt;/p&gt;
&lt;h2&gt;3. 配置客户端节点&lt;/h2&gt;
&lt;p&gt;以 Node B 为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Interface]
Address = 10.20.0.3/24
PrivateKey = &amp;lt;NODE_B_PRIVATE_KEY&amp;gt;

[Peer]
PublicKey = &amp;lt;VPS_PUBLIC_KEY&amp;gt;
Endpoint = &amp;lt;VPS_PUBLIC_HOST&amp;gt;:51820
AllowedIPs = 10.20.0.0/24
PersistentKeepalive = 25
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;PersistentKeepalive = 25&lt;/code&gt; 对位于 NAT 后的节点很有帮助，它会周期性维持映射。Node A 按相同方式配置自己的地址和密钥。&lt;/p&gt;
&lt;h2&gt;4. 启动与验证&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable --now wg-quick@wg0
sudo wg show
ping 10.20.0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当 &lt;code&gt;wg show&lt;/code&gt; 出现最近握手时间、收发字节数增长时，说明隧道已建立。随后可从节点间 ping 私网地址，或用 &lt;code&gt;ip route&lt;/code&gt; 检查预期路由。&lt;/p&gt;
&lt;h2&gt;常见故障排查&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;没有握手：检查 UDP 端口、防火墙、Endpoint 地址和双方公钥是否对应。&lt;/li&gt;
&lt;li&gt;能握手但无法访问：检查 &lt;code&gt;AllowedIPs&lt;/code&gt;、本机路由及防火墙规则。&lt;/li&gt;
&lt;li&gt;需要让一个节点转发到其他子网：在转发节点启用 IP forwarding，并明确增加路由与最小化的防火墙/NAT 规则。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;仅需服务器之间私网互访时，不要把 &lt;code&gt;AllowedIPs&lt;/code&gt; 配为 &lt;code&gt;0.0.0.0/0&lt;/code&gt;；这会把全部流量都导向 VPN，增加意外断网和暴露风险。&lt;/p&gt;
</content:encoded></item><item><title>Linux 服务器初始化：创建管理员用户并启用 SSH 密钥登录</title><link>https://blog.xqcherry.top/posts/operations/linux-user-and-ssh-access/</link><guid isPermaLink="true">https://blog.xqcherry.top/posts/operations/linux-user-and-ssh-access/</guid><description>一份最小而安全的 Linux 服务器初始化流程：创建日常管理员账户、配置 sudo、部署 SSH 公钥，并在验证后关闭密码登录。</description><pubDate>Wed, 27 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;新服务器不应长期直接使用 root 账户，也不应把密码作为主要远程登录方式。下面的流程将日常操作切换到拥有 sudo 权限的普通用户，并通过 SSH 密钥认证访问服务器。&lt;/p&gt;
&lt;p&gt;:::caution[避免把自己锁在门外]
请保持当前 root 会话不断开，在&lt;strong&gt;另一个终端&lt;/strong&gt;确认新用户可以通过密钥登录并运行 &lt;code&gt;sudo&lt;/code&gt; 后，再修改 SSH 的密码登录策略。
:::&lt;/p&gt;
&lt;h2&gt;1. 创建日常管理员账户&lt;/h2&gt;
&lt;p&gt;将 &lt;code&gt;&amp;lt;user&amp;gt;&lt;/code&gt; 替换为实际用户名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo useradd --create-home --shell /bin/bash &amp;lt;user&amp;gt;
sudo passwd &amp;lt;user&amp;gt;
sudo usermod -aG sudo &amp;lt;user&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Debian/Ubuntu 上，可用以下命令验证该用户是否拥有 sudo 权限：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;su - &amp;lt;user&amp;gt;
sudo -v
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 在本地生成 SSH 密钥&lt;/h2&gt;
&lt;p&gt;如果还没有可用密钥，在本地机器执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t ed25519 -a 100 -C &quot;your_email@example.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;私钥应始终只保留在你的受控设备或密钥管理工具中；不要上传、粘贴到聊天记录，也不要提交到仓库。&lt;/p&gt;
&lt;h2&gt;3. 部署并验证公钥&lt;/h2&gt;
&lt;p&gt;使用密码仅完成这一次公钥部署：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-copy-id &amp;lt;user&amp;gt;@&amp;lt;server-host&amp;gt;
ssh &amp;lt;user&amp;gt;@&amp;lt;server-host&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;无法使用 &lt;code&gt;ssh-copy-id&lt;/code&gt; 时，可将本地 &lt;code&gt;~/.ssh/id_ed25519.pub&lt;/code&gt; 的内容追加到服务器用户的 &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt;，并确保权限正确：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 验证后收紧 SSH 策略&lt;/h2&gt;
&lt;p&gt;确认密钥登录正常后，在 &lt;code&gt;/etc/ssh/sshd_config.d/hardening.conf&lt;/code&gt;（或发行版对应的 sshd 配置文件）加入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检查配置并平滑重载服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo sshd -t
sudo systemctl reload ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::important[恢复方式]
在关闭密码认证前，确保至少两台受控设备保存了可用私钥，或确认云厂商控制台能够提供带外恢复入口。
:::&lt;/p&gt;
</content:encoded></item></channel></rss>