5. 最长回文子串 作者: zorth 时间: 2024-11-24 分类: 算法 评论 中等题 | 动态规划 | 给你一个字符串 `s`,找到 `s` 中最长的回文子串。 **示例 1:** ``` 输入:s = "babad" 输出:"bab" 解释:"aba" 同样是符合题意的答案。 ``` **示例 2:** ``` 输入:s = "cbbd" 输出:"bb" ``` **提示:** - `1 <= s.length <= 1000` - `s` 仅由数字和英文字母组成 --- 这是一个有故事的题目,2019年的春天,当时面试浦发银行的时候就有这道题目,当年四道算法题,一道都没有做出来,经过那场面试之后,我才知道了有力扣这东西。 --- 动态规划(Dynamic Programming,简称 DP)是一种通过将复杂问题分解为更小的子问题,并利用子问题的解来构建原问题解的算法设计方法。它通常用于解决具有**重叠子问题**和**最优子结构**性质的问题。 ### 动态规划的核心思想 动态规划的核心思想是通过**记忆化**或**递推**的方式,避免重复计算子问题,从而提高算法效率。它的基本步骤包括: 1. **定义状态(State)**:用一个数组或表格来表示问题的解。 2. **状态转移方程(State Transition Equation)**:找到状态之间的递推关系。 3. **初始化(Initialization)**:确定初始状态的值。 4. **计算顺序(Order of Computation)**:通常是从小问题逐步递推到大问题。 5. **返回结果(Result Extraction)**:从状态表中提取最终解。 ### 动态规划的两种实现方式 1. **自顶向下(Top-Down)**:也称为记忆化搜索(Memoization)。通过递归的方式解决问题,同时将已经计算过的子问题结果存储起来,避免重复计算。 2. **自底向上(Bottom-Up)**:也称为递推(Tabulation)。从最小的子问题开始,逐步计算出更大的子问题的解,直到得到最终解。 ### 动态规划的两个重要性质 1. **重叠子问题(Overlapping Subproblems)** 问题可以分解为多个子问题,且这些子问题会重复出现。例如,斐波那契数列的递归计算中,`F(5)` 会调用 `F(4)` 和 `F(3)`,而 `F(4)` 又会调用 `F(3)` 和 `F(2)`,导致重复计算。 2. **最优子结构(Optimal Substructure)** 问题的最优解可以通过子问题的最优解构造出来。例如,最短路径问题中,从起点到终点的最短路径可以通过中间节点的最短路径组合而成。 ### 动态规划的常见问题类型 1. **线性问题** - 斐波那契数列:`F(n) = F(n-1) + F(n-2)` - 最长递增子序列(LIS) - 最大子数组和(Kadane's Algorithm) 2. **区间问题** - 戳气球问题(Burst Balloons) - 最长回文子序列 3. **背包问题** - 0-1 背包问题 - 完全背包问题 4. **序列问题** - 编辑距离(Edit Distance) - 最长公共子序列(LCS) 5. **路径问题** - 矩阵路径问题(如最小路径和) - 图中的最短路径问题(如 Floyd-Warshall 算法) 6. **划分问题** - 分割等和子集 - 石子游戏问题 ### 动态规划的经典例子 #### 1. 斐波那契数列 **问题描述**:计算第 `n` 个斐波那契数。 **状态定义**:`dp[i]` 表示第 `i` 个斐波那契数。 **状态转移方程**:`dp[i] = dp[i-1] + dp[i-2]` **初始化**:`dp[0] = 0, dp[1] = 1` **代码实现**: ```python def fibonacci(n): if n <= 1: return n dp = [0] * (n + 1) dp[0], dp[1] = 0, 1 for i in range(2, n + 1): dp[i] = dp[i - 1] + dp[i - 2] return dp[n] ``` #### 2. 0-1 背包问题 **问题描述**:给定 `n` 个物品,每个物品有重量 `w[i]` 和价值 `v[i]`,以及一个容量为 `W` 的背包,求如何选择物品使得总价值最大。 **状态定义**:`dp[i][j]` 表示前 `i` 个物品在容量为 `j` 的情况下的最大价值。 **状态转移方程**: - 如果不选第 `i` 个物品:`dp[i][j] = dp[i-1][j]` - 如果选第 `i` 个物品:`dp[i][j] = dp[i-1][j-w[i]] + v[i]` - 综合:`dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])` **初始化**:`dp[0][j] = 0`(没有物品时价值为 0) **代码实现**: ```python def knapsack(weights, values, W): n = len(weights) dp = [[0] * (W + 1) for _ in range(n + 1)] for i in range(1, n + 1): for j in range(W + 1): if weights[i-1] <= j: dp[i][j] = max(dp[i-1][j], dp[i-1][j-weights[i-1]] + values[i-1]) else: dp[i][j] = dp[i-1][j] return dp[n][W] ``` ### 动态规划的优化 1. **空间优化**: - 如果状态转移只依赖于前一行或前几个状态,可以将二维数组压缩为一维数组。例如,0-1 背包问题可以优化为一维数组。 2. **状态压缩**: - 在某些问题中,可以通过位运算等方式压缩状态空间。 ### 动态规划的优缺点 **优点**: - 通过记忆化或递推避免了重复计算,显著提高效率。 - 能够解决许多复杂的优化问题。 **缺点**: - 状态空间可能很大,导致时间和空间复杂度较高。 - 需要仔细设计状态和转移方程,问题建模较为复杂。 --- 最长回文子序列(Longest Palindromic Subsequence, LPS)的常见动态规划解法通常是**自底向上的**,即通过递推的方式逐步计算出子问题的解,最终得到原问题的解。 ### 问题描述 给定一个字符串 `s`,求其最长回文子序列的长度。回文子序列是指一个序列可以正着读和反着读都相同,但不要求是连续的。 --- ### 自底向上的动态规划解法 #### 1. **状态定义** 设 `dp[i][j]` 表示字符串 `s` 从第 `i` 个字符到第 `j` 个字符之间的最长回文子序列的长度。 #### 2. **状态转移方程** - 如果 `s[i] == s[j]`,那么 `dp[i][j] = dp[i+1][j-1] + 2`,因为首尾字符相等,可以将它们加入回文子序列。 - 如果 `s[i] != s[j]`,那么 `dp[i][j] = max(dp[i+1][j], dp[i][j-1])`,即最长回文子序列要么不包含 `s[i]`,要么不包含 `s[j]`。 #### 3. **初始化** - 当 `i == j` 时,`dp[i][i] = 1`,因为单个字符本身就是一个长度为 1 的回文子序列。 - 当 `i > j` 时,`dp[i][j] = 0`,因为无效区间。 #### 4. **计算顺序** 由于 `dp[i][j]` 的值依赖于 `dp[i+1][j-1]`、`dp[i+1][j]` 和 `dp[i][j-1]`,所以我们需要从区间长度较短的子问题开始计算,逐步扩展到整个字符串。 #### 5. **最终结果** `dp[0][n-1]` 即为整个字符串的最长回文子序列长度,其中 `n` 是字符串的长度。 --- ### 代码实现 以下是自底向上的动态规划实现: ```python def longestPalindromeSubseq(s): n = len(s) # 初始化 dp 数组 dp = [[0] * n for _ in range(n)] # 单个字符的回文子序列长度为 1 for i in range(n): dp[i][i] = 1 # 从长度为 2 的子问题开始计算,逐步扩展到整个字符串 for length in range(2, n + 1): # 子串长度从 2 到 n for i in range(n - length + 1): # 起始位置 j = i + length - 1 # 终止位置 if s[i] == s[j]: dp[i][j] = dp[i + 1][j - 1] + 2 else: dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]) # 返回整个字符串的最长回文子序列长度 return dp[0][n - 1] ``` --- ### 示例 ```python s = "bbbab" print(longestPalindromeSubseq(s)) # 输出: 4 ``` **解释**:最长回文子序列是 `"bbbb"`,长度为 4。 --- ### 时间和空间复杂度 - **时间复杂度**:`O(n^2)`,因为我们需要填充一个 `n x n` 的二维数组,每个状态的计算需要常数时间。 - **空间复杂度**:`O(n^2)`,用于存储 `dp` 数组。 #### 空间优化 如果只需要返回最长回文子序列的长度,可以将二维数组压缩为一维数组,因为 `dp[i][j]` 只依赖于当前行和上一行的状态。 --- ```java public class Solution { public String longestPalindrome(String s) { int len = s.length(); if (len < 2) { return s; } int maxLen = 1; int begin = 0; // dp[i][j] 表示 s[i..j] 是否是回文串 boolean[][] dp = new boolean[len][len]; // 初始化:所有长度为 1 的子串都是回文串 for (int i = 0; i < len; i++) { dp[i][i] = true; } char[] charArray = s.toCharArray(); // 递推开始 // 先枚举子串长度 for (int L = 2; L <= len; L++) { // 枚举左边界,左边界的上限设置可以宽松一些 for (int i = 0; i < len; i++) { // 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得 int j = L + i - 1; // 如果右边界越界,就可以退出当前循环 if (j >= len) { break; } if (charArray[i] != charArray[j]) { dp[i][j] = false; } else { if (j - i < 3) { dp[i][j] = true; } else { dp[i][j] = dp[i + 1][j - 1]; } } // 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置 if (dp[i][j] && j - i + 1 > maxLen) { maxLen = j - i + 1; begin = i; } } } return s.substring(begin, begin + maxLen); } } ``` 这里的 `L` 表示**子串的长度**,是动态规划中递推的一个关键变量。通过枚举子串的长度 `L`,我们可以从短的子串开始计算,逐步扩展到更长的子串,最终覆盖整个字符串。这种写法是动态规划中常见的**自底向上递推**方式。 --- ### 为什么要用 `L` 表示子串长度? 动态规划的核心是通过已知的子问题解来推导更大的问题解。在这个问题中,`dp[i][j]` 表示从字符串第 `i` 个字符到第 `j` 个字符之间的最长回文子序列长度。为了计算 `dp[i][j]`,我们需要依赖更短的子串的结果,比如 `dp[i+1][j-1]`、`dp[i+1][j]` 和 `dp[i][j-1]`。 因此,递推的顺序必须是从短的子串开始,逐步扩展到更长的子串。`L` 就是用来控制子串的长度的变量。 --- ### 代码逻辑解析 ```cpp // 先枚举子串长度 for (int L = 2; L <= len; L++) { // 枚举左边界,左边界的上限设置可以宽松一些 for (int i = 0; i < len; i++) { // 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得 int j = L + i - 1; // 如果右边界越界,就可以退出当前循环 if (j >= len) { break; } // 状态转移逻辑 if (s[i] == s[j]) { dp[i][j] = dp[i + 1][j - 1] + 2; } else { dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); } } } ``` #### 1. **外层循环:枚举子串长度** ```cpp for (int L = 2; L <= len; L++) { ``` - `L` 表示子串的长度,从 2 开始,因为长度为 1 的子串已经初始化(`dp[i][i] = 1`)。 - 通过从小到大的子串长度递推,保证在计算 `dp[i][j]` 时,所依赖的状态(如 `dp[i+1][j-1]`)已经被计算过。 --- #### 2. **内层循环:枚举左边界** ```cpp for (int i = 0; i < len; i++) { ``` - `i` 是子串的左边界。 - 枚举所有可能的左边界 `i`,从 0 开始。 --- #### 3. **确定右边界** ```cpp int j = L + i - 1; ``` - 通过子串长度 `L` 和左边界 `i`,可以确定右边界 `j`: \[ j - i + 1 = L \implies j = L + i - 1 \] - 例如: - 如果子串长度 `L = 3`,左边界 `i = 2`,那么右边界 `j = 3 + 2 - 1 = 4`。 --- #### 4. **右边界越界检查** ```cpp if (j >= len) { break; } ``` - 如果右边界 `j` 超出了字符串的长度 `len`,说明当前左边界 `i` 和子串长度 `L` 不合法,直接退出循环。 --- #### 5. **状态转移** ```cpp if (s[i] == s[j]) { dp[i][j] = dp[i + 1][j - 1] + 2; } else { dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); } ``` - 如果 `s[i] == s[j]`,说明首尾字符可以构成回文,最长回文子序列长度等于去掉首尾后的子串的最长回文子序列长度 `dp[i+1][j-1]` 加上 2。 - 如果 `s[i] != s[j]`,说明首尾字符不能同时出现在回文子序列中,取决于去掉左边界或右边界后的子串的最长回文子序列长度,即 `max(dp[i+1][j], dp[i][j-1])`。 --- ### 为什么要用 `L` 控制子串长度? 1. **保证递推顺序正确** 动态规划的递推顺序必须从小问题到大问题。如果直接枚举 `i` 和 `j`,可能会导致依赖的状态(如 `dp[i+1][j-1]`)还未被计算,无法完成递推。而通过 `L` 控制子串长度,可以确保在计算长度为 `L` 的子串时,长度为 `L-1` 的子串已经被计算过。 2. **逻辑清晰** 使用 `L` 表示子串长度,逻辑上更清晰:先计算长度为 2 的子串,再计算长度为 3 的子串,依次类推,直到整个字符串。 3. **避免重复计算** 通过从短子串到长子串的顺序递推,避免了重复计算子问题的解。 --- ### 改写为更清晰的版本 如果觉得原代码不够直观,可以稍微调整注释和变量名,使其更易理解: ```cpp // 动态规划求解最长回文子序列 for (int length = 2; length <= len; length++) { // 枚举子串长度 for (int left = 0; left < len; left++) { // 枚举左边界 int right = left + length - 1; // 计算右边界 if (right >= len) { // 如果右边界越界,跳出循环 break; } if (s[left] == s[right]) { // 如果首尾字符相等 dp[left][right] = dp[left + 1][right - 1] + 2; } else { // 如果首尾字符不相等 dp[left][right] = max(dp[left + 1][right], dp[left][right - 1]); } } } ``` --- ### 总结 - `L` 表示子串的长度,控制递推的顺序,从短子串到长子串逐步计算。 - 这种写法是动态规划中常见的自底向上递推方式,保证了依赖的状态已经被计算。 - 通过 `L` 和 `i` 确定右边界 `j`,可以避免直接枚举所有可能的 `(i, j)`,使得逻辑更加清晰。
2024.11.24日记 作者: zorth 时间: 2024-11-24 分类: 日记 评论 后端开发到全栈开发是一条漫长的路,近一年来我反复尝试了进行前端开发,大都是浅尝辄止,四处copy。最近回头体会,发现自己基础并不好,最后的收获也是寥寥。 我的后端开发之路就不太一样,从大学到读研再到最后工作,一路虽然没有刻意学习,但是耳濡目染之下,后端的很多基础知识是潜移默化得具备的。一段时间的积累之后,后端的开发水平也算是水到渠成。 最近看到了一个很优秀的小伙子,比我年轻好几岁,但是成长的速度好快,让我中间一度很焦躁。现在想要是自己太过急功近利了,我在推上关注了很多人,今天翻了翻,基本上都是前端开发。后端开发们都不上推吧(笑)。 学习是一个细水长流的过程,日拱一卒,功不唐捐。最终量变一定会引起质变。 最近刚把自己的个人博客收拾起来,心理难免急躁,仅留下此文作为纪念。
3238. 求出胜利玩家的数目 作者: zorth 时间: 2024-11-23 分类: 算法 评论 简单题 | 数组 | 给你一个整数 `n` ,表示在一个游戏中的玩家数目。同时给你一个二维整数数组 `pick` ,其中 `pick[i] = [xi, yi]` 表示玩家 `xi` 获得了一个颜色为 `yi` 的球。 如果玩家 `i` 获得的球中任何一种颜色球的数目 **严格大于** `i` 个,那么我们说玩家 `i` 是胜利玩家。换句话说: - 如果玩家 0 获得了任何的球,那么玩家 0 是胜利玩家。 - 如果玩家 1 获得了至少 2 个相同颜色的球,那么玩家 1 是胜利玩家。 - ... - 如果玩家 `i` 获得了至少 `i + 1` 个相同颜色的球,那么玩家 `i` 是胜利玩家。 请你返回游戏中 **胜利玩家** 的数目。 **注意**,可能有多个玩家是胜利玩家。 **示例 1:** **输入:**n = 4, pick = [[0,0],[1,0],[1,0],[2,1],[2,1],[2,0]] **输出:**2 **解释:** 玩家 0 和玩家 1 是胜利玩家,玩家 2 和玩家 3 不是胜利玩家。 **示例 2:** **输入:**n = 5, pick = [[1,1],[1,2],[1,3],[1,4]] **输出:**0 **解释:** 没有胜利玩家。 **示例 3:** **输入:**n = 5, pick = [[1,1],[2,4],[2,4],[2,4]] **输出:**1 **解释:** 玩家 2 是胜利玩家,因为玩家 2 获得了 3 个颜色为 4 的球。 **提示:** - `2 <= n <= 10` - `1 <= pick.length <= 100` - `pick[i].length == 2` - `0 <= xi <= n - 1 ` - `0 <= yi <= 10` --- 刚上来的时候觉得可能是要用哈希表,还顺带复习了一下哈希表的相关知识: ```java // Creating a HashMap HashMap map = new HashMap<>(); // Adding elements map.put(key, value); // Getting values ValueType value = map.get(key); // Removing elements map.remove(key); // Checking if key exists boolean exists = map.containsKey(key); ``` 实际上,从哈希表的角度来想,太复杂了,针对每一个玩家都要进行一遍哈希表的对比,时间复杂度上、空间复杂度上来说,都太高了。 不得已看了题解,题解巧妙的地方在于把任务拆解了,因为提示里提到颜色的范围是大于0小于10的,所以至多有11个颜色,如果创建一个二维数组,每一行代表每一个用户,每一列代表每一个颜色,然后遍历pick数组,他的每一个数值出现时,在二维数组上对应坐标加一。 最后问题会变成一个简单的二维数组遍历问题。 ```java class Solution { public int winningPlayerCount(int n, int[][] pick) { int cnt[][] = new int[n][11]; for (int[] p : pick) { cnt[p[0]][p[1]]++; } int ans = 0; for (int i = 0; i < n; i++) { for (int j = 0; j < 11; j++) { if (cnt[i][j] > i) { ans++; break; } } } return ans; } } ``` --- 这里带上官方的题解: 首先我们需要统计每个玩家得到的每种颜色的球的数目,此时需要遍历 数组 pick。由于颜色 y 的取值范围为 [0,10],此时我们用一个 n×11 的二维数组统计每个玩家得到的每种颜色的球的数目。然后我们从 0 到 n−1 依次遍历每个玩家,如果d当前第 i 个玩家至少有一种颜色的球大于玩家编号 i,则胜利玩家数目加 1,返回总的胜利玩家数目即可。
兰空图床搭建 作者: zorth 时间: 2024-11-23 分类: 默认分类 评论 ![](https://zorth-1305096201.cos.ap-guangzhou.myqcloud.com/undefined20241123200945.png) 列出php的dnf源 [root@iZuf61llk9btoze85qtbmzZ ~]# dnf list php CentOS Stream 9 - BaseOS 34 MB/s | 8.3 MB 00:00 CentOS Stream 9 - AppStream 53 MB/s | 21 MB 00:00 CentOS Stream 9 - Extras packages 426 kB/s | 19 kB 00:00 Available Packages php.x86_64 8.0.30-1.el9 appstream [root@iZuf61llk9btoze85qtbmzZ ~]# 非常好,8.0.30,可以直接用了 [root@iZuf61llk9btoze85qtbmzZ ~]# dnf install php [root@iZuf61llk9btoze85qtbmzZ ~]# php -v PHP 8.0.30 (cli) (built: Aug 3 2023 17:13:08) ( NTS gcc x86_64 ) Copyright (c) The PHP Group Zend Engine v4.0.30, Copyright (c) Zend Technologies with Zend OPcache v8.0.30, Copyright (c), by Zend Technologies 下面开始安装nginx,安装好之后记得启动nginx [root@iZuf61llk9btoze85qtbmzZ ~]# dnf install nginx [root@iZuf61llk9btoze85qtbmzZ ~]# nginx -v nginx version: nginx/1.20.1 [root@iZuf61llk9btoze85qtbmzZ ~]# ps aux | grep nginx root 4721 0.0 0.1 6408 2304 pts/0 S+ 13:09 0:00 grep --color=auto nginx [root@iZuf61llk9btoze85qtbmzZ ~]# systemctl start nginx [root@iZuf61llk9btoze85qtbmzZ ~]# ps aux | grep nginx root 4728 0.0 0.0 11236 1632 ? Ss 13:09 0:00 nginx: master process /usr/sbin/nginx nginx 4729 0.0 0.2 15564 5216 ? S 13:09 0:00 nginx: worker process nginx 4730 0.0 0.3 15564 5344 ? S 13:09 0:00 nginx: worker process root 4737 0.0 0.1 6408 2304 pts/0 S+ 13:09 0:00 grep --color=auto nginx 启动nginx之后,可以直接访问网站了: ![image.png](https://note.youdao.com/yws/res/1/WEBRESOURCE6d4677ad43d90956d1293cd90c31c061) 进入nginx路径,开始配置https,操作配置文件之前记得备份 [root@iZuf61llk9btoze85qtbmzZ nginx]# cd /etc/nginx/ [root@iZuf61llk9btoze85qtbmzZ nginx]# cp nginx.conf nginx.conf.bak [root@iZuf61llk9btoze85qtbmzZ nginx]# 在此之前先去配置一下域名的dns,把我的域名和ip绑定好。 ![image.png](https://note.youdao.com/yws/res/0/WEBRESOURCE5587d5d43464a152cc7062301d19a350) 这里有一个很关键的地方,使用cf作为dns服务器的时候,还需要设置一下SSL的强制HTTPS ![image.png](https://note.youdao.com/yws/res/4/WEBRESOURCE38317958c014c0328d0b7cf55d256514) 现在开始配置nginx的配置文件: # HTTP服务器(重定向到HTTPS) server { listen 80; listen [::]:80; server_name zorth.top; # 替换为你的域名 # 将所有HTTP请求重定向到HTTPS return 301 https://$server_name$request_uri; } # HTTPS服务器 server { listen 443 ssl; listen [::]:443 ssl; server_name zorth.top; # 替换为你的域名 # SSL配置 ssl_certificate /path/to/your/certificate.crt; # 替换为你的证书路径 ssl_certificate_key /path/to/your/private.key; # 替换为你的私钥路径 # SSL安全配置 ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; # 网站根目录 root /usr/share/nginx/html; # 替换为你的网站目录 index index.php index.html index.htm; location / { try_files $uri $uri/ /index.php?$query_string; } # PHP配置 location ~ \.php$ { fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock; # 如果使用TCP,改为 127.0.0.1:9000 fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } # 禁止访问 .htaccess 文件 location ~ /\.ht { deny all; } } 修改完配置之后,记得验证一下配置: [root@iZuf61llk9btoze85qtbmzZ nginx]# nginx -t nginx: [emerg] cannot load certificate "/path/to/your/certificate.crt": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/path/to/your/certificate.crt, r) error:10000080:BIO routines::no such file) nginx: configuration file /etc/nginx/nginx.conf test failed [root@iZuf61llk9btoze85qtbmzZ nginx]# nginx -t nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful [root@iZuf61llk9btoze85qtbmzZ nginx]# systemctl restart nginx 再次访问刚才的域名时,会直接重定向到https了 ![image.png](https://note.youdao.com/yws/res/3/WEBRESOURCEfff93410ce04d94d96b5b1d1d3a0a4c3) ![image.png](https://note.youdao.com/yws/res/f/WEBRESOURCE1903c4a491d17ffe03b11bad66289b4f) nginx配置完毕了,开始安装图床 [root@iZuf61llk9btoze85qtbmzZ nginx]# cd /usr/share/nginx/html/ [root@iZuf61llk9btoze85qtbmzZ html]# ll total 22356 -rw-r--r-- 1 root root 3971 Jul 16 07:21 404.html -rw-r--r-- 1 root root 4020 Jul 16 07:21 50x.html drwxr-xr-x 2 root root 27 Nov 23 13:08 icons lrwxrwxrwx 1 root root 25 Jul 16 07:22 index.html -> ../../testpage/index.html -rw-r--r-- 1 root root 22876343 Nov 23 13:32 lsky-pro-2.1.zip -rw-r--r-- 1 root root 368 Jul 16 07:21 nginx-logo.png lrwxrwxrwx 1 root root 14 Jul 16 07:22 poweredby.png -> nginx-logo.png lrwxrwxrwx 1 root root 37 Jul 16 07:22 system_noindex_logo.png -> ../../pixmaps/system-noindex-logo.png [root@iZuf61llk9btoze85qtbmzZ html]# 上传安装包之后,解压安装包。![image.png](https://note.youdao.com/yws/res/6/WEBRESOURCEf98d22b0e002a18e1c58de03ef076436) 我这里在root路径下解压了,然后将配置指向了public路径。
HTTP到HTTPS 作者: zorth 时间: 2024-11-20 分类: 默认分类 评论 # HTTP 与 HTTPS:有什么区别 HTTPS 是支持加密和验证的 HTTP。两种协议的唯一区别是HTTPS 使用 TLS (SSL) 来加密普通的 HTTP 请求和响应,并对这些请求和响应进行数字签名。因此,HTTPS 比 HTTP 安全得多。使用 HTTP 的网站的 URL 中带有 `http://`,而使用 HTTPS 的网站则带有 `https://`。 ![1732255490180.png](https://47.116.196.61/i/2024/11/22/67401f0495d7b.png) # 什么是 HTTP HTTP 代表超文本传输协议,它是一种用于通过网络传输数据的协议,或是一种表示信息的规范顺序和语法。通过互联网发送的大多数信息(包括网站内容和 API 调用)都使用 HTTP 协议。HTTP 消息主要有两种:请求和响应。 简单来说,HTTP 请求是遵循 HTTP 协议的一系列文本行。GET 请求可能如下所示: ```json GET /hello.txt HTTP/1.1 User-Agent: curl/7.63.0 libcurl/7.63.0 OpenSSL/1.1.l zlib/1.2.11 Host: www.example.com Accept-Language: en ``` 用户浏览器生成的这部分文本将通过 Internet 发送。而问题在于,它是明文形式发送的,监视连接的任何人都能读取它。(不熟悉 HTTP 协议的人可能觉得此文本难以理解,但任何对协议的命令和语法有基本了解的人都能轻松读懂。) 当用户通过网站或 Web 应用程序提交敏感数据时,这尤其是一个问题。敏感数据可能是密码、信用卡号,或在表单中输入的任何其他数据。而且在 HTTP 中,所有这些数据都以明文形式发送,任何人都能读取。(当用户提交表单时,浏览器会将其转换为 HTTP POST 请求,而不是 HTTP GET请求。) 源站服务器收到 HTTP 请求时,将发送 HTTP 响应,其类似于: ```json HTTP/1.1 200 OK Date: Wed, 30 Jan 2019 12:14:39 GMT Server: Apache Last-Modified: Mon, 28 Jan 2019 11:17:01 GMT Accept-Ranges: bytes Content-Length: 12 Vary: Accept-Encoding Content-Type: text/plain Hello World! ``` 如果网站使用 HTTP 而非 HTTPS,则监视会话的任何人都可以读取所有请求和响应。本质上,恶意行为者可以只读取请求或响应中的文本,就能知道某人正在索取、发送或接收的确切信息。 # 什么是 HTTPS HTTPS 中的 S 代表“安全”。HTTPS 使用 TLS(或 SSL)来加密HTTP 请求和响应,因此在上例中,攻击者看到的不是其文本,而是一堆看似随机的字符。 攻击者不会看到: ```json GET /hello.txt HTTP/1.1 User-Agent: curl/7.63.0 libcurl/7.63.0 OpenSSL/1.1.l zlib/1.2.11 Host: www.example.com Accept-Language: en ``` 而会看到类似如下: ```json t8Fw6T8UV81pQfyhDkhebbz7+oiwldr1j2gHBB3L3RFTRsQCpaSnSBZ78Vme+DpDVJPvZdZUZHpzbbcqmSW1+3xXGsERHg9YDmpYk0VVDiRvw1H5miNieJeJ/FNUjgH0BmVRWII6+T4MnDwmCMZUI/orxP3HGwYCSIvyzS3MpmmSe4iaWKCOHQ== ``` # 在 HTTPS 中,TLS/SSL 如何加密 HTTP 请求和响应 TLS 使用一种称为公钥加密的技术:密钥有两个,即公钥和私钥,其中公钥通过服务器的 SSL 证书与客户端设备共享。当客户端打开与服务器的连接时,这两个设备使用公钥和私钥商定新的密钥(称为会话密钥),以加密它们之间的后续通信。 然后,所有 HTTP 请求和响应都使用这些会话密钥进行加密),使任何截获通信的人都只能看到随机字符串,而不是明文。 有关加密和密钥的工作原理的更多信息,请参阅什么是加密? # HTTPS 如何帮助验证 Web 服务器身份 身份验证是指核实一个人或一台计算机是否是声称的身份。HTTP 中没有身份验证,它基于信任原则。HTTP 的架构师不一定是做出了隐式信任所有 Web 服务器的决定;他们当时除了安全以外还有其他优先事务。但在现代 Internet 上,身份验证是不可或缺的。 就像身份证件能确认一个人的身份一样,私钥可以确认服务器的身份。当客户端打开与源站服务器的连接通道时(例如,当用户导航到网站时),拥有与网站 SSL 证书中公钥匹配的私钥可证明此服务器确实是该网站的合法主机。这可以防止或帮助阻止在没有身份验证时可能发生的多种攻击. 此外,SSL 证书由签发它的证书颁发机构进行数字签名。这可以确认服务器就是它声称的身份。 # 举例 为了更鲜明得展示两者的区别,我们使用抓包工具Wireshark,分别抓取http请求和https请求。 ![http请求.png](https://47.116.196.61/i/2024/11/22/67401e4446047.png) ![https请求.png](https://47.116.196.61/i/2024/11/22/67401e84dd082.png) 我们可以看到,当使用http请求时,我们可以在Wireshark面板看到请求体中的明文信息,当我们使用https请求时,所有的请求体都是加密的。 ![微信图片_20240421104020.jpg](https://47.116.196.61/i/2024/11/20/673d66e182758.jpg) 这两天折腾了图床、博客还有学了会日语。我觉得这种状态挺好的,折腾图床的过程中学习了使用https提供加密的服务,原来可以通过修改nginx的配置来重定向http请求到https。 ```json # HTTP服务器配置(重定向到HTTPS) server { listen 80; server_name your_domain.com; # 替换成你的域名 # 将所有HTTP请求重定向到HTTPS return 301 https://$server_name$request_uri; } # HTTPS服务器配置 server { listen 443 ssl; server_name your_domain.com; # 替换成你的域名 # SSL证书配置 ssl_certificate /path/to/your/certificate.crt; # 替换成你的证书路径 ssl_certificate_key /path/to/your/private.key; # 替换成你的私钥路径 # SSL配置优化 ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; # SSL会话缓存 ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; # HSTS配置(可选) add_header Strict-Transport-Security "max-age=31536000" always; # 网站根目录配置 root /usr/share/nginx/html; index index.html index.htm; location / { try_files $uri $uri/ =404; } } ``` 这个是基础的使用示例,其实部署别人的服务的话,以php为例,还需要做一些处理,对于PHP服务,我们需要在Nginx配置中添加PHP-FPM的相关配置。以下是一个完整的包含PHP支持的HTTPS配置: ```json # HTTP服务器配置(重定向到HTTPS) server { listen 80; server_name your_domain.com; return 301 https://$server_name$request_uri; } # HTTPS服务器配置 server { listen 443 ssl; server_name your_domain.com; # SSL证书配置 ssl_certificate /path/to/your/certificate.crt; ssl_certificate_key /path/to/your/private.key; # SSL配置优化 ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; # HSTS配置(可选) add_header Strict-Transport-Security "max-age=31536000" always; # 网站根目录 root /var/www/html; # 修改为你的网站目录 index index.php index.html index.htm; # PHP文件处理 location ~ \.php$ { try_files $uri =404; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock; # PHP-FPM socket路径 # 或者使用TCP方式: # fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; # 安全相关头部 fastcgi_param HTTPS on; } # 隐藏文件访问限制 location ~ /\. { deny all; } # 静态文件缓存设置 location ~* \.(jpg|jpeg|gif|png|css|js|ico|xml)$ { expires 30d; add_header Cache-Control "public, no-transform"; } # 主目录配置 location / { try_files $uri $uri/ /index.php?$query_string; # 支持PHP框架的URL重写 } } ``` 这里还需要确保php-fpm正在运行 ```json # 安装PHP-FPM(以Ubuntu为例) apt-get install php-fpm # CentOS/RHEL系统: yum install php-fpm # 启动PHP-FPM服务 systemctl start php-fpm systemctl enable php-fpm ``` 检查PHP-FPM配置文件(通常在 /etc/php-fpm.d/www.conf 或 /etc/php/7.x/fpm/pool.d/www.conf): ```json ; 确保用户和组设置正确 user = nginx group = nginx ; 如果使用Unix socket listen = /var/run/php-fpm/php-fpm.sock listen.owner = nginx listen.group = nginx listen.mode = 0660 ; 如果使用TCP ; listen = 127.0.0.1:9000 ``` 设置正确的文件权限: ```json # 设置网站目录权限 chown -R nginx:nginx /var/www/html chmod -R 755 /var/www/html ``` 注意:PHP-FPM错误日志(通常在 /var/log/php-fpm/error.log) --- ```json location ~ \.php$ { # ~ \.php$ 是一个正则表达式,匹配所有以.php结尾的文件 # location 块定义了对PHP文件的处理规则 try_files $uri =404; # 检查PHP文件是否存在,如果不存在则返回404错误 # 这是一个安全措施,防止执行不存在的PHP文件 fastcgi_split_path_info ^(.+\.php)(/.+)$; # 分割URL中的路径信息 # 例如:/script.php/extra/info 会被分割成: # $fastcgi_script_name: /script.php # $fastcgi_path_info: /extra/info fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock; # 指定如何连接到PHP-FPM处理程序 # 这里使用Unix socket方式(更快,推荐在同一服务器上使用) # fastcgi_pass 127.0.0.1:9000; # 另一种连接方式是TCP(如果PHP-FPM在不同服务器上,就用这个) fastcgi_index index.php; # 设置默认的PHP文件名 # 当访问目录时,默认寻找index.php fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; # 设置PHP脚本的实际文件路径 # $document_root 是网站根目录 # $fastcgi_script_name 是PHP文件名 # 例如:/var/www/html/index.php include fastcgi_params; # 包含Nginx预定义的FastCGI参数 # 这个文件通常在 /etc/nginx/fastcgi_params # 包含了许多必要的环境变量 fastcgi_param HTTPS on; # 告诉PHP脚本当前是HTTPS连接 # 这样PHP可以正确识别安全连接 } ``` 这里有一些实际的例子: 当访问 https://your-site.com/index.php 时: - Nginx 匹配到这是一个 .php 文件 - 检查文件是否存在(try_files) - 如果存在,通过 socket 或 TCP 传递给 PHP-FPM 处理 - PHP-FPM 执行这个文件并返回结果给 Nginx - Nginx 再将结果发送给用户 部署时的问题排查: ```json # 1. 检查PHP-FPM是否运行 systemctl status php-fpm # 2. 检查socket文件是否存在 ls -l /var/run/php-fpm/php-fpm.sock # 3. 检查日志文件权限 ls -l /var/log/php-fpm/error.log # 4. 测试PHP文件权限 ls -l /var/www/html/index.php ```