[{"content":"这是一个独立页面入口。\n你可以把这里当作：\n个人介绍 项目导航 赞助与联系方式 任何你想固定展示、不按时间流排序的内容 ","permalink":"https://blog.dontalk.org/single/","summary":"这是一个独立页面入口。\n你可以把这里当作：\n个人介绍 项目导航 赞助与联系方式 任何你想固定展示、不按时间流排序的内容 ","title":"单独页面"},{"content":"这是一种用于保证系统安全性和提升用户体验的现代认证方案。\n🔑 核心概念：Access Token 与 Refresh Token 传统的会话（Session）机制依赖服务器状态，而 Token 机制是无状态的。为了平衡安全性和用户体验，我们引入了两种 Token：\n特性 Access Token (存取令牌) Refresh Token (续签令牌) 用途 访问受保护的 API 资源（如获取用户资料、创建文章）。 换取新的 Access Token，避免用户重复登录。 有效期限 短效 (通常 15 分钟到 1 小时)。 长效 (通常 7 天到 30 天)。 存储位置 客户端应安全存储（如内存、HTTP-only Cookie）。 客户端应安全存储，并存储在 Redis 或数据库 中。 安全性 泄露损失小，因为很快失效。 泄露损失大，必须严格保护，可用于撤销。 🛠️ 两种 Token 的工作流程 整个认证流程分为三个主要步骤：登录、资源访问和续签。\n1. 登录 (POST /api/auth/login) 这是用户获取两种 Token 的起点：\n步骤 角色 描述 1. 用户/客户端 发送用户名和密码到 /api/auth/login。 2. 服务器 (Auth) 验证凭证，生成 短效 Access Token (AT) 和 长效 Refresh Token (RT)。 3. 服务器 (Auth) 将 RT 存储到 Redis 中，并以用户 ID 为 Key。 4. 服务器 (Auth) 将 AT 和 RT 一起返回给客户端。 5. 客户端 安全地存储 AT 和 RT。 2. 资源访问 (访问受保护的 API) 这是 AT 的主要用途：\n步骤 角色 描述 1. 客户端 在 HTTP Header (Authorization: Bearer ) 中携带 Access Token。 2. 服务器 (Middleware) authenticateToken 中间件验证 AT 的签名和是否过期。 3.a 成功 AT 有效，允许访问资源。 3.b 失败 AT 过期或无效（HTTP 403 Forbidden），客户端需要进入续签流程。 3. Token 续签 (POST /api/auth/refresh-token) 当 Access Token 过期时，客户端需要使用 Refresh Token 来悄悄地获取新的 Access Token，避免用户被强制登出。\n步骤 角色 描述 1. 客户端 发送 Refresh Token (RT) 到 /api/auth/refresh-token。 2. 服务器 (Auth) 验证 RT 的签名和过期时间。 3. 服务器 (Auth) 🔑 关键检查：查询 Redis，确认该 RT 是否与存储的 Token 匹配，且没有被撤销。 4. 服务器 (Auth) 如果有效，生成 新的 Access Token (NEW AT) 和 新的 Refresh Token (NEW RT)。 5. 服务器 (Auth) 替换 Redis 中的 RT（使用 NEW RT 覆盖旧的 RT）。 6. 服务器 (Auth) 返回 NEW AT 和 NEW RT 给客户端。 7. 客户端 用新的 AT 和 RT 替换本地存储的旧 Token。 💻 如何在项目中使用 A. 客户端 (前端/App) 如何使用 登录后：拿到 AT 和 RT 后，立即将它们存储起来。\nAT：用于 API 调用。 RT：通常存储在 Local Storage 或更安全的 HTTP-only Cookie 中。 API 请求：所有受保护的请求（如 /api/user/profile）都必须在请求头中加入 Authorization: Bearer \u0026lt;AT\u0026gt;。\nToken 过期处理：\n当 API 返回 403/401 错误，且错误信息显示 AT 过期时。 客户端自动向 /api/auth/refresh-token 发送存储的 RT。 如果续签成功，用 NEW AT 和 NEW RT 替换旧的，并重新发送原始请求。 B. 服务器端 如何使用 在 routes/auth.js 中：\nRedis 存储 (持久化)：使用 saveRefreshToken(user.id, refreshToken) 将 RT 存储在 Redis 中，这是实现撤销的基础。\n/login 路由：\nconst accessToken = generateAccessToken(user); // 短效 const refreshToken = generateRefreshToken(user); // 长效 await saveRefreshToken(user.id, refreshToken); // 返回 { accessToken, refreshToken, ... } /refresh-token 路由 (验证和轮换)： const storedToken = await getRefreshToken(userId); if (!storedToken || storedToken !== refreshToken) { // ❌ 如果不匹配，说明是无效或已撤销的 Token return res.status(403); } // ... 生成 NEW AT/RT await saveRefreshToken(userId, newRefreshToken); // 覆盖旧的 RT /logout 路由 (撤销)： await deleteRefreshToken(req.user.userId); // 从 Redis 中删除 RT // 客户端下次尝试续签就会失败，实现真正的登出 ","permalink":"https://blog.dontalk.org/posts/access-token-%E5%92%8C-refresh-token-%E5%AD%98%E5%8F%96%E4%BB%A4%E7%89%8C%E5%92%8C%E7%BB%AD%E7%AD%BE%E4%BB%A4%E7%89%8C-%E6%9C%BA%E5%88%B6/","summary":"这是一种用于保证系统安全性和提升用户体验的现代认证方案。\n🔑 核心概念：Access Token 与 Refresh Token 传统的会话（Session）机制依赖服务器状态，而 Token 机制是无状态的。为了平衡安全性和用户体验，我们引入了两种 Token：\n特性 Access Token (存取令牌) Refresh Token (续签令牌) 用途 …","title":"Access Token 和 Refresh Token (存取令牌和续签令牌) 机制"},{"content":"在HTTP请求时候，或者说写后端时，就会经常接触到这个 application/json和 multipart/form-data ，但是有一段时间里，我都不是很能搞清楚和分清楚它们具体的应用场景，只是模糊地使用着二者，频繁地使用着前者。\n正文 它们的区别挺大的，主要体现是在 数据结构 和 场景 上(这不是废话吗)\n我们先来说 JSON(application-json)\nApplication-JSON 请求方式 JSON（JavaScript Object Notation）是一种轻量级的数据交换格式。\n主要的特点是数据结构化，方便 后端/前端 的 发送/接受，发送的数据很轻量级，所以对网络的要求会比较低，常用于登陆时传送 username 和 password。\n{ \u0026#34;username\u0026#34;: \u0026#34;hello\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;world\u0026#34; } 非常直观吧？这也是JSON的优点之一，直观，很符合人类的阅读习惯，左边对应右边。\n无论是前端还是后端，都可以很方便地赋值和取用数据。\n但是缺点也很明显，就是它不能传输二进制文件(如压缩包、图片等等)，它只是对文本传输非常友好\nmultipart/form-data 请求方法 此方法的最大优点估计就是可以支持二进制文件传输了(如压缩包、图片等)，这也是互联网文件上传时不得不用的一个方法，因为Application-JSON并不支持上传二进制文件。\n但是，有一些网站可能会在 POST 文章的时候，直接采用 multipart/form-data 方法，图片、文章、文件一通上传到服务器API，这样做是情有可原的，但是 multipart/form-data 阅读起来并不直观，它看起来是这样的(AI生成)\nContent-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWgDLT0ADGvT // ========================================================= // 第一部分：普通文本字段 (title) // 边界符开始，注意前面有两个破折号 ------WebKitFormBoundary7MA4YWgDLT0ADGvT Content-Disposition: form-data; name=\u0026#34;title\u0026#34; // Content-Type: text/plain（默认可省略） 我的文章标题 // ========================================================= // 第二部分：二进制文件字段 (cover_image) // 边界符继续分隔 ------WebKitFormBoundary7MA4YWgDLT0ADGvT Content-Disposition: form-data; name=\u0026#34;cover_image\u0026#34;; filename=\u0026#34;photo.jpg\u0026#34; Content-Type: image/jpeg // \u0026lt;--- 明确指出这一部分是图片 // 这里是图片的原始二进制数据流（人眼不可读） // 比如：ÿØÿà JFIF ... [许多乱码或编码后的数据]... // ========================================================= // 请求体结束 // 结束边界符，注意后面多跟了两个破折号 ------WebKitFormBoundary7MA4YWgDLT0ADGvT-- 这并不符合阅读习惯，但是确实有这样的情况存在。但是非常不优雅。\n现代网页开发 如何混合使用它们 现代网页开发，发布文章是一个API，上传文件是一个API，而发布文章的API接受的是 application-json ，上传文件的API接受的是 multipart-form-data。\n让我们来假设一下有两个 API，发布文章的API是 /post，上传文件的API是 /upload。\n而前端的不同模块组件，决定你去请求哪一个API，我们写一个文章，之后添加文件，点击发送。此时，你的前端判断是否选择了文件，如果有，那就把数据发送给 /upload 这个API，这个API返回一段JSON数据：\n{ \u0026#34;status\u0026#34;: \u0026#34;success\u0026#34;, \u0026#34;file_id\u0026#34;: \u0026#34;9a38f0-b8c2-4d1e-9a1f\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://cdn.example.com/assets/articles/9a38f0.jpg\u0026#34; } 是的，虽然它接受 multipart-form-data，但是返回的却是 application-json(JSON)，这个时候，你的前端可以把这个返回的JSON数据(包含文件名和PATH)添加进主要的发送体里面，一起发送给 /post 这个API。\n// 这是准备发送给 /post 的 JSON 结构 { \u0026#34;title\u0026#34;: \u0026#34;JSON与FormData的混合使用\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;这是文章的主要文本内容。\u0026#34;, \u0026#34;author_id\u0026#34;: 101 // 在这里，前端将 /upload 返回的数据嵌入 \u0026#34;attachments\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;image\u0026#34;, \u0026#34;source_url\u0026#34;: \u0026#34;https://cdn.example.com/assets/articles/9a38f0.jpg\u0026#34; } ] } 这个方法的优点是可以尝试重新上传图片，可以让后端解藕，更好维护，可以让数据更加直观\u0026hellip;这是现代网页开发最常用的方法，也是最优雅的方法。\n","permalink":"https://blog.dontalk.org/posts/http%E8%AF%B7%E6%B1%82-application-json-%E5%92%8C-multipart-form-data-%E7%9A%84%E4%B8%8D%E5%90%8C/","summary":"在HTTP请求时候，或者说写后端时，就会经常接触到这个 application/json和 multipart/form-data ，但是有一段时间里，我都不是很能搞清楚和分清楚它们具体的应用场景，只是模糊地使用着二者，频繁地使用着前者。\n正文 它们的区别挺大的，主要体现是在 数据结构 和 场景 上(这不是废话吗)\n我们先来说 …","title":"HTTP请求 application Json 和 multipart Form Data 的不同"},{"content":"很多东西，在如今，都由各大寡头公司垄断。\n但是，依旧有很多人选择 “自己种田，自己自足” 。因以摆脱寡头和付费套餐的监控与枷锁。\n我有一台公网的服务器，一台内网的主机。如何使用Frp快速地穿透到外网去呢？\n正文 你需要一台拥有公网地址的主机，无论是Windows，Linux还是Mac，甚至是安卓都好，只要官方有支持就行。(Frps服务端和Frpc客户端都在一个压缩包里)\n下载地址 Github 官方项目地址：https://github.com/fatedier/frp\n官方文档地址：https://gofrp.org/zh-cn/\nfrp的功能很多很丰富，但本文只专注于内网穿透，ssh公网连接到内网，和网页穿透这一点。\nFrps 服务端 下载与配置 由于我是 Linux – x86(amd64) 的服务器，所以我就在服务器去下载对应的 frp_0.64.0_linux_amd64.tar.gz。(要分清楚你的架构是什么，目前x86平台，基本都是amd64的服务器-64位)。\n# 注意版本号 wget https://github.com/fatedier/frp/releases/download/v0.64.0/frp_0.64.0_linux_amd64.tar.gz # 下载 tar -zxvf frp_0.64.0_linux_amd64.tar.gz # 解压 mv frp_0.64.0_linux_amd64 frp # 目录不好记名字，改个名字 cd frp # 进入目录 rm -rf LICENSE frpc frpc.toml # 删除客户端相关文件 完成后，ls 看看，估计就剩下 frps，frps.toml 两个文件了。\n我们编辑 frps.toml 文件：vim frps.toml\n#bindPort = 7000 bindPort = 7020 # frp沟通端口，建议修改。有防火墙的话，记得放行。 # 服务面板可查看frp服务状态信息 webServer.addr = \u0026#34;0.0.0.0\u0026#34; # 后台管理地址,默认是127.0.0.1，如果是公网访问则改成0.0.0.0，否则可能绑定不上公网IP webServer.port = 7050 # 后台管理端口(浏览器上，IP+此端口 访问Web管理页) webServer.user = \u0026#34;admin\u0026#34; # （可选）Web后台登录用户名 webServer.password = \u0026#34;JbkIp7mzrjJK\u0026#34; # （可选）Web后台登录密码 #transport.tls.force = true # 服务端将只接受 TLS链接 auth.method = \u0026#39;token\u0026#39; # 客户端访问验证方式 auth.token = \u0026#34;cxrCISjm\u0026#34; # 客户端访问验证密码(自己取)，frpc要与frps一致，不然不让连接 # 自定义的监听的端口，所有对服务器该端口访问将被转发到本地内网，做了反向代理可不处理防火墙放行 #vhostHTTPPort = 8000 #vhostHTTPSPort = 45443 启动 frps 建议第一次启动，先前台启动，可以看到连接日志。通过下面命令可以启动：\n./frps -c ./frps.toml # ./frps -c ./frps.toml \u0026amp; # 后台启动，ssh关闭后依旧可用。但不建议，不好管理。 守护进程在文尾\nFrpc 客户端 (服务器放行相关端口了吗？) 我的内网客户端是一台 Arm-x64 架构的 Linux系统 开发版，运行了一个网页。我希望穿透网页和ssh。(要分清楚你的架构是什么，同时，arm也分32/64位)。\n下载与配置 下载太慢的话，可以用代理的电脑下载完，之后把文件scp上去没有代理的主机。\nwget https://github.com/fatedier/frp/releases/download/v0.64.0/frp_0.64.0_linux_arm64.tar.gz # 下载 tar -zxvf frp_0.64.0_linux_arm64.tar.gz # 解压 mv frp_0.64.0_linux_arm64 frp # 目录不好记名字，改个名字 cd frp # 进入目录 rm -rf LICENSE frps frps.toml # 删除服务端相关文件 完成后，ls 看看，估计就剩下 frpc，frpc.toml 两个文件了。\n我们编辑 frpc.toml 文件：vim frpc.toml\nserverAddr = \u0026#34;xxx.xxx.xxx.xxx\u0026#34; # 这里填服务器Frps的IP serverPort = 7020 # 这里填服务器的Frp沟通端口 auth.method = \u0026#39;token\u0026#39; # 服务端设置的验证方法 auth.token = \u0026#39;cxrCISjm\u0026#39; # 服务端设置的密码 [[proxies]] # 标准写法，下面是一个块 name = \u0026#34;ssh\u0026#34; # 在服务器看到的连接信息 type = \u0026#34;tcp\u0026#34; # 数据类型 localIP = \u0026#34;127.0.0.1\u0026#34; # 本主机IP，也就是127.0.0.1(穿透自己) localPort = 22 # SSH 端口 remotePort = 6000 # 让服务器给 6000 这个端口“我”用 [[proxies]] name = \u0026#34;web\u0026#34; type = \u0026#34;tcp\u0026#34; localIP = \u0026#34;127.0.0.1\u0026#34; localPort = 80 # Nginx 默认的网页端口，如果你有多个网页，可以用不同端口。 # 或者干脆docker(docker暴露的端口每个都不一样) remotePort = 6001 启动 frpc 建议第一次启动，先前台启动，可以看到连接日志。通过下面命令可以启动：\n./frpc -c ./frpc.toml # ./frpc -c ./frpc.toml \u0026amp; # 后台启动，ssh关闭后依旧可用。但不建议，不好管理。 我没有启动内网Web页面的穿透，只穿透了ssh\nFrps 服务端日志\nFrps 服务端 Web\nFrpc 客户端日志\n守护进程 但是上面的方法并不稳定，所以我们需要分别给服务端和客户端书写一个守护进程。\nFrps 服务器 vim /etc/systemd/system/frps.service [Unit] Description = frp server # 服务名称，可自定义 After = network.target syslog.target Wants = network.target [Service] Type = simple # 启动命令，改为你实际存放frps的路径，但是一般默认这个目录 ExecStart = /root/frp/frps -c /root/frp/frps.toml [Install] WantedBy = multi-user.target 保存后，我们重载配置文件: systemctl reload\n之后设置开机启动: systemctl enable --now frps\n看看状态: systemctl status frps\n可以看到，服务正常启动\n之后客户端也成功再次连接上了\nFrpc 客户端 (服务器放行相关端口了吗？) 我们需要为客户端也写一个守护进程，和上面差不多，做一些小改变即可。\nvim /etc/systemd/system/frpc.service [Unit] Description = frp client After = network.target syslog.target Wants = network.target [Service] Type = simple # 这里需要修改 frps为c ExecStart = /root/frp/frpc -c /root/frp/frpc.toml [Install] WantedBy = multi-user.target 保存后，我们重载配置文件: systemctl reload\n之后设置开机启动: systemctl enable –now frpc\n看看状态: systemctl status frpc\n成功：\n测试 (服务器放行相关端口了吗？) ssh 之后我们连接这个ssh试试，也就是上面设置的 6000 端口\n服务器IP + 6000 ，就可以连接上内网这台服务器的ssh了\n成功：\nWeb 刚刚我们假设了一个网页，配置如下\n... [[proxies]] name = \u0026#34;web\u0026#34; type = \u0026#34;tcp\u0026#34; localIP = \u0026#34;127.0.0.1\u0026#34; localPort = 80 # Nginx 默认的网页端口，如果你有多个网页，可以用不同端口。 # 或者干脆docker(docker暴露的端口每个都不一样) remotePort = 6001 使用了服务器的6001端口，如果你想访问这个网页，就只有在浏览器访问 服务器IP+6001 了。所以我们其实可以做一个反向代理，在服务器上面。\napt install nginx # 你的服务器有nginx吗？ vim /etc/nginx/conf.d/web.conf 配置如下：\nserver { listen 80; # 你的域名，或者你服务器的 IP 地址 server_name xxx.com; # 反向代理块 location / { # 将请求转发到你后端应用的地址和端口，也就是本地IP+6001 proxy_pass http://localhost:6001; # 保持客户端的真实IP地址，这样后端日志会更准确 proxy_set_header X-Real-IP \u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;remote_addr; proxy_set_header X-Forwarded-For\u0026lt;/span\u0026gt;proxy_add_x_forwarded_for; } } 之后检查配置文件没问题就重载nginx即可\nnginx -t nginx -s reload 之后就可以通过域名访问此“内网网页”了。至于证书申请，就不赘述了，看我之前的文章。\n参考 官方文档 – https://gofrp.org/zh-cn/\nisedu-Blog – https://isedu.top/index.php/archives/278/\niyatt-Blog – https://blog.iyatt.com/?p=17340\n","permalink":"https://blog.dontalk.org/posts/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8frp%E8%BF%9B%E8%A1%8C%E5%86%85%E7%BD%91%E7%A9%BF%E9%80%8F%E8%87%AA%E5%BB%BA%E7%A9%BF%E9%80%8F%E6%9C%8D%E5%8A%A1%E5%99%A8frp%E9%85%8D%E7%BD%AEtoml%E6%96%87%E4%BB%B6%E7%A9%BF%E9%80%8F%E6%9C%AC%E5%9C%B0%E7%9A%84ssh%E5%92%8C%E7%BD%91%E7%AB%99/","summary":"很多东西，在如今，都由各大寡头公司垄断。\n但是，依旧有很多人选择 “自己种田，自己自足” 。因以摆脱寡头和付费套餐的监控与枷锁。\n我有一台公网的服务器，一台内网的主机。如何使用Frp快速地穿透到外网去呢？\n正文 你需要一台拥有公网地址的主机，无论是Windows，Linux还是Mac，甚至是安卓都好，只要官方有支持就行。(Frps服务端和Frpc客户端都在一 …","title":"如何使用Frp进行内网穿透，自建穿透服务器，frp配置toml文件。穿透本地的ssh和网站。"},{"content":"现代的网页开发，如何快速开发出一个功能齐全，并且好看又实用的网页呢？\n我们曾经经常提到 CSS框架 ，但它到底是怎么样的一个东西？\n正文 – CSS框架(组件库)的正确用法 CSS框架 事实上就是给你快速构建网站用的，也包括功能。比如说CSS框架提供了一个导航栏，你只需要在他们官网把代码复制下来，之后更改关键点就好了。比如说下面这个框架，就提供了很多功能\nTailwindCSS 和 BootStrap5(引入) 比较频繁使用类名，但是如果希望快速开发网页的话，并且代码不混乱(很多类名)，CSS框架的精髓就应该是标签。如下面的css，如果我们需要卡片样式，就引入并使用 \u0026lt;Card\u0026gt; 标签，他们提供一些属性来DIY。这一点 AntDesign 做得很好\nAntDesign – https://ant.design/\nui.shadcn – https://ui.shadcn.com/docs/\n例如，我们想要构建一个 图片轮询/个人信息 页面，只需要把 CSS框架 里面提供的组件复制到本地，之后写 fetch 从后端取得数据，填入 CSS框架的“萝卜坑” 里就行了。\n现代网页开发就是那么简单。\n派别之争 但这其实是一个派别之争，我在 现代网页开发 教程里面书写的习惯是 – 类名 – 功能优先(Utility-First) 而本文的习惯是 标签 – 组件优先(Component-First)。\n如果你希望你的代码不要出现太多类名导致混乱，并且更加快速地构建一个完美，好看的页面，那就可以试试 标签 – 组件优先(Component-First)。\n你也可以亲切将这种 CSS框架 称之为 CSS组件库。\nTailwind CSS：核心思想是Utility-First（功能优先）。它的精髓在于类名。你不需要 Card 标签，只需要使用一系列原子化的类名（如 flex, bg-white, rounded-lg）来直接构建卡片。你无法直接使用 \u0026lt;Card\u0026gt; 标签，你需要自己组合类名来创建卡片样式。\nBootstrap 5：介于两者之间，但更偏向 组件优先。它提供了像 .card、.btn 这样的组件类，你只需要在你的 \u0026lt;div\u0026gt; 或 \u0026lt;button\u0026gt; 上添加这些类，就可以得到一个预设样式的组件。它也有一些功能类（如 d-flex），但其核心仍然是组件类。\nAnt Design：核心思想是 Component-First（组件优先）。它的精髓在于标签。它提供了像 \u0026lt;Card\u0026gt;、\u0026lt;Button\u0026gt; 这样的 React 组件。你不需要关心背后的 CSS 类名，只需要引入这些组件，然后通过它们的属性（props）来定制样式和行为，例如 \u0026lt;Card title=\u0026quot;My Card\u0026quot;\u0026gt;。\n开发流程 找到合适的模块/组件：从 UI 框架或组件库中，找到你需要的模块或组件（如导航栏、表单、表格、按钮）。\n复制粘贴：将这些组件的代码粘贴到你的项目中。这些组件通常已经包含了样式和基础功能。\n获得数据：在组件内部，使用 fetch 或其他 HTTP 客户端库（如 Axios）向后端发起请求，获取数据。\n填充数据：将从后端获取的数据，通过 props 或状态管理，动态地填充到组件的相应部分。\n你会得到什么？\n这个流程让你能够专注于业务逻辑和数据流，而不用花费大量时间在 CSS 样式和基础组件的构建上。你可以把更多精力放在如何获取和展示数据，而不是如何让按钮居中或者如何设计一个美观的功能列表。\n这正是 CSS 框架(组件库)受欢迎的原因\n","permalink":"https://blog.dontalk.org/posts/%E7%8E%B0%E4%BB%A3%E7%BD%91%E9%A1%B5%E5%BC%80%E5%8F%91-%E5%89%8D%E7%AB%AF%E4%B8%8Ecss%E6%A1%86%E6%9E%B6%E7%9A%84%E5%85%B3%E7%B3%BB%E5%A6%82%E4%BD%95%E5%BF%AB%E9%80%9F%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E5%A5%BD%E7%9C%8B%E5%AE%9E%E7%94%A8%E7%9A%84%E9%A1%B5%E9%9D%A2css%E6%A1%86%E6%9E%B6%E7%9A%84%E6%AD%A3%E7%A1%AE%E7%94%A8%E6%B3%95/","summary":"现代的网页开发，如何快速开发出一个功能齐全，并且好看又实用的网页呢？\n我们曾经经常提到 CSS框架 ，但它到底是怎么样的一个东西？\n正文 – CSS框架(组件库)的正确用法 CSS框架 事实上就是给你快速构建网站用的，也包括功能。比如说CSS框架提供了一个导航栏，你只需要在他们官网把代码复制下来，之后更改关键点就好了。比如说下面这个框架，就提供了很多功能 …","title":"现代网页开发 – 前端与CSS框架的关系。如何快速开发一个好看实用的页面，CSS框架的正确用法。"},{"content":"React路由在本地开发时候正常，但是编译部署在服务器后，会发现直接访问这个网站的某个路由，或者在某个路由刷新页面后会报错404。\n其实这是一个比较典型的问题：Nginx 服务器配置未能正确处理前端的客户端路由。\n背景说明(请认真看完)：域名是 dontalk.org，后端路由直接通过编辑这个网站的 Nginx配置文件，把 dontalk.org/api/ 定向到我的后端 127.0.0.1:3456 (后端代码中定义)。访问前端页面，前端直接从 /api/xxx 里面取数据。\n在本地开发时，前端直接写明白向后端 “http://localhost:3000/xxx” 取数据的。编译前我改成了 /api/xxx 这个相对地址。因为通过反向代理，已经把这个页面的 /api/ 请求，指向了后端 /。记得，一定是 /api/，因为 /api 会导致 GET 到 /api//xxx。\n正文 当我直接访问 / 时，并没有报错，在 / 中点击某个页面，进去那个页面(路由)，也正常。但是，直接通过URL访问某个页面(路由)，却报错 404。这个 404 甚至不是我写的页面，而是很简陋，很像 Nginx 提供的页面(事实上就是)。\n这个时候去看 Nginx 错误日志，会发现记录有一条：\n...open() ＂/var/wwwroot/dontalk.org/xxx＂ failed (2: No such file or directory), ... 没错，这就是 Nginx 的锅，它没有正确解析我们的路由，所以导致我们在 页面刷新、直接通过URL访问 就报错。甚至给人一种 “是这个路由页面不工作，是代码写错了” 的错觉。\n解决 其实解决方案也简单到令人不可置信，你只需要在你的 Nginx (该页面)配置里面增加短短三行内容，整个网站就正常了。无论是刷新还是直接URL访问，都正常了。\nlocation / { try_files $uri $uri/ /index.html; } 保存，检查语法，重启Nginx，就那么简单。\n为什么会这样？ ...open() ＂/var/wwwroot/dontalk.org/xxx＂ failed (2: No such file or directory), ... 表明 Nginx 尝试访问 /xxx 路径对应的物理文件或目录，但未能找到，导致 404 错误。这是因为 React 应用使用客户端路由 (react-router-dom)，而 Nginx 未正确配置为单页应用 (SPA) ，所以无法将 /xxx 等路由请求重定向到 index.html。\n","permalink":"https://blog.dontalk.org/posts/react%E8%B7%AF%E7%94%B1%E5%9C%A8%E6%9C%AC%E5%9C%B0%E6%AD%A3%E5%B8%B8%E8%AE%BF%E9%97%AE%E7%BC%96%E8%AF%91%E9%83%A8%E7%BD%B2%E5%9C%A8%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%8A%A5%E9%94%99404%E4%B8%8D%E6%AD%A3%E5%B8%B8react%E9%A1%B5%E9%9D%A2%E5%88%B7%E6%96%B0%E5%90%8E%E6%8A%A5%E9%94%99404react%E9%A1%B5%E9%9D%A2%E7%9B%B4%E6%8E%A5%E8%AE%BF%E9%97%AE%E6%8A%A5%E9%94%99404/","summary":"React路由在本地开发时候正常，但是编译部署在服务器后，会发现直接访问这个网站的某个路由，或者在某个路由刷新页面后会报错404。\n其实这是一个比较典型的问题：Nginx 服务器配置未能正确处理前端的客户端路由。\n背景说明(请认真看完)：域名是 dontalk.org，后端路由直接通过编辑这个网站的 Nginx配置文件，把 dontalk.org/api/ …","title":"React路由在本地正常访问，编译部署在服务器报错404(不正常)，React页面刷新后报错404，React页面直接访问报错404。"},{"content":"我决定使用，纯血 Arch Linux ! 但是我遇到了，很尴尬的问题，就是我安装好的 Arch Linux 根本没有网络，但是安装的时候，却有(通过iwctl连接)网络，安装好却怎么样都连不上。\n前言 对于 Arch 的安装，如果你插线了，那直接开箱就能安装。如果使用的是 Wi-Fi 网络，就需要通过 iwctl 命令来连接网络。\n下面展示一些基础的 iwctl 命令：\niwctl # 进入 iwctl 交互式命令行 device list # 列出无线网卡设备名，比如无线网卡看到叫 wlan0 station wlan0 scan # 扫描网络 station wlan0 get-networks # 列出所有 wifi 网络 station wlan0 connect wifi-name # 进行连接，注意这里无法输入中文。回车后输入密码即可（可以Tab补全） exit # 连接成功后退出 iwctl ping dontalk.org # 测试是否有返回 - 判断网络是否有连接 解决方法 当 Arch Linux 安装完毕后，无论是手动安装，还是 Archinstall 安装索引，都记得千万不要马上选择重启到系统。\nArch Linux 安装完成后，如果你需要从 Live 环境（比如安装 U 盘）或者从另一个已安装的 Linux 系统中 chroot 进入你新安装的 Arch Linux 系统来安装软件、修复系统或进行配置，这是一个非常常见的操作。\n否则再 chroot 进系统会很麻烦，你需要挂载各个分区 – 详见文章末尾。\nArch 提供了一个一键挂载的工具：arch-chroot,只需要 arch-chroot /mnt 即可。\n在安装完毕后，你仍旧处于 chroot 的情况下。(或者 Archinstall 安装完毕后选择 Chroot 到新系统，而不是重启)\n我们需要安装两个东西：1.dhcpcd，2.iwd\ndhcpcd 是为了 DHCP 分配 IP。\niwd 是为了可以使用 iwctl 命令连接 Wi-Fi\n在 连接网络 后的 安装完毕/救援Live 时候 Chroot 到新系统，使用下面命令安装：\nping dontalk.org # 测试是否有返回 - 判断网络是否有连接 pacman -S dhcpcd pacman -S iwd 安装完毕后，通过下面命令设置为开机自启动即可：\nsystemctl enable dhcpcd systemctl enable iwd 之后再重启进系统，通过 iwctl 即可完成联网。\niwctl 基本命令看文章上面。下面有更多的选择：\n但是上面的选择并非唯一，如果你想卸载上面，试试下面，可以把 pacman 参数 -S 换为 -Rns 来卸载.\nNetworkManager：对于桌面用户来说，NetworkManager 是更常用和推荐的网络管理工具，它提供了更便捷的命令行工具（nmcli）和各种桌面环境的图形前端。它通常会自动处理 Wi-Fi 和有线连接，并且包含了 DHCP 客户端功能。\nsystemd-networkd：systemd 自带的网络配置工具，对于需要更精细控制或使用 systemd 生态系统的用户来说是一个强大的选择。\nnetctl：Arch Linux 官方维护的另一个网络配置文件管理器，可以与 wpa_supplicant 配合使用。\n手动挂载入 Chroot Arch 官方推荐的 arch-chroot 工具会自动处理 /dev、/proc、/sys 的绑定挂载以及 resolv.conf 的复制，大大简化了过程。如果不是为了学习或特定需求，使用 arch-chroot 会更方便和不容易出错。\n就好比 手动安装 Arch Linux，有不少人拒绝使用 Archinstall 安装索引，当然可能也会有人拒绝使用 Arch-chroot 自动挂载命令。\n所以，这里说说如何 手动挂载入 Chroot。（使用Arch手动安装证明你有一定Linux能力，下面内容不可照搬，只可参考）\n步骤 1: 读分区 lsblk 步骤 2: 挂载根分区 假设你的根分区是 /dev/sda1：\nmount /dev/sda1 /mnt 步骤 3: 挂载其他必要分区 (如果存在独立分区) 如果你的 /boot 是一个单独的分区（例如 /dev/sda2）：\nmkdir /mnt/boot mount /dev/sda2 /mnt/boot 如果你的 /home 是一个单独的分区（例如 /dev/sda3）：\nmkdir /mnt/home mount /dev/sda3 /mnt/home 如果你使用了 EFI 系统分区 (ESP)（例如 /dev/sda0，通常挂载到 /boot/efi）：\nmkdir /mnt/boot/efi mount /dev/sda0 /mnt/boot/efi 步骤 4: 挂载虚拟文件系统 在 chroot 之前，你需要将当前运行环境的一些虚拟文件系统（如 /proc、/sys、/dev）挂载到新根目录 (/mnt) 下的对应位置，这是 chroot 环境正常运行所必需的。\nmount --bind /dev /mnt/dev # 设备文件系统 mount --bind /proc /mnt/proc # 进程信息文件系统 mount --bind /sys /mnt/sys # 系统信息文件系统 –bind 选项创建了一个绑定挂载（或称作“循环挂载”），它实际上是将一个目录树挂载到另一个目录树上。这里是将宿主系统的 /dev、/proc、/sys 目录“镜像”到 /mnt/dev、/mnt/proc、/mnt/sys 上。\n步骤 5: 复制 resolv.conf 以启用网络 resolv.conf 文件包含了 DNS 服务器的地址，没有它，你的 chroot 环境就无法解析域名\ncp /etc/resolv.conf /mnt/etc/resolv.conf 步骤 6: 执行 chroot 命令 chroot /mnt /bin/bash chroot /mnt：将根目录切换到 /mnt。\n/bin/bash：指定在新的 chroot 环境中要启动的 shell 程序。通常是 Bash，如果你习惯用 Zsh，也可以用 /bin/zsh，但前提是新系统里已经安装了 Zsh。 最后: 卸载分区 umount -R /mnt 如果遇到 “target is busy” 的错误，这证明有进程还在使用这些挂载点。你可以尝试等待片刻或使用 lsof | grep /mnt 来查找占用进程并杀死它们，或者直接重启。\n","permalink":"https://blog.dontalk.org/posts/archlinux%E5%AE%89%E8%A3%85%E5%AE%8C%E6%AF%95%E5%90%8E%E6%B2%A1%E5%8A%9E%E6%B3%95%E4%BD%BF%E7%94%A8%E7%BD%91%E7%BB%9C%E5%AE%89%E8%A3%85%E6%97%B6%E6%AD%A3%E5%B8%B8%E4%BD%BF%E7%94%A8%E7%BD%91%E7%BB%9C/","summary":"我决定使用，纯血 Arch Linux ! 但是我遇到了，很尴尬的问题，就是我安装好的 Arch Linux 根本没有网络，但是安装的时候，却有(通过iwctl连接)网络，安装好却怎么样都连不上。\n前言 对于 Arch 的安装，如果你插线了，那直接开箱就能安装。如果使用的是 Wi-Fi 网络，就需要通过 iwctl 命令来连接网络。\n下面展示一些基础的 …","title":"ArchLinux安装完毕后，没办法使用网络。(安装时正常使用网络)"},{"content":"水一篇文章。我有一台Chromebook，其大小只有32G，事实上，实际大小仅有可怜的28G，那么，【交换分区】和【交换到文件】(交换文件)该如何选择。\n前言 曾经主流派系 Linux(不是说如今放弃了)，都有设置交换分区的概念。事实上，也因为很久之前，有设置一个分区为交换分区的传统，如今不少 Linux 还保留了「交换分区」这个保守选择，稳妥选择。\n虽然交换分区在性能上可能更稳定(因为它独占一块物理空间)，但是对于一些【小存储】设备，或是想要【动态调整分区大小】的用户，非常不友好。\n所以后面出来了一个，交换分区到文件 – 也就是「交换文件」。\n铺垫了那么多，那么如今，我们如何从【交换分区】和【交换到文件】(交换文件)之间作出选择？\n主题 我对于我的Chromebook，使用了【交换到文件】，因为我的Chromebook只有小得可怜的28G存储。如果的硬盘大一些，我依旧会坚定选择更加保守(稳健)的【分区】方式来做交换分区。\n交换到文件(交换文件)没有缺点吗？\n曾经有，但是如今，在没有特别多碎片化文件的情况下，以现在(2025/5)的存储介质速度，完全可以忽略不计。\n而且，交换文件还能很动态分配使用空间。\n嗯，所以我对于【小存储】设备的选择是【交换文件】\n深入探讨 我来说说上面的 – 如今没有什么缺点是为什么\nSSD 的影响： SSD 的随机读写性能极高，其延迟远低于机械硬盘。文件系统层面的少量开销和碎片化对 SSD 性能的影响微乎其微。\n文件系统优化： 现代 Linux 文件系统（如 Ext4, Btrfs, XFS 等）在管理文件碎片方面也做得越来越好。\n实际感知： 对于日常使用、轻中度工作负载，甚至是一般的开发工作，交换文件和交换分区在性能上的差异几乎是不可感知的。\n那么使用【交换分区】的优势什么？(下面直接套用Gemini的回答)\n更直接的性能（理论上与极端场景下）\n无文件系统开销： 交换分区是一块独立的、不包含文件系统的磁盘空间。操作系统可以直接读写这块裸磁盘，而无需经过文件系统的复杂层（如文件分配表、元数据管理、目录结构等）。这减少了额外的 CPU 和 I/O 开销。\n碎片化问题： 交换分区本身不会出现碎片化问题，因为它是一块连续的专用空间。而交换文件，即使在 SSD 上影响微乎其微，但理论上仍可能因文件系统碎片化而降低性能。在极端高内存压力、频繁交换且同时进行大量磁盘 I/O的场景下，交换分区可能仍能提供微弱的性能优势。 休眠功能 (Suspend-to-Disk) 的可靠性\n必备条件： 对于需要将系统状态完整写入硬盘以实现休眠（也称作“挂起到磁盘”）的功能，Linux 通常要求存在一个大小等于或大于物理内存容量的交换分区。虽然一些现代发行版和内核尝试支持交换文件休眠，但交换分区的兼容性和稳定性通常更高，是实现可靠休眠的首选。 管理上的简洁性（对于“设置即用”型用户）\n一旦在系统安装时设置好交换分区，后续几乎不需要任何维护或管理。它是一个“配置一次，永久使用”的解决方案。而交换文件，虽然灵活，但可能需要用户手动创建、调整大小或在文件系统出现问题时重新管理。 文件系统独立性\n交换分区与你主文件系统（例如根分区 /）的健康状况相对独立。即使主文件系统出现严重错误，交换分区通常仍能正常工作（尽管这种情况可能表示系统已经严重受损）。交换文件则完全依赖于其所在文件系统的完整性。 在旧硬件或机械硬盘 (HDD) 上的表现\n在老旧的电脑上，特别是那些依然使用机械硬盘（HDD）的设备，由于 HDD 的寻道时间长、随机读写性能差，交换分区的无文件系统开销和无碎片化的优势会更加明显，能够提供比交换文件更稳定的性能。 用人话总结就是\n提供稳定可靠的休眠功能\n服务器或高性能工作站，在极端内存交换负载下追求每一丝性能\n对使用机械硬盘的旧设备支持好\n一次性设置，无需后期管理\n","permalink":"https://blog.dontalk.org/posts/linux%E4%BA%A4%E6%8D%A2%E5%88%86%E5%8C%BA%E5%92%8C%E4%BA%A4%E6%8D%A2%E5%88%B0%E6%96%87%E4%BB%B6%E4%BA%A4%E6%8D%A2%E6%96%87%E4%BB%B6%E5%A6%82%E4%BD%95%E9%80%89%E6%8B%A9/","summary":"水一篇文章。我有一台Chromebook，其大小只有32G，事实上，实际大小仅有可怜的28G，那么，【交换分区】和【交换到文件】(交换文件)该如何选择。\n前言 曾经主流派系 Linux(不是说如今放弃了)，都有设置交换分区的概念。事实上，也因为很久之前，有设置一个分区为交换分区的传统，如今不少 Linux 还保留了「交换分区」这个保守选择，稳妥选择。\n虽然交 …","title":"Linux【交换分区】和【交换到文件】(交换文件)如何选择？"},{"content":"或许大家都很困惑，曾经我也是…写得云里雾里的，我还以为是我自己英文水平不行的锅。交换分区【带休眠】和【不带休眠】的最直观区别，估计就是【带休眠】需要的空间比【不带休眠】要大很多。\n这是为什么？和它的设计有关 —— 带/不带休眠\n让我们直接进入正题\n交换分区 首先，要探寻这个问题，我们就必须先再次了解一下，交换分区是做什么的：\nLinux 中，“交换分区”（Swap Partition）或“交换文件”（Swap File）的主要作用是作为内存的扩展。当物理内存（RAM）不足时，系统会将不常用或不活跃的数据从 RAM 移动到交换空间中，以释放物理内存供当前活跃的进程使用。这个过程称为“交换出”（swap out），当需要再次访问这些数据时，再从交换空间将其“交换入”（swap in）RAM。\n“带睡眠”和“不带睡眠”的交换分区，主要区别在于它们是否支持休眠 (Hibernate) 功能。\n带休眠 (Hibernate/Suspend-to-Disk) 主要作用：\n支持休眠功能： 这是它与不带休眠交换分区的最大区别。当您选择休眠时，系统会将 整个 RAM 的内容（所有运行中的程序和数据）完整地写入到交换分区中，然后完全切断电源（如同关机）。当您再次开机时，系统会从交换分区中读取之前保存的内存内容，并恢复到休眠前的状态。 对交换分区大小的要求：\n为了支持休眠功能，交换分区的大小必须大于或等于您的物理内存（RAM）大小。 例如：如果您的系统有 16GB RAM，那么交换分区至少需要 16GB 才能支持休眠。通常建议略大于 RAM 大小，以留有余地，因为内存内容在写入交换分区时可能会有少量压缩或额外的元数据。\n如果交换分区小于 RAM 大小，系统将无法成功进入休眠状态。 优点：\n完全断电，省电： 休眠后系统完全不消耗电量，适合笔记本电脑用户长时间离开而不想关机。\n快速恢复工作状态： 恢复速度比从头启动系统快得多，所有应用程序和窗口都保持原样。 缺点：\n占用大量磁盘空间： 需要与物理内存一样大或更大的交换空间，尤其对于大内存系统（如 32GB RAM 需要 32GB+ 交换空间）来说，会显著占用硬盘空间。\n对 SSD 寿命有一定影响： 休眠时会进行一次性的大量数据写入操作，如果频繁使用休眠，会对固态硬盘（SSD）的写入寿命造成更大影响。\n恢复过程可能不如“睡眠（Suspend-to-RAM）”稳定： 尽管现代 Linux 内核对休眠的支持已经很好，但在某些硬件配置下，休眠和恢复可能会遇到兼容性问题。\n不带睡眠 (No Hibernate) 主要作用：\n内存溢出时的缓冲区： 当系统物理内存不足时，作为临时存储空间，防止系统因内存耗尽而崩溃。\n优化内存管理： 即使有足够的物理内存，Linux 内核也会将不常用或“脏”的内存页交换到磁盘上，从而为文件缓存等更重要的任务腾出 RAM 空间，提高系统响应速度。\n应对突发内存需求： 当应用程序突然需要大量内存时，交换空间可以提供一个备用方案。\n对交换分区大小的要求：\n对于不带休眠功能的系统，交换分区的大小通常建议为 RAM 大小的 1/2 到 1 倍，具体取决于您的 RAM 大小和工作负载。 例如：8GB RAM 的系统，可以设置 4GB – 8GB 的交换分区。\n现代系统如果 RAM 足够大（如 16GB 或 32GB 以上），并且不运行内存密集型应用（如大型虚拟机、视频编辑），甚至可以不设置交换分区（不推荐，但可行），或者设置一个较小的交换分区（如 2GB-4GB）。 优点：\n节省磁盘空间： 不需要与物理内存一样大的空间，节省宝贵的硬盘空间。\n对 SSD 寿命影响小： 由于写入次数相对较少，对固态硬盘（SSD）的寿命影响较小。 缺点：\n不支持休眠功能。 “睡眠”的另一个概念：Suspend-to-RAM（挂起到内存） 需要注意的是，在 Linux 中通常所说的“睡眠”或“挂起”，通常指的是 Suspend-to-RAM（挂起到内存），这与“休眠”不同。\nSuspend-to-RAM (挂起到内存 / 睡眠 / Sleep)： 系统会将大部分硬件组件断电，但内存（RAM）会继续保持通电以保存当前状态。恢复速度极快，但仍然会消耗少量电量。这个功能不需要特定大小的交换分区，因为它不将内存内容写入磁盘。 所以，当您在讨论“带睡眠”和“不带睡眠”的交换分区时，通常指的是是否支持“休眠 (Hibernate)”这个更深层次的睡眠状态。如果您不使用休眠功能，那么就不需要一个与 RAM 等大的交换分区。\n表格 特性/目的 不带睡眠（不带休眠 No Hibernate）交换分区 带睡眠（带休眠 Hibernate/Suspend-to-Disk）交换分区 主要功能 内存溢出缓冲，辅助内存管理 支持休眠 (Suspend-to-Disk)，兼具前者功能 对大小的要求 通常通常通常 为 RAM 的 1/2 到 1 倍 必须必须必须 ≥ RAM 大小 省电功耗 仅辅助内存管理，系统正常运行或进入睡眠 (Suspend-to-RAM) 时耗电 系统完全断电，不耗电 恢复速度 不涉及恢复系统状态（仅是在常启动或从睡眠唤醒） 快速恢复到休眠前的工作状态 磁盘空间占用 相对较少 较大（与 RAM 等同或更大） 对 SSD 寿命影响 较小 较大（频繁休眠会增加写入量） 适用场景 大部分桌面和服务器用户，不需休眠功能 笔记本用户，需要长时间不关机但完全省电的用户 一口气写了两篇文章，最近事情太多了，实在不舒服，上面内容由 Gemini 辅助整理的。\n","permalink":"https://blog.dontalk.org/posts/linux%E4%BA%A4%E6%8D%A2%E5%88%86%E5%8C%BAswap%E7%9A%84%E5%B8%A6%E4%BC%91%E7%9C%A0%E5%92%8C%E4%B8%8D%E5%B8%A6%E4%BC%91%E7%9C%A0%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88/","summary":"或许大家都很困惑，曾经我也是…写得云里雾里的，我还以为是我自己英文水平不行的锅。交换分区【带休眠】和【不带休眠】的最直观区别，估计就是【带休眠】需要的空间比【不带休眠】要大很多。\n这是为什么？和它的设计有关 —— 带/不带休眠\n让我们直接进入正题\n交换分区 首先，要探寻这个问题，我们就必须先再次了解一下，交换分区是做什么的：\nLinux 中，“交换分区 …","title":"Linux交换分区(Swap)的【带休眠】和【不带休眠】，区别是什么？"},{"content":" systemd-boot (也常被称为 system-boot 或 Gummiboot，它是 systemd-boot 的前身) 和 GRUB (GNU GRand Unified Bootloader) 都是 Linux 系统中常用的引导加载程序（Bootloader）。它们的主要职责是在计算机启动时加载操作系统的内核。尽管目标相同，但它们的设计理念、功能集和适用场景有很大不同。\nGRUB (GNU GRand Unified Bootloader)是什么？ GRUB 是最常见和功能最强大的引导加载程序之一。它支持多种文件系统、多种操作系统（Linux、Windows、macOS 等），并且提供了丰富的配置选项和强大的脚本功能。它可以在 BIOS/MBR (Legacy Boot) 和 UEFI/GPT (EFI Boot) 两种引导模式下工作。大多数主流的 Linux 发行版（如 Ubuntu, Fedora, Debian, Arch Linux 等）默认都使用 GRUB。\n优点：\n兼容性极佳：\n同时支持 BIOS 和 UEFI 引导： 可以在几乎所有现代和老旧的硬件上工作。\n支持多种文件系统： 能够直接读取各种文件系统（如 ext2/3/4, XFS, Btrfs, FAT, NTFS 等）上的内核和 initramfs 文件，无需专门的引导分区。\n多操作系统引导能力强： 可以轻松地检测并引导同一台机器上的多个操作系统（包括 Linux、Windows、macOS 等），并提供一个友好的菜单供用户选择。\n功能丰富：\n强大的脚本能力： 允许用户编写复杂的引导脚本，实现高级的引导逻辑。\n模块化设计： 具有高度模块化的架构，可以根据需要加载不同的模块（如文件系统驱动、加密支持等）。\n恢复和调试功能： 提供了命令行界面，可以在引导过程中进行调试和修复。\n主题和自定义： 支持图形主题，可以自定义引导菜单的外观。\n广泛使用和社区支持： 由于其普及性，GRUB 拥有庞大的用户群和完善的文档，遇到问题时容易找到解决方案。\n缺点：\n复杂性高：\n配置文件复杂： grub.cfg 配置文件通常由脚本自动生成，但手动编辑起来相对复杂，容易出错。\n体积较大： 相比其他引导加载程序，GRUB 的体积和代码量较大，加载时间可能稍长（尽管在现代硬件上差异不大）。 更新管理： 每次内核更新时，通常需要重新生成 GRUB 配置，虽然大多数发行版会自动处理，但有时也会出现问题。\nsystemd-boot (以前的 Gummiboot) 是什么？ systemd-boot 是 systemd 项目的一部分，是一个简单、轻量级的 UEFI 引导管理器。它的设计理念是“只做一件事，并把它做好”：即在 UEFI 系统上快速引导操作系统。它不是一个通用的引导加载程序，因为它仅支持 UEFI 引导，并且无法直接读取复杂的文件系统。它通常用于引导位于 EFI 系统分区 (ESP) 上的 Linux 内核（通常是统一内核镜像，Unified Kernel Image, UKI）或 EFI 应用程序。\n优点：\n简单且快速：\n配置简单： 配置文件非常简单，通常是纯文本文件，易于阅读和手动编辑。\n引导速度快： 由于代码量少，功能精简，在 UEFI 系统上引导速度通常比 GRUB 快。\n易于维护： 不需要复杂的脚本来生成配置，维护起来更简单。\n符合 UEFI 标准： 紧密遵循 UEFI 规范，与 UEFI 固件的集成度高。\n原生支持统一内核镜像 (UKI)： 越来越多地被用于引导包含内核、initramfs 和内核命令行参数的单个 EFI 可执行文件（UKI），简化了引导过程。\n缺点：\n仅支持 UEFI 引导： 这是最大的限制。如果您的机器使用传统的 BIOS/MBR 引导模式，或者需要引导不支持 UEFI 的系统，systemd-boot 就无法使用。\n功能有限：\n文件系统支持有限： 只能直接读取 FAT32 文件系统（这是 ESP 的标准文件系统），不能直接从 ext4、Btrfs 等文件系统加载内核。这意味着内核和 initramfs 必须位于 ESP 上，或者通过 UKI 方式整合。\n多操作系统引导能力弱： 尽管可以引导其他操作系统（如 Windows），但通常是通过链式引导 (chainload) Windows 的引导管理器来实现，不像 GRUB 那样可以统一管理和显示多个操作系统的引导项。\n缺乏高级功能： 没有 GRUB 那样强大的脚本功能、复杂的模块化支持或丰富的调试选项。\n用户群和文档相对较少： 虽然在 Arch Linux 社区中越来越受欢迎，但相比 GRUB，其用户群和非官方文档资源仍相对较少。\n对比 特性 GRUB systemd-boot 引导模式 BIOS (MBR) 和 UEFI (GPT) 仅限 UEFI (GPT) 文件系统支持 广泛支持多种 Linux 文件系统、FAT、NTFS 等 仅支持 FAT32 (ESP) 配置 复杂脚本生成，手动修改困难 简单纯文本文件，易于手动编辑 引导速度 相对较慢（但通常可忍受） 相对较快 多系统引导 强大，统一管理且显示多种操作系统 简单，主要通过链式引导，管理能力有限 功能/定制 功能丰富，支持插件、模块、主题 极简，功能受限，侧重快速引导 体积 较大 小巧轻量 适用场景差异 传统 BIOS 机器，多系统启动，需更高灵活引导功能 现代 UEFI 机器，追求极致简洁和快速引导 EFI(ESP) 分区是什么？为什么引导依赖它 ESP 是一个特殊的、独立的分区，格式通常为 FAT32。\n它是 UEFI (Unified Extensible Firmware Interface) 固件用于查找和加载操作系统引导加载程序的标准位置。— 这是现代电脑的主流\nUEFI 固件会扫描磁盘上的 ESP，并查找其中的 .efi 引导文件来启动系统。\nGrub 不依赖EFI(ESP)分区也行，但是 Systemd-boot 必须依赖EFI分区。在严格模式下，这句话是错的，为什么错？\n因为决定是否使用 EFI(ESP) 分区引导的【并非是】软件(Grub/Systemd-boot)，【而是】由主板设置的 UEFI 启动决定的。(由主板设置的 (传统)BIOS/MBR 引导 还是 UEFI/GPT 引导 决定)\n如果主板设置的是 UEFI，那么 Grub 也得要有 EFI(ESP) 分区才行。但是现代的主板(一般)都支持混合启动方式(多支持)，所以，使用Grub时候，没有 EFI(ESP) 分区也可以启动。但是，你使用的就不再是 UEFI 了。\n而 Systemd-boot 要求 UEFI 启动，所以就会容易给人一种错觉 \u0026gt; Grub 不依赖 EFI(ESP) 分区启动，而 Systemd-boot 依赖 EFI(ESP) 分区启动。\n一旦你这样错误地认为，那么当你在只支持 UEFI 的电脑(或者设置了只使用UEFI启动)，使用 Grub ，但不分配 EFI(ESP) 分区用来记录启动文件时，就会无法引导到系统～～甚至安装都无法完成。\n目录情况 GRUB： BIOS/MBR 引导模式下：\nGRUB 会将一部分代码写入 MBR (Master Boot Record)，另一部分（核心镜像）通常放在 /boot 目录下的 /boot/grub 文件夹中。\nUEFI/GPT 引导模式下\nGRUB 的 EFI 可执行文件 (grubx64.efi 或 grubia32.efi) 会被安装到 ESP 中（通常是 /EFI/GRUB 或 /EFI/Linux发行版名 目录下）。\n虽然 GRUB 的核心配置文件 grub.cfg 和模块通常仍然位于 Linux 根分区下的 /boot/grub 目录中，但 UEFI 固件首先会从 ESP 启动 grubx64.efi，然后由这个 EFI 文件加载 /boot/grub/grub.cfg。\nSystemd-boot： Systemd-boot 是一个 UEFI 引导管理器，它必须运行在 EFI 系统分区（ESP）上。ESP 是一个特殊的 FAT32 格式分区，被 UEFI 固件识别为存储引导加载程序的地方。systemd-boot 的可执行文件和所有引导条目配置都直接放在 ESP 上。\n","permalink":"https://blog.dontalk.org/posts/systemd-boot%E5%BC%95%E5%AF%BC%E5%92%8Cgrub%E5%BC%95%E5%AF%BC%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%ABefiesp-%E5%88%86%E5%8C%BA%E6%98%AF%E4%BB%80%E4%B9%88%E4%B8%BA%E4%BB%80%E4%B9%88%E5%BC%95%E5%AF%BC%E4%BE%9D%E8%B5%96%E5%AE%83/","summary":" systemd-boot (也常被称为 system-boot 或 Gummiboot，它是 systemd-boot 的前身) 和 GRUB (GNU GRand Unified Bootloader) 都是 Linux 系统中常用的引导加载程序（Bootloader）。它们的主要职责是在计算机启动时加载操作系统的内核。尽管目标相同，但它们的设计理念、功 …","title":"Systemd-boot引导和Grub引导有什么区别？EFI(ESP) 分区是什么？为什么引导依赖它"},{"content":"桌面环境(Desktop Environments – DEs)和窗口管理器(Window Managers – WMs)到底有什么区别？几乎没有谁说得清楚，每篇文章都看得云里雾里的。\n一句话概括 就是。【桌面环境】提供了一整套桌面环境，如文件管理器(是的，这个也是桌面环境提供的)，文本编辑器，播放器..甚至包括了E-Mail软件，办公软件等等。而【窗口管理器】只是给出了一个桌面，你可以使用鼠标了，可以打开你自己安装的软件了。但是如果你不安装软件，如浏览器，播放器，文本编辑器的话，那么依旧是没有。窗口管理器只是让你的鼠标有了用武之地，仅此而已(提供了可视化功能，一个显示器可以开出多个分页面出来，而不像tty，只展示黑漆漆一个页面，用命令交互)。\n那，让我们深入说说吧\n正文 桌面环境 (Desktop Environment – DE) 桌面环境是一个比窗口管理器更完整、更集成的图形用户界面集合。它包含了窗口管理器，并在此基础上提供了用户期望从现代操作系统中获得的所有常见桌面功能。\n核心组件： 窗口管理器： 作为 DE 的一部分，负责窗口管理。\n文件管理器： 用于浏览、管理文件和文件夹（如 Nautilus 在 GNOME 中，Dolphin 在 KDE Plasma 中）。\n面板/任务栏： 提供应用程序启动器、系统托盘、时钟、通知区域等。\n系统设置工具： 统一的界面来配置网络、显示、声音、电源、用户账户等。\n应用程序启动器/菜单： 快速启动应用程序的方式。\n壁纸和主题管理： 定制桌面外观。\n一系列集成应用： 通常包括文本编辑器、图片查看器、计算器等。\n会话管理： 负责登录、注销和会话恢复。\n特点： 开箱即用： 安装后即可拥有一个功能齐全的桌面，无需额外配置。\n用户友好： 更适合新手用户，提供直观的图形界面操作。\n资源占用较高： 由于集成了大量功能和后台服务，通常比独立的 WM 占用更多系统资源。\n集成度高： 各个组件之间紧密配合，提供统一的视觉风格和用户体验。\n常见示例： GNOME： 现代、简洁、以活动为中心(动画好，吃配置)。\nKDE Plasma： 功能强大、高度可定制、界面华丽(依旧吃配置)。\nXFCE： 轻量级、快速、稳定、占用资源少(清爽)。\nCinnamon： 基于 GNOME 技术，提供传统桌面布局。\nMATE： 基于 GNOME 2，提供经典的桌面体验。\n窗口管理器 (Window Manager – WM) 核心功能： 窗口的绘制与装饰： 负责绘制窗口的边框、标题栏（包括最小化、最大化、关闭按钮）以及窗口的阴影等“装饰”。\n窗口的定位与大小调整： 决定新窗口出现的位置，并允许您移动、调整窗口大小。\n窗口的堆叠顺序： 管理哪个窗口在前面，哪个在后面。\n工作区/虚拟桌面： 提供在不同虚拟屏幕之间切换的功能。\n窗口焦点： 确定哪个窗口当前处于活动状态并接收键盘输入。\n特点： 轻量级： 由于功能单一，WM 通常资源占用极低。\n高度可定制： 通常通过配置文件进行定制，您可以精确控制窗口的行为和外观。\n键盘驱动： 许多 WM（尤其是平铺式窗口管理器）设计为高度依赖键盘快捷键操作，效率极高。\n“裸机”体验： 它们只提供窗口管理功能，不包括文件管理器、面板、系统设置、应用程序启动器、壁纸管理等。您需要手动安装和配置这些额外组件来构建一个完整的桌面体验。\n类型： 浮动式 (Floating/Stacking) WM： 窗口可以自由移动、重叠，类似 macOS 或 Windows 的传统桌面（例如：Openbox, Fluxbox）。\n平铺式 (Tiling) WM： 窗口会自动排列，平铺在屏幕上，通常不重叠，最大化屏幕空间利用率（例如：i3, Awesome, Sway, bspwm）。\n常见示例：更多见下方\n窗口管理器 的 浮动式 和 平铺式 具体区别？ 平铺式窗口管理器 (Tiling Window Managers) 核心理念： 最大化屏幕空间利用率和键盘操作效率，减少鼠标使用。 窗口像瓷砖一样自动排列，通常不会相互重叠。\n工作方式： 自动布局： 当您打开一个新窗口时，WM 会自动调整所有现有窗口的大小和位置，以适应新窗口，使它们平铺在屏幕上，不重叠。\n键盘驱动： 大多数操作（如切换窗口、移动窗口、调整大小、创建新工作区等）都是通过键盘快捷键完成的。鼠标的使用被最小化，通常只用于点击内容。\n分区域管理： 屏幕通常被分成几个区域（主区域、堆栈区域等），窗口可以放置在这些区域中，并且它们的布局方式可以配置。\n无窗口装饰（或极简）： 许多平铺式 WM 默认没有或只有极小的窗口边框和标题栏，进一步节省屏幕空间。\n工作区/标签页： 强调使用多个工作区（或虚拟桌面）来组织应用程序，每个工作区通常有自己独立的窗口布局。\n优点： 高效利用屏幕空间： 窗口不重叠，所有打开的应用程序都能一览无余，尤其适合大屏幕。\n提高工作效率： 熟悉键盘快捷键后，可以非常快速地切换和管理窗口，减少鼠标移动时间。\n资源占用极低： 通常非常轻量，适合资源有限的机器。\n高度定制化： 可以通过配置文件精确控制布局行为和快捷键。\n专注于内容： 减少了窗口管理本身对注意力的分散。\n缺点： 学习曲线陡峭： 对于习惯传统浮动桌面的用户来说，一开始可能不适应其操作模式。\n不适合所有应用： 对于需要精确鼠标拖放、自由调整大小（如图形设计软件、视频编辑软件）或有很多小浮动窗口（如弹窗、工具面板）的应用，平铺模式可能不太方便。\n视觉效果可能不如传统桌面华丽。\n典型代表： i3 (i3-gaps), Sway, Awesome, bspwm, Xmonad，hyprland(很好看)。\n浮动式窗口管理器 (Floating/Stacking Window Managers) 核心理念： 模拟传统桌面操作，窗口可以自由移动、重叠，提供更灵活的视觉布局。\n工作方式： 自由定位和重叠： 窗口可以在屏幕上自由移动，并且可以相互重叠，就像纸张堆叠在一起。\n鼠标驱动： 大多数操作（如移动、调整大小、切换焦点）可以通过鼠标拖拽和点击完成。\n有窗口装饰： 通常会有明显的窗口边框和标题栏，包含最小化、最大化和关闭按钮。\n经典用户体验： 提供与 Windows 或 macOS 类似的视觉和交互体验。\n优点： 直观易用： 对于大多数用户来说，学习曲线平缓，因为它模拟了他们熟悉的桌面环境。\n适合所有应用程序： 无论应用程序的性质如何，都能很好地适应浮动布局。\n视觉效果更传统。\n缺点： 屏幕空间利用率可能较低： 窗口重叠会导致部分内容被遮挡，需要频繁移动或调整窗口。\n效率可能不如平铺式： 频繁的鼠标操作可能会降低工作流的效率，尤其是在处理大量窗口时。\n资源占用通常高于平铺式 WM（但仍远低于完整的桌面环境）。\n典型代表： Openbox, Fluxbox, FVWM, Blackbox。\n","permalink":"https://blog.dontalk.org/posts/%E6%A1%8C%E9%9D%A2%E7%8E%AF%E5%A2%83de%E5%92%8C%E7%AA%97%E5%8F%A3%E7%AE%A1%E7%90%86%E5%99%A8wm%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB/","summary":"桌面环境(Desktop Environments – DEs)和窗口管理器(Window Managers – WMs)到底有什么区别？几乎没有谁说得清楚，每篇文章都看得云里雾里的。\n一句话概括 就是。【桌面环境】提供了一整套桌面环境，如文件管理器(是的，这个也是桌面环境提供的)，文本编辑器，播放器..甚至包括了E-Mail软件，办公软件等等。而【窗口管理 …","title":"桌面环境(DE)和窗口管理器(WM)有什么区别？"},{"content":"核心概念：JWT 是一种“令牌”格式，而不是一种“存储或传输机制”\nJWT 本身只是一种紧凑且自包含的方式，用于在各方之间安全地传输信息。它定义了令牌的结构（Header.Payload.Signature），并包含验证令牌完整性（未被篡改）和真实性（确实由已知方签发）的方法。\nJWT 不关心你如何存储或传输它。 令牌本身可以放在：\nHTTP 请求头 (Authorization: Bearer )\nURL 查询参数\n请求体\nCookie\n所以我选择把 JWT 放在了 Cookie 里面，通过 res.cookie 的设置，可以很方便地设置 Cookie 自携带，并且可以一定程度避免 XSS 攻击。\nres.cookie(\u0026#39;authToken\u0026#39;, token, { httpOnly: true, secure: process.env.NODE_ENV === \u0026#39;production\u0026#39;, maxAge: 14400000, sameSite: \u0026#39;Lax\u0026#39;, // 或 \u0026#39;None\u0026#39; }); 这条代码的作用就是告诉浏览器：“嘿，我这里有一个 authToken 叫这个值，请把它存起来，并在将来发送到 localhost:3000 的请求中。”\n浏览器自动管理 HTTP-only Cookie。 一旦 Cookie 被设置，浏览器会负责在后续发送到同一域 (或兼容域/路径) 的请求中，自动将其包含在 Request Headers 的 Cookie 字段中。你的前端 JavaScript 代码无需手动读取 localStorage 或 sessionStorage 并将其添加到每个请求头中。 后端通过 cookie-parser 中间件解析这些 Cookie。 当请求到达后端时，cookie-parser 中间件会解析 Request Headers 中的 Cookie 字段，并将这些 Cookie 的键值对填充到 req.cookies 对象中。 所以，当你在 /login 路由或 authenticateJWT 中间件中执行 const token = req.cookies.authToken; 时，你实际上是在从浏览器自动发送过来的 Cookie 中获取 JWT。 XSS 和 CSRF 业界普遍认为 XSS 攻击的危害性更大，因为它可以做的事情更多（窃取所有可访问的数据，甚至在用户会话中执行任意操作）。因此，很多应用会选择将 Token 存储在 HttpOnly 的 Cookie 中来防御 XSS，然后通过 SameSite 属性（Lax 或 Strict）或配合 CSRF Token 来防御 CSRF。\n所以：\n在 JWT 的认证体系中：\n将 JWT 存储在 localStorage 或 sessionStorage，并通过 Authorization 请求头发送：\n优点： 天然防御 CSRF。\n缺点： 极易受到 XSS 攻击，因为恶意 JavaScript 可以直接读取 Token。 将 JWT 存储在 HttpOnly 的 Cookie 中：\n优点： 有效防御 XSS 攻击（因为 JavaScript 无法访问 HttpOnly Cookie）。\n缺点： 易受 CSRF 攻击（如果 SameSite 设置不当或没有其他 CSRF Token 机制），因为浏览器会自动携带 Cookie。 所以，当你回过神的时候，有没有发现，这个博文的封面，其实是 Session Vs Token …\n","permalink":"https://blog.dontalk.org/posts/%E7%8E%B0%E4%BB%A3%E7%BD%91%E9%A1%B5%E5%BC%80%E5%8F%91%E6%9C%89jwt%E7%9A%84token%E4%B8%BA%E4%BB%80%E4%B9%88%E8%BF%98%E8%A6%81%E7%94%A8cookie/","summary":"核心概念：JWT 是一种“令牌”格式，而不是一种“存储或传输机制”\nJWT 本身只是一种紧凑且自包含的方式，用于在各方之间安全地传输信息。它定义了令牌的结构（Header.Payload.Signature），并包含验证令牌完整性（未被篡改）和真实性（确实由已知方签发）的方法。\nJWT 不关心你如何存储或传输它。 令牌本身可以放在：\nHTTP 请求头 …","title":"【现代网页开发】有JWT的Token为什么还要用Cookie？"},{"content":"为了解决(搞明白)这个问题，实在花了我超级长的时间，由于我是熬夜写代码，一写就是很长一段时间，甚至一两天。以至于我一直盯着我的前后端代码，怎么都看不出来个所以然。\n即便查阅了大量资料，甚至是翻阅我之前写的文档(还有询问Ai)，问题都没有很好解决，甚至自我怀疑去重新看老外的 JWT 入门课。\n因为网络上已经很多人提供了解决方案，但是按照我的经历来说，我觉得更重要的是思路。因为这个问题太抽象了，所以请允许我啰嗦那么一下。\n技术栈 Express 和 React。原生的 Fetch 方法(也试过 Axios)\n前提 请保证，你实在找不到前后端的问题了，比如说..(这些事情你记得做了吗？)\n后端： 在 App.js 入口文件有没有设置CORS和解析CookieParser的中间件\n... app.use( cors({ origin: \u0026#39;http://localhost:5173\u0026#39;(因为我的Vite的React项目), credentials: true(允许验证Cookie), }) ); ... app.use(cookieParser()); // 为什么设置这个呢？ // 因为我为了获得更高的安全性，我选择的是 JWT 通过 Cookie 传输。(详见下一篇文章说明) 上面的设置是没什么问题的(我甚至tmd还调换了代码的顺序)\n前端： 之后是前端，需要保证在登陆的路由(POST你username和password那个Fetch)，以及需要验证的路由(GET出数据前验证你登陆那个Fetch)\n保证格式有这个:\n// POST const response = await fetch(\u0026#39;http://localhost:3000/user/login\u0026#39;, { // 确保这里的URL与你的后端匹配 method: \u0026#39;POST\u0026#39;, headers: { \u0026#39;Content-Type\u0026#39;: \u0026#39;application/json\u0026#39;, }, body: JSON.stringify({ username, password }), credentials: \u0026#39;include\u0026#39;, // \u0026lt;-- 确保这一行存在！ }); // GET const response = await fetch(`http://localhost:3000/article/${id}`, { method: \u0026#39;GET\u0026#39;, headers: { \u0026#39;Content-Type\u0026#39;: \u0026#39;application/json\u0026#39;, // 即使是 GET 请求，最好也指定 Content-Type }, credentials: \u0026#39;include\u0026#39;, // \u0026lt;-- 确保这一行存在！ }); credentials: 'include' 这一行和后端的那个差不多。这个是为了告诉浏览器，记得带上 Cookie 去 Fetch 这个接口。\n中间件 我有一个中间件 (verifyLogin)，用于校验用户是否登陆，否则无权查看内容，里面的开头是这样的：\nconst verifyLogin = (req, res, next) =\u0026gt; { const authHeader = req.headers.authorization; const token = authHeader \u0026amp;\u0026amp; authHeader.startsWith(\u0026#39;Bearer \u0026#39;) ? authHeader.split(\u0026#39; \u0026#39;)[1] : req.cookies?.authToken; // 我的代码逻辑是先尝试从Headers里面获得Token，没有再从req(请求)的Cookie里面尝试找authToken if (!token) { console.log(\u0026#39;未找到授权标头\u0026#39;); req.user = null; return next(); } ... 我访问一次需要验证的路由页面，我的后端终端就报一次 未找到授权标头。我当然不死心，我打印出赋值的 token ，结果居然是空的。\n浏览器控制台 打开浏览器控制台，NetWork标签，在登陆后，明显可以看到接口返回了200，Response里面，明明就有一个 Set-Cookie .\n但是在 Application(Storage) – Cookies – 点进去URL – 就是找不到有一个叫 authToken 的玩意。\n我做了什么： 我对后端的 登陆路由 ，改了很久(这个很重要)\n// 登陆路由 router.post(\u0026#39;/login\u0026#39;, async (req, res) =\u0026gt; { const { username, password } = req.body; try { // 查找用户 const user = await User.findOne({ where: { username } }); if (!user) { return res.status(401).json({ error: \u0026#39;账号或密码错误.\u0026#39; }); }; // 验证密码 const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { return res.status(401).json({ error: \u0026#39;账号或密码错误.\u0026#39; }); }; // 生成 Token const token = generateToken({ id: user.id, username: user.username, role: user.role, }); res.cookie(\u0026#39;authToken\u0026#39;, token, { httpOnly: true, // 防止 XSS 攻击 secure: false, // 本地开发使用 HTTP sameSite: \u0026#39;Lax\u0026#39;, // 允许跨域请求携带 Cookie path: \u0026#39;/\u0026#39;, // 确保适用于所有路径 maxAge: 14400000, // 4小时 }); res.json({ message: `登录成功！欢迎回来, ${user.username}` }); } catch(error) { ... 代码大概如上，最重要的就是：\nres.cookie(\u0026#39;authToken\u0026#39;, token, { httpOnly: true, // 防止 XSS 攻击 secure: false, // 本地开发使用 HTTP sameSite: \u0026#39;Lax\u0026#39;, // 允许跨域请求携带 Cookie path: \u0026#39;/\u0026#39;, // 确保适用于所有路径 maxAge: 14400000, // 4小时 }); 这里的组合一旦出错就会“爆炸”(可能是你的情绪)。\n正题 好了，铺垫了那么久，我们也该进入正题了。到底为什么。\n为了解决这个问题，我一共下载了 四个 不同的浏览器，最终靠着 Opera 浏览器解决了。\n在 Opera 浏览器里面可以看到一个 Set-Cookie 有一个黄色的三角感叹警告标。\n并且在 Application(Storage) – Cookies – 点进去URL – 能找到 authToken 这个玩意(不过是黄色的)。\n当我点击开一个新的网页，总之就是网页发生变化时，这个黄色的 authToken 就直接消失了。\n加上我 Chrome 莫名其妙打上了 Cookie。\n我开始怀疑是不是浏览器的问题了。\n是的，就是浏览器的问题，浏览器的策略不同，以至于因为你开发环境配置的\n(不同的浏览器(甚至是同一个浏览器不同的版本号)它们所接受的策略组都不同)\nres.cookie(\u0026#39;authToken\u0026#39;, token, { httpOnly: true, // 防止 XSS 攻击 secure: false, // 本地开发使用 HTTP sameSite: \u0026#39;Lax\u0026#39;, // 允许跨域请求携带 Cookie path: \u0026#39;/\u0026#39;, // 确保适用于所有路径 maxAge: 14400000, // 4小时 }); 不同，从而打不上 authToken ，并且，除了 Opera 浏览器，其他浏览器都没让我知道发生了什么。\n所以，我把配置调整成了下面这样，居然就好了\nres.cookie(\u0026#39;authToken\u0026#39;, token, { httpOnly: true, // 防止 XSS 攻击 secure: false, // 本地开发使用 HTTP sameSite: \u0026#39;Lax\u0026#39;, // 允许跨域请求携带 Cookie path: \u0026#39;/\u0026#39;, // 确保适用于所有路径 maxAge: 14400000, // 4小时 }); 如果你没能好，并且确定了不是代码问题，就换浏览器/关一下浏览器 Cookie过滤 看看\n因为我最早就是上面的配置，未能通过，我把配置调整成这样：(依旧被拒绝)\nres.cookie(\u0026#39;authToken\u0026#39;, token, { httpOnly: false, // 允许 Javascript 获取 Cookie secure: false, // 本地开发使用 HTTP sameSite: \u0026#39;None\u0026#39;, // NONE maxAge: 14400000, // 4小时 }); 再或者是这样的：\nres.cookie(\u0026#39;authToken\u0026#39;, token, { httpOnly: true, // 防止 XSS 攻击 secure: true, // 使用 HTTPS sameSite: \u0026#39;Strict\u0026#39;, path: \u0026#39;/\u0026#39;, // 确保适用于所有路径 maxAge: 14400000, // 4小时 }); 无论如何，就是不行。\n最终使用下面配置，成功了\nres.cookie(\u0026#39;authToken\u0026#39;, token, { httpOnly: true, // 防止 XSS 攻击 secure: false, // 本地开发使用 HTTP sameSite: \u0026#39;Lax\u0026#39;, // 允许跨域请求携带 Cookie path: \u0026#39;/\u0026#39;, // 确保适用于所有路径 maxAge: 14400000, // 4小时 }); 我的情况是：\n3000 端口的 Express 后端。5173 端口的 React 前端。\nReact 前端请求 3000端口的 /login 后端登陆，获得返回的 Token\n只有 HTTP，都在localhost，端口不同\n最终靠上面的配置成功了。\n知识拓展 – 参数代表什么 Set-Cookie 是服务器在 HTTP 响应头中设置的，用于将 Cookie 发送到客户端，浏览器会存储这些 Cookie 并在后续请求中自动带上\n所以在访问这些接口的时候，要记得带上\nconst response = await fetch(`http://localhost:3000/article/${id}`, { method: \u0026#39;GET\u0026#39;, headers: { \u0026#39;Content-Type\u0026#39;: \u0026#39;application/json\u0026#39;, }, credentials: \u0026#39;include\u0026#39;, // \u0026lt;-- 确保这一行存在！ // 这样浏览器才会自动带上 Cookie }); res.cookie 的属性：\n– HttpOnly: true/false：防止客户端 JavaScript 访问 Cookie，增强安全性。\nSecure: true/false：仅通过 HTTPS 传输 Cookie。\nSameSite: Strict/Lax/None：控制跨站请求是否携带 Cookie（Strict、Lax 或 None）。\n‘Strict’：仅同站请求（Origin 相同）携带 Cookie。\n‘Lax’：同站请求和部分跨站请求（如 GET 导航）携带 Cookie。(Chrome 自 Chrome 84+ 起默认）\n‘None’：允许所有跨站请求携带 Cookie（需 secure: true）。\n默认值搭配：SameSite=None 必须搭配 secure: true，否则浏览器（如 Chrome）会拒绝存储。(具体看不同浏览器)\nExpires 或 Max-Age：设置 Cookie 有效期(毫秒)。\nPath 和 Domain：控制 Cookie 的作用范围(如’/’, ‘/user’)。\n如何设置Cookie(sessionId)\nconst express = require(\u0026#39;express\u0026#39;); const app = express(); app.post(\u0026#39;/login\u0026#39;, (req, res) =\u0026gt; { // 假设用户登录成功，生成会话或令牌 const user = { id: 123, username: \u0026#39;example\u0026#39; }; // 设置 Cookie (sessionId) res.cookie(\u0026#39;sessionId\u0026#39;, \u0026#39;abc123\u0026#39;, { maxAge: 24 * 60 * 60 * 1000, // 有效期 1 天 httpOnly: true, // 防止 XSS secure: process.env.NODE_ENV === \u0026#39;production\u0026#39;, // 生产环境用 HTTPS sameSite: \u0026#39;strict\u0026#39; // 防止 CSRF }); res.json({ message: \u0026#39;登录成功\u0026#39; }); }); 响应：Set-Cookie: sessionId=abc123; Max-Age=86400; HttpOnly; Secure; SameSite=Strict\n如何验证Cookie(sessionId)\n首先我们需要解析 Cookie：\n– 手动解析：从 req.headers.cookie 中提取 Cookie 字符串，然后解析为键值对。\n使用中间件：许多框架（如 Express.js）提供 Cookie 解析中间件（如 cookie-parser），自动将 Cookie 解析为 req.cookies 对象。 流程是：\n检查 req.cookies 中是否存在目标 Cookie（如 sessionId）。\n使用 Cookie 值查询会话存储（Redis、数据库）或验证 JWT。\n如果 Cookie 无效、过期或不存在，返回错误响应。\n使用 cookie-parser 验证 Cookie(sessionId)：\nconst express = require(\u0026#39;express\u0026#39;); const cookieParser = require(\u0026#39;cookie-parser\u0026#39;); const app = express(); app.use(cookieParser()); // 解析 Cookie const authMiddleware = (req, res, next) =\u0026gt; { const sessionId = req.cookies.sessionId; // 获取 Cookie if (!sessionId) { return res.status(401).json({ error: \u0026#39;未提供会话 ID\u0026#39; }); } // 假设会话存储在 Redis 中（验证sessionId） redisClient.get(`session:${sessionId}`, (err, data) =\u0026gt; { if (err || !data) { return res.status(401).json({ error: \u0026#39;会话无效或已过期\u0026#39; }); } req.user = JSON.parse(data); // 将用户信息附加到 req next(); }); }; 基于 Session 的验证： 使用 express-session 或类似库管理会话。 会话数据存储在服务器端（如 Redis），客户端仅存储 sessionId。\n中间件检查 req.session.user 是否存在。 基于 JWT 的验证： 登录时生成 JWT，存储在 Cookie 或 Authorization 头中。 中间件验证 JWT 的签名和有效期。\n无需服务器端存储会话，适合无状态应用。\n混合方式： – 结合 Session 和 JWT：将 JWT 存储在 Cookie 中，中间件验证 JWT 并查询会话存储。\n一般而言：\nhttpOnly：安全，设 true。\nsecure：开发 false，生产 true。\nsameSite：开发 Lax，生产 None（需 secure: true）。\nmaxAge：定义有效期。\npath：确保有效范围。\n总结 总结：排除掉代码问题后，尝试调整你的res.cookie策略，更换浏览器/调整浏览器Cookie设置。再测试。\n","permalink":"https://blog.dontalk.org/posts/%E6%9C%89set-cookie%E8%BF%94%E5%9B%9E%E4%BD%86%E5%B0%B1%E6%98%AF%E6%97%A0%E6%B3%95%E6%88%90%E5%8A%9F%E8%AE%BE%E7%BD%AE%E6%9C%80%E7%BB%88%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95/","summary":"为了解决(搞明白)这个问题，实在花了我超级长的时间，由于我是熬夜写代码，一写就是很长一段时间，甚至一两天。以至于我一直盯着我的前后端代码，怎么都看不出来个所以然。\n即便查阅了大量资料，甚至是翻阅我之前写的文档(还有询问Ai)，问题都没有很好解决，甚至自我怀疑去重新看老外的 JWT 入门课。\n因为网络上已经很多人提供了解决方案，但是按照我的经历来说，我觉得更重 …","title":"有Set-Cookie返回，但就是无法成功设置(最终解决方法)"},{"content":"因为我经常会对劳烦于 CSS ，烦躁到宁愿自己手写原生的 CSS 。我想，我们需要一些适合自己的 CSS 框架。这个文章列举一些还不错的 CSS 框架，方便大家(我)作挑选。\n1. Chakra UI 特点： 现代化设计，默认主题优雅且美观 组件高度可定制，支持暗黑模式 内置响应式设计，易于使用的样式系统 完全基于 React 组件，支持 TypeScript 适合：需要快速构建美观且功能丰富的 React 应用 官网：https://chakra-ui.com/\n2. Material-UI (MUI) 特点： 基于 Google Material Design 规范，设计现代且专业 丰富的组件库，支持主题定制和响应式设计 强大的社区和生态系统，文档完善 支持 TypeScript，适合大型项目 适合：喜欢 Material Design 风格，追求成熟稳定的框架 官网：https://mui.com/\n3. Ant Design 特点： 企业级设计体系，界面简洁优雅 丰富的组件和图表支持，适合后台管理系统 国际化支持，文档详细 支持 TypeScript，社区活跃 适合：需要构建企业级应用或后台管理系统 官网：https://ant.design/\n4. Tailwind CSS + Headless UI 特点： Tailwind 是一个实用类优先的 CSS 框架，极大自由度 Headless UI 提供无样式的可访问 React 组件，方便自定义样式 设计风格极简且现代，适合打造独特界面 需要一定的 CSS 基础，灵活性高 适合：喜欢高度自定义设计，追求极简和现代感的开发者 官网：\n– Tailwind CSS: https://tailwindcss.com/\n– Headless UI: https://headlessui.dev/\n5. Semantic UI React 特点： 语义化的类名，易读易用 组件设计优雅，风格现代 适合快速开发，文档丰富 适合：喜欢语义化 HTML 和简洁设计的项目 官网：https://react.semantic-ui.com/\n总结建议 框架 设计风格 适用场景 学习曲线 主题定制 组件丰富度 Chakra UI 现代优雅 通用，快速开发 低 高 高 Material-UI Material Design 企业级，专业应用 中 高 非常高 Ant Design 企业级简洁 后台管理系统 中 高 非常高 Tailwind+Headless UI 极简现代，自定义自由 设计师主导，个性化界面 中高 极高 依赖自定义 Semantic UI 语义化简洁 快速开发，语义化需求 低 中 中 如果你想要快速上手且界面优雅，我推荐你从 Chakra UI 开始；如果你喜欢 Google 的设计语言，选择 Material-UI 会很合适；如果是企业后台项目，Ant Design 是非常成熟的选择。\n更多的选择建议 框架 适合场景 设计风格 学习曲线 备注 Chakra UI 快速开发，现代优雅 现代优雅 低 主题定制和暗黑模式支持 Material-UI 企业级应用，Google 风格 Material Design 中 组件丰富，生态完善 Ant Design 企业后台，管理系统 企业级简洁 中 国际化支持，组件丰富 Tailwind CSS 高度自定义，设计师主导 极简现代 中高 需要写更多样式类 React Bootstrap 传统 Bootstrap 风格，兼容性好 经典实用 低 适合熟悉 Bootstrap 的开发者 Blueprint.js 复杂桌面应用，数据密集型界面 专业简洁 中 适合数据密集型应用 Grommet 企业和设计师，响应式和无障碍 现代灵活 中 设计现代，支持无障碍 Evergreen 企业级现代应用 现代企业风格 中 由 Segment 维护 Rebass 极简轻量，快速构建 极简风格 低 基于 Styled System ","permalink":"https://blog.dontalk.org/posts/%E6%8E%A8%E8%8D%90%E4%BA%9B-%E7%8E%B0%E4%BB%A3%E5%8C%96%E7%9A%84%E6%94%AF%E6%8C%81react%E7%9A%84%E4%BC%98%E9%9B%85%E7%BE%8E%E4%B8%BD%E7%9A%84css%E6%A1%86%E6%9E%B6/","summary":"因为我经常会对劳烦于 CSS ，烦躁到宁愿自己手写原生的 CSS 。我想，我们需要一些适合自己的 CSS 框架。这个文章列举一些还不错的 CSS 框架，方便大家(我)作挑选。\n1. Chakra UI 特点： 现代化设计，默认主题优雅且美观 组件高度可定制，支持暗黑模式 内置响应式设计，易于使用的样式系统 完全基于 React 组件，支持 …","title":"推荐些-现代化的，支持React的，优雅美丽的CSS框架"},{"content":"前端 – React 本文对 React 基本语法不作任何解释，如幽灵标签与闭合等等。。只解释疑难杂症和项目上手\n前端使用 React 挂载数据。使用 Tailwind CSS框架。 创建项目 – 官方(不推荐) 先不要跟着做\n❯ npx create-next-app@latest Need to install the following packages: create-next-app@15.3.2 Ok to proceed? (y) y ✔ What is your project named? … xxxxx ✔ Would you like to use TypeScript? … No✅ / Yes ✔ Would you like to use ESLint? … No✅ / Yes ✔ Would you like to use Tailwind CSS? … No / Yes✅ ✔ Would you like your code inside a `src/` directory? … No / Yes✅ ✔ Would you like to use App Router? (recommended) … No / Yes✅ ✔ Would you like to use Turbopack for `next dev`? … No✅ / Yes ✔ Would you like to customize the import alias (`@/*` by default)? … No✅ / Yes ...... ... 项目创建完毕后，我们打开 package.json 文件，可以看到下面内容\n{ \u0026#34;name\u0026#34;: \u0026#34;项目名字\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;0.1.0\u0026#34;, \u0026#34;private\u0026#34;: true, \u0026#34;scripts\u0026#34;: { \u0026#34;dev\u0026#34;: \u0026#34;PORT=3005 next dev\u0026#34;, // 更改这一行。在前面添加 PORT=3005 \u0026#34;build\u0026#34;: \u0026#34;next build\u0026#34;, \u0026#34;start\u0026#34;: \u0026#34;next start\u0026#34;, \u0026#34;lint\u0026#34;: \u0026#34;next lint\u0026#34; }, \u0026#34;dependencies\u0026#34;: { \u0026#34;react\u0026#34;: \u0026#34;^19.0.0\u0026#34;, \u0026#34;react-dom\u0026#34;: \u0026#34;^19.0.0\u0026#34;, \u0026#34;next\u0026#34;: \u0026#34;15.3.2\u0026#34; }, \u0026#34;devDependencies\u0026#34;: { \u0026#34;@tailwindcss/postcss\u0026#34;: \u0026#34;^4\u0026#34;, \u0026#34;tailwindcss\u0026#34;: \u0026#34;^4\u0026#34; } } 我们需要按照上面一样更改，因为 3000 端口在本地开发环境里被 Express 占用了，所以我们就用 3005 端口吧\n之后进入目录，启动项目就行\nnpm run dev 项目目录结构： . └── XXXX # 项目名 ├── README.md # 项目详情 ├── src/app # 核心目录，用于存放页面、布局和全局样式等内容 │ ├── favicon.ico │ ├── globals.css │ ├── layout.js │ └── page.js ├── jsconfig.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs └── public # 存放静态资源（如图片、字体等）此目录文件，编译时不会重命名 ├── file.svg ├── globe.svg ├── next.svg ├── vercel.svg └── window.svg 可以看到，项目目录有点云里雾里的，有些东西不知道如何存放才好。\n创建项目 – Vite(推荐) – 支持各种前端框架创建 如果你无意参加编程语言的派别之争，那么使用 Vite 去创建 React 无疑是非常好的选择，尤其面对新手。\n兼容性说明:\nVite 需要 版本 18+ 或 20+。但是，某些模板需要更高的 Node.js 版本才能运行，如果您的包管理器出现警告，请升级。\nVite 官网: https://vite.dev/guide/\n❯ npm create vite@latest Need to install the following packages: create-vite@6.5.0 Ok to proceed? (y) y \u0026gt; npx \u0026gt; create-vite │ ◇ Project name: │ XXXX │ ◇ Select a framework: │ React │ ◇ Select a variant: │ JavaScript + SWC │ ◇ Scaffolding project in /path/to/project... │ └ Done. Now run: cd XXXX npm install npm run dev 项目目录结构 看起来好多了\n. └── XXXX ├── README.md ├── eslint.config.js ├── index.html # index 躯壳页面 - jsx最终挂载上这里(main.jsx) ├── package.json ├── public # 存放静态资源（如图片、字体等）此目录文件，编译时不会重命名 │ └── vite.svg # 非 require 方式引入(如src)的内容(如LOGO)，不可以变更名字 ├── src # 源码文件 │ ├── App.css # 对应了 App 这个页面的CSS(但ReactCSS不是你想那么简单) │ ├── App.jsx # 代码的根，根路由，它提供给了 main.jsx 挂载 - 整理路由和逻辑用 │ ├── assets # 资源地址(这里的文件编译时会被重命名) │ │ └── react.svg │ ├── index.css # main.jsx 的CSS文件 │ └── main.jsx # 挂载文件 - 挂载上index.html - (真正的)index 文件 └── vite.config.js 5 directories, 11 files main.jsx 和 App.jsx 的关系 (重要) 说实话，这两个真的把人整得云里雾里的，这里说明一下。\nmain.jsx – 项目的入口文件 main.jsx 是项目的入口文件？很奇怪对吧，毕竟写惯了程序，入口文件不应该给定参数，调取参数吗？事实上，我也被这个概念搞得晕头转向。所以，你只需要知道，这个文件是最终挂载，渲染根路由(App.jsx)，或者更多的 大组件 到 index.html 就行\n为什么要说 大组件 …我都要哭了，因为那些路由之类的处理，一般都是 App.jsx 这个根组件完成就行。除非你的项目足够大型和复杂，不然一个 App.jsx ，之后 App.jsx 递交给 main.jsx 挂载到 index.html 就行了。\nmain.jsx – 它负责挂载 App.jsx 到 HTML 的根节点（index.html）\n作用： 初始化 React 应用。 将 React 应用挂载到 HTML 中的根节点（通常是 \u0026lt;div id=\u0026quot;root\u0026quot;\u0026gt;）。 引入全局样式文件（如 index.css）。 渲染整个 React 应用的根组件（App）。 核心职责： 负责启动 React 应用： 使用 ReactDOM.createRoot() 创建 React 的根节点。 使用 .render() 方法，将 React 的根组件（App）渲染到页面中。 引入全局资源： 加载全局样式（如 index.css）。 加载应用的根组件（App.jsx）。 App.jsx – React 应用的根组件 作用： 定义整个应用的核心逻辑和页面结构。 包含路由配置（如 React Router 的 Routes 和 Route）。 组织并引入其他子组件（如 Header、Footer、Home 等）。 可以包含全局状态管理（如 Context API 或 Redux 的 Provider）。 核心职责： 负责布局和页面逻辑： 定义页面的整体结构（如头部、导航、主内容区）。 配置页面路由，加载不同的页面组件。 组织组件： 引入其他子组件，并将它们组合起来构建应用的 UI。 控制全局状态： 如果使用 Context API、Redux 等全局状态管理工具，通常会在 App.jsx 中初始化状态管理。 我们进入目录，执行下面命令即可启动 React 测试服务器\nnpm install npm run dev ## 不需要额外更改端口，因为vite默认使用 5173 这个端口 - (自带热更新) 删除一些东西 你可以启动服务器，并且熟悉一下项目目录。但是出于开发一个新的页面目标出发，我们需要删除一些东西。\n. └── XXXX ├── README.md ├── eslint.config.js ├── index.html # 里面一些东西可以看着修改(如Title) ├── package.json ├── public # 存放静态资源（如图片、字体等）此目录文件，编译时不会重命名 │ └── vite.svg # LOGO，删除即可 ├── src # 源码文件 │ ├── App.css # 删除文件里面的代码 │ ├── App.jsx # 删除 return 里面的代码(以及App函数里面一行const) │ ├── assets # 资源地址(这里的文件编译时会被重命名) │ │ └── react.svg # react图标，删除即可 │ ├── index.css # 删除文件里面的代码 │ └── main.jsx # 无需删除任何，但是之后会作改变 └── vite.config.js 5 directories, 11 files 安装React-Router(路由 ) Vite 创建项目时，一般不会询问你是否引入 React-Router 功能，所以我们需要自己安装\n我们需要使用到 页面路由 (现代网页开发就几乎没有不用的)，所以需要安装：\nnpm install react-router-dom 页面路由，比如说在页面快速切换 -主页 -关于我们 -我的 功能页，而不重新刷新网页。\n基本语法和项目文件增减在后面安装完 CSS 框架后说\n不使用CSS框架 / 自己写一些CSS (警告⚠️) 为什么是重要呢，因为 React 导入的 CSS 是全局的。这意味着，你在一个 pages/xxx.jsx 导入的 CSS 文件，将会被全局应用到 App.jsx, main.jsx 等等文件，一旦出现 选择器(class等) 名称相同，就会被 CSS规则命中。\n所以你必须(最好)这样做： 创建 CSS 文件时，不要使用 .css 结尾，要使用 .module.css 结尾(或者.module.scss) 在 jsx 源码文件，这样导入 CSS：import importStyles from './xxxx.module.css'; 在代码中，这样命名元素：\u0026lt;button className={importStyles.button}\u0026gt;\n这就是最原汁原味的用法。 当然你也可以使用 CSS-in-JS，安装库：npm install styled-components\n如果，你在该页面下，没有用到多个 CSS 文件，而且 ClassName 没有重合，可以直接这样导入：\nimport \u0026#39;./xxxx.module.css\u0026#39;; CSS框架(TailwindCSS / Bootstrap5) 尽管它们使用起来，体验感都很相似，都是使用一堆类名，但是 Tailwind CSS 比 Bootstrap5 安装起来要麻烦些，不过 Tailwind CSS 更加现代好看。其实我也挺喜欢 Bootstrap5 的设计的，具体看个人，本文使用 Tailwind CSS。\n2025.08.19 – v1.4(增量版)(不并入pdf) – 有关CSS最新内容 现代网页开发 – 前端与CSS框架的关系。如何快速开发一个好看实用的页面，CSS框架(组件库)的正确用法。 CSS框架 事实上就是给你快速构建网站用的，也包括功能。比如说CSS框架提供了一个导航栏，你只需要在他们官网把代码复制下来，之后更改关键点就好了。比如说下面这个框架，就提供了很多功能\nTailwindCSS 和 BootStrap5(引入) 比较频繁使用类名，但是如果希望快速开发网页的话，并且代码不混乱(很多类名)，CSS框架的精髓就应该是标签。如下面的css，如果我们需要卡片样式，就引入并使用 \u0026lt;Card\u0026gt; 标签，他们提供一些属性来DIY。这一点 AntDesign 做得很好。\nAntDesign – https://ant.design/\nui.shadcn – https://ui.shadcn.com/docs/\n例如，我们想要构建一个 图片轮询/个人信息 页面，只需要把 CSS框架 里面提供的组件复制到本地，之后写 fetch 从后端取得数据，填入 CSS框架的“萝卜坑” 里就行了。\n现代网页开发就是那么简单。\n派别之争 但这其实是一个派别之争，我在 现代网页开发 教程里面书写的习惯是 – 类名 – 功能优先(Utility-First) 而本文的习惯是 标签 – 组件优先(Component-First)。\n如果你希望你的代码不要出现太多类名导致混乱，并且更加快速地构建一个完美，好看的页面，那就可以试试 标签 – 组件优先(Component-First)。\n你也可以亲切将这种 CSS框架 称之为 CSS组件库。\nTailwind CSS：核心思想是Utility-First（功能优先）。它的精髓在于类名。你不需要 Card 标签，只需要使用一系列原子化的类名（如 flex, bg-white, rounded-lg）来直接构建卡片。你无法直接使用 \u0026lt;Card\u0026gt; 标签，你需要自己组合类名来创建卡片样式。\nBootstrap 5：介于两者之间，但更偏向 组件优先。它提供了像 .card、.btn 这样的组件类，你只需要在你的 \u0026lt;div\u0026gt; 或 \u0026lt;button\u0026gt; 上添加这些类，就可以得到一个预设样式的组件。它也有一些功能类（如 d-flex），但其核心仍然是组件类。\nAnt Design：核心思想是 Component-First（组件优先）。它的精髓在于标签。它提供了像 \u0026lt;Card\u0026gt;、\u0026lt;Button\u0026gt; 这样的 React 组件。你不需要关心背后的 CSS 类名，只需要引入这些组件，然后通过它们的属性（props）来定制样式和行为，例如 \u0026lt;Card title=\u0026quot;My Card\u0026quot;\u0026gt;。\n开发流程 找到合适的模块/组件：从 UI 框架或组件库中，找到你需要的模块或组件（如导航栏、表单、表格、按钮）。\n复制粘贴：将这些组件的代码粘贴到你的项目中。这些组件通常已经包含了样式和基础功能。\n获得数据：在组件内部，使用 fetch 或其他 HTTP 客户端库（如 Axios）向后端发起请求，获取数据。\n填充数据：将从后端获取的数据，通过 props 或状态管理，动态地填充到组件的相应部分。\n你会得到什么？\n这个流程让你能够专注于业务逻辑和数据流，而不用花费大量时间在 CSS 样式和基础组件的构建上。你可以把更多精力放在如何获取和展示数据，而不是如何让按钮居中或者如何设计一个美观的功能列表。\n这正是 CSS 框架(组件库)受欢迎的原因\n接着上文 CSS框架(TailwindCSS / Bootstrap5) (v1.3)\n安装 Tailwind CSS npm install tailwindcss @tailwindcss/vite 配置 Tailwind 编辑 vite.config.js 文件，指定 Tailwind 的内容扫描范围：\nimport { defineConfig } from \u0026#39;vite\u0026#39; import react from \u0026#39;@vitejs/plugin-react-swc\u0026#39; import tailwindcss from \u0026#39;@tailwindcss/vite\u0026#39; // 增加这一行 export default defineConfig({ plugins: [ react(), tailwindcss() // 增加这一行 ], }) 添加 Tailwind 样式 在 src/index.css 中导入 Tailwind 的基础样式即可 (头部插入)：\n@import \u0026#34;tailwindcss\u0026#34;; 即可应用 TailwindCSS\n安装 Bootstrap 5 npm install bootstrap 导入 Bootstrap 样式 在 src/main.jsx 中导入 Bootstrap 的 CSS 文件：\nimport \u0026#39;bootstrap/dist/css/bootstrap.min.css\u0026#39;; 使用 Bootstrap 样式 编辑 src/App.jsx，使用 Bootstrap 样式：\nfunction App() { return ( \u0026lt;div className=\u0026#34;container mt-5\u0026#34;\u0026gt; \u0026lt;h1 className=\u0026#34;text-primary\u0026#34;\u0026gt;Welcome to Bootstrap 5 in Vite!\u0026lt;/h1\u0026gt; \u0026lt;button className=\u0026#34;btn btn-primary\u0026#34;\u0026gt;Click Me\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; ); } export default App; 组件 – 现代网页开发 重要思维之一 在开始路由前，我们需要先讲组件。虽然网站是像剥洋葱一样剥开的，但是我们学的话，剥洋葱地学很容易乱，所以要从洋葱里面开始(有点像，把洋葱当成积木，拼起来)。\n什么是组件？ 在一个页面上面，有很多框框，比如说左边是你的头像和个人信息，中边是新闻，最右边是搜索。\n类似于…Twitter，Facebook 之类的。\n现代开发，组件的应用是必不可少的，重要的，先进的！\n导航栏和公告栏 – 组件 首先我们创建一个目录用于存放组件，src/components ，组件还有个优点就是，复用性。\n如果曾经有开发过网站的经验，应该都会就 每个页面都有的「导航栏」，「回到顶端」等功能，进行剥离，放到某个文件夹(如组件文件夹)，之后被各大页面复用(引用)。就无须10个页面，就写(复制粘贴)10次的导航栏。\n组件也就是这样的思维，不过因为 CSS 框架和前端框架的加持，会使得组件用起来，更加现代，优雅。\n我们在 src/components 下创建两个文件，分别为 src/components/AnnouncementBar.jsx (公告行)， src/components/Navbar.jsx (导航栏)\n公告栏 AnnouncementBar.jsx import React, { useState, useEffect } from \u0026#34;react\u0026#34;; const AnnouncementBar = () =\u0026gt; { const [announcement, setAnnouncement] = useState(\u0026#34;\u0026#34;); // 获取公告内容 useEffect(() =\u0026gt; { const fetchAnnouncement = async () =\u0026gt; { try { const response = await fetch(\u0026#34;http://localhost:3000/data\u0026#34;); // 后端 API 地址 const data = await response.json(); setAnnouncement(data.readData.announcement); // 剥离数据 } catch (error) { console.error(\u0026#34;Failed to fetch announcement:\u0026#34;, error); setAnnouncement(\u0026#34;无法加载公告，请稍后重试。\u0026#34;); // 错误时展示的默认消息 } }; fetchAnnouncement(); }, []); // 如果没有公告，则不显示组件 if (announcement == null) { return null; } return ( \u0026lt;div className=\u0026#34;bg-blue-500 text-white text-center py-2 px-4\u0026#34;\u0026gt; \u0026lt;p className=\u0026#34;text-sm font-medium\u0026#34;\u0026gt;{announcement}\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; ); }; export default AnnouncementBar; 导航栏 Navvar.jsx 代码用到一个 LOGO，logo位于 /public/logo.png，随便找一个替代即可。\nimport React, { useState } from \u0026#34;react\u0026#34;; // 子组件：箭头图标 const ArrowRightIcon = () =\u0026gt; ( \u0026lt;svg xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; viewBox=\u0026#34;0 0 20 20\u0026#34; fill=\u0026#34;currentColor\u0026#34; className=\u0026#34;w-5 h-5 ml-1\u0026#34; \u0026gt; \u0026lt;path fillRule=\u0026#34;evenodd\u0026#34; d=\u0026#34;M3 10a.75.75 0 01.75-.75h10.638L10.23 5.29a.75.75 0 111.04-1.08l5.5 5.25a.75.75 0 010 1.08l-5.5 5.25a.75.75 0 11-1.04-1.08l4.158-3.96H3.75A.75.75 0 013 10z\u0026#34; clipRule=\u0026#34;evenodd\u0026#34; /\u0026gt; \u0026lt;/svg\u0026gt; ); // 子组件：菜单图标 const MenuIcon = () =\u0026gt; ( \u0026lt;svg className=\u0026#34;block h-6 w-6\u0026#34; xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; fill=\u0026#34;none\u0026#34; viewBox=\u0026#34;0 0 24 24\u0026#34; stroke=\u0026#34;currentColor\u0026#34; aria-hidden=\u0026#34;true\u0026#34; \u0026gt; \u0026lt;path strokeLinecap=\u0026#34;round\u0026#34; strokeLinejoin=\u0026#34;round\u0026#34; strokeWidth=\u0026#34;2\u0026#34; d=\u0026#34;M4 6h16M4 12h16M4 18h16\u0026#34; /\u0026gt; \u0026lt;/svg\u0026gt; ); // 子组件：关闭图标 const CloseIcon = () =\u0026gt; ( \u0026lt;svg className=\u0026#34;block h-6 w-6\u0026#34; xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; fill=\u0026#34;none\u0026#34; viewBox=\u0026#34;0 0 24 24\u0026#34; stroke=\u0026#34;currentColor\u0026#34; aria-hidden=\u0026#34;true\u0026#34; \u0026gt; \u0026lt;path strokeLinecap=\u0026#34;round\u0026#34; strokeLinejoin=\u0026#34;round\u0026#34; strokeWidth=\u0026#34;2\u0026#34; d=\u0026#34;M6 18L18 6M6 6l12 12\u0026#34; /\u0026gt; \u0026lt;/svg\u0026gt; ); function Navbar() { const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); // 导航链接 const navLinks = [ { name: \u0026#34;主页\u0026#34;, href: \u0026#34;#\u0026#34; }, { name: \u0026#34;关于我们\u0026#34;, href: \u0026#34;#\u0026#34; }, { name: \u0026#34;测试页面\u0026#34;, href: \u0026#34;#\u0026#34; }, ]; // 公共样式 const linkStyle = \u0026#34;text-gray-700 hover:text-indigo-600 px-3 py-2 rounded-md text-sm font-medium\u0026#34;; // 渲染导航链接 const renderNavLinks = () =\u0026gt; navLinks.map((link) =\u0026gt; ( \u0026lt;a key={link.name} href={link.href} className={linkStyle}\u0026gt; {link.name} \u0026lt;/a\u0026gt; )); return ( \u0026lt;nav className=\u0026#34;bg-white shadow-sm\u0026#34;\u0026gt; \u0026lt;div className=\u0026#34;max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\u0026#34;\u0026gt; \u0026lt;div className=\u0026#34;flex items-center justify-between h-16\u0026#34;\u0026gt; {/* 左侧导航链接 */} \u0026lt;div className=\u0026#34;hidden md:flex space-x-4\u0026#34;\u0026gt;{renderNavLinks()}\u0026lt;/div\u0026gt; {/* 中间 Logo */} \u0026lt;div className=\u0026#34;flex-shrink-0\u0026#34;\u0026gt; \u0026lt;a href=\u0026#34;/\u0026#34; aria-label=\u0026#34;Home\u0026#34;\u0026gt; \u0026lt;img src=\u0026#34;/public/logo.png\u0026#34; alt=\u0026#34;Logo\u0026#34; className=\u0026#34;h-8 w-auto object-contain\u0026#34; /\u0026gt; \u0026lt;/a\u0026gt; \u0026lt;/div\u0026gt; {/* 右侧登录链接 */} \u0026lt;div className=\u0026#34;hidden md:flex items-center\u0026#34;\u0026gt; \u0026lt;a href=\u0026#34;#\u0026#34; className={`\u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;{linkStyle} flex items-center`}\u0026gt; Log in \u0026lt;ArrowRightIcon /\u0026gt; \u0026lt;/a\u0026gt; \u0026lt;/div\u0026gt; {/* 移动端菜单按钮 */} \u0026lt;div className=\u0026#34;md:hidden\u0026#34;\u0026gt; \u0026lt;button onClick={() =\u0026gt; setIsMobileMenuOpen(!isMobileMenuOpen)} className=\u0026#34;bg-white p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500\u0026#34; aria-expanded={isMobileMenuOpen} \u0026gt; \u0026lt;span className=\u0026#34;sr-only\u0026#34;\u0026gt;Toggle menu\u0026lt;/span\u0026gt; {isMobileMenuOpen ? \u0026lt;CloseIcon /\u0026gt; : \u0026lt;MenuIcon /\u0026gt;} \u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; {/* 移动端菜单 */} {isMobileMenuOpen \u0026amp;\u0026amp; ( \u0026lt;div className=\u0026#34;md:hidden\u0026#34;\u0026gt; \u0026lt;div className=\u0026#34;px-2 pt-2 pb-3 space-y-1\u0026#34;\u0026gt;{renderNavLinks()}\u0026lt;/div\u0026gt; \u0026lt;div className=\u0026#34;border-t border-gray-200 pt-4 pb-3\u0026#34;\u0026gt; \u0026lt;a href=\u0026#34;#\u0026#34; className={`\u0026lt;/span\u0026gt;{linkStyle} flex items-center`}\u0026gt; Log in \u0026lt;ArrowRightIcon /\u0026gt; \u0026lt;/a\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; )} \u0026lt;/nav\u0026gt; ); } export default Navbar; 挂载 完成上面的一切，现在要做的就是挂载了。\n但是我们需要挂载到路由上，但是路由页面还没有写，所以先放一放，把路由解决，之后挂载到路由上，再将路由挂载到 App.jsx(主应用) 上。\n层级关系 ｜ 「组件」 ==挂载==\u0026gt; 「路由页面」 ==挂载==\u0026gt; 「主页」(主应用)\nReact-Router (路由)使用 前后端路由 – 概念： 前后端路由是不同的，后端路由 指的是 API 接口，一般一个路由专注于做一件事，比如说注册路由，index路由，文章路由等等。\n前端路由 是为了在浏览器中实现无刷新，即时切换，无需每次点击一个页面都要更新一次页面。\n比如说，我们有一个页面，页面下面有三个分页，首页，关于我们，测试页面。那么在该页面下面点击分页，就在指定的区域加载分页出来，而不是 “跳转”，“重载” 整个页面。\n1. 创建页面组件 在 src/pages 文件夹中创建页面组件。\nHome.jsx (首页) import React from \u0026#34;react\u0026#34;; import AnnouncementBar from \u0026#39;../components/AnnouncementBar\u0026#39;; import Navbar from \u0026#39;../components/Navbar\u0026#39;; const Home = () =\u0026gt; { return ( \u0026lt;\u0026gt; \u0026lt;div\u0026gt; \u0026lt;AnnouncementBar /\u0026gt; \u0026lt;Navbar /\u0026gt; \u0026lt;div className=\u0026#34;h-screen bg-gradient-to-r from-blue-500 to-purple-500 flex items-center justify-center\u0026#34;\u0026gt; \u0026lt;div className=\u0026#34;text-center text-white\u0026#34;\u0026gt; \u0026lt;h1 className=\u0026#34;text-5xl font-bold mb-6\u0026#34;\u0026gt; Welcome to Our Application \u0026lt;/h1\u0026gt; \u0026lt;p className=\u0026#34;text-lg text-gray-200 max-w-xl mx-auto\u0026#34;\u0026gt; This is a modern React application powered by Tailwind CSS. Explorethe features and enjoy a seamless user experience. \u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/\u0026gt; ); }; export default Home; About.jsx (关于页) import React from \u0026#34;react\u0026#34;; import AnnouncementBar from \u0026#34;../components/AnnouncementBar\u0026#34;; import Navbar from \u0026#34;../components/Navbar\u0026#34;; const About = () =\u0026gt; { return ( \u0026lt;\u0026gt; \u0026lt;div\u0026gt; \u0026lt;AnnouncementBar /\u0026gt; \u0026lt;Navbar /\u0026gt; \u0026lt;div className=\u0026#34;min-h-screen flex flex-col items-center justify-center bg-white text-gray-800\u0026#34;\u0026gt; \u0026lt;h1 className=\u0026#34;text-4xl font-bold mb-4\u0026#34;\u0026gt;About Us\u0026lt;/h1\u0026gt; \u0026lt;p className=\u0026#34;text-lg text-gray-600 text-center max-w-2xl\u0026#34;\u0026gt; Welcome to our application! We are a team dedicated to delivering high-quality solutions that empower our users to achieve their goals. Our mission is to blend simplicity, beauty, and functionality in all that we do. \u0026lt;/p\u0026gt; \u0026lt;div className=\u0026#34;mt-6\u0026#34;\u0026gt; \u0026lt;a href=\u0026#34;https://example.com\u0026#34; target=\u0026#34;_blank\u0026#34; rel=\u0026#34;noopener noreferrer\u0026#34; className=\u0026#34;px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition\u0026#34;\u0026gt; Learn More \u0026lt;/a\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/\u0026gt; ); }; export default About; NotFound.jsx (404 页面) import React from \u0026#34;react\u0026#34;; import AnnouncementBar from \u0026#39;../components/AnnouncementBar\u0026#39;; import Navbar from \u0026#39;../components/Navbar\u0026#39;; import { Link } from \u0026#34;react-router-dom\u0026#34;; // 用来返回上一页（用a标签会刷新页面） const NotFound = () =\u0026gt; { return ( \u0026lt;\u0026gt; \u0026lt;div\u0026gt; \u0026lt;AnnouncementBar /\u0026gt; \u0026lt;Navbar /\u0026gt; \u0026lt;div className=\u0026#34;h-screen flex flex-col items-center justify-center bg-gray-100 text-gray-800\u0026#34;\u0026gt; \u0026lt;h1 className=\u0026#34;text-9xl font-bold text-blue-500\u0026#34;\u0026gt;404\u0026lt;/h1\u0026gt; \u0026lt;h2 className=\u0026#34;text-2xl font-semibold mt-4\u0026#34;\u0026gt;Oops! Page not found.\u0026lt;/h2\u0026gt; \u0026lt;p className=\u0026#34;text-gray-500 mt-2\u0026#34;\u0026gt; Sorry, the page you are looking for doesn\u0026#39;t exist or has been moved. \u0026lt;/p\u0026gt; \u0026lt;Link to=\u0026#34;/\u0026#34; className=\u0026#34;mt-6 px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition\u0026#34;\u0026gt; Back to Home \u0026lt;/Link\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/\u0026gt; ); }; export default NotFound; 2. 定义路由结构 在 src/App.jsx 中挂载路由。\nApp.jsx import React from \u0026#39;react\u0026#39;; // 请注意，这条引用用了重命名，把 BrowserRouter 重命名成了 Router // import { BrowserRouter as Router, Routes, Route } from \u0026#34;react-router-dom\u0026#34;; import Home from \u0026#39;./pages/Home\u0026#39;; import About from \u0026#39;./pages/About\u0026#39;; import NotFound from \u0026#39;./pages/NotFound\u0026#39; import \u0026#39;./App.css\u0026#39;; const App = () =\u0026gt; { return ( \u0026lt;\u0026gt; \u0026lt;Router\u0026gt; \u0026lt;div\u0026gt; \u0026lt;Routes\u0026gt; \u0026lt;Route path=\u0026#39;/\u0026#39; element={\u0026lt;Home /\u0026gt;} /\u0026gt; \u0026lt;Route path=\u0026#39;/About\u0026#39; element={\u0026lt;About /\u0026gt;} /\u0026gt; \u0026lt;Route path=\u0026#39;/*\u0026#39; element={\u0026lt;NotFound /\u0026gt;} /\u0026gt; \u0026lt;/Routes\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/Router\u0026gt; \u0026lt;/\u0026gt; ); }; export default App; 上面为什么用 * 号？ 认真把下面看完。\n路由标签的区别。BrowserRouter、Routes和Route 上面的 BrowserRouter 被重命名为了 Router ，代码实际的情况是：\n... ... \u0026lt;BrowserRouter\u0026gt; \u0026lt;div\u0026gt; \u0026lt;Routes\u0026gt; \u0026lt;Route path=\u0026#39;/\u0026#39; element={\u0026lt;Home /\u0026gt;} /\u0026gt; \u0026lt;Route path=\u0026#39;/About\u0026#39; element={\u0026lt;About /\u0026gt;} /\u0026gt; \u0026lt;Route path=\u0026#39;/*\u0026#39; element={\u0026lt;NotFound /\u0026gt;} /\u0026gt; \u0026lt;/Routes\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/BrowserRouter\u0026gt; ... ... BrowserRouter 提供上下文 管理路由的核心逻辑，监听浏览器地址栏的变化（如用户切换页面、点击返回按钮）。 将当前 URL 状态传递给子组件（Routes 和 Route）。 Routes 定义规则集 收集所有的 Route 规则，检查当前路径是否匹配某条规则。 如果找到匹配的规则，就渲染对应的组件。 Route 定义具体规则 path：表示匹配的路径。 element：表示路径对应的组件。 例如： / 匹配 Home 组件。 /about 匹配 About 组件。 * 匹配所有未定义的路径（通常用作 404 页面）。 为什么用 * 号？ * 号匹配了所有的路径地址，但是因为我们没有定义这些路径，所以其他路径自然而然就返回不出来模版，那么就把这些被匹配到的所有、找不到模版的路径，统一返回 404(NotFound) 模版(路由)页面\n从 URL 到页面渲染的过程： 假设用户访问了 /About：\nBrowserRouter 检测 URL：\n– 当前路径是 /About。\n– 将 URL 状态传递给 Routes。 Routes 匹配路径：\n– 检查所有的 Route：\n– / 不匹配。\n– /About 匹配 – 返回 element属性 选择的模版。\n– *（404）不匹配 (因为上面找到有匹配的)。\n– 找到 /About 的匹配规则。 Route 渲染组件：\n– 根据 /About 的规则，渲染 \u0026lt;About /\u0026gt; 组件。 最终，浏览器显示 About 页面内容。\n假设用户访问了 /Axxt 不存在的路由(路径)：\nBrowserRouter 检测 URL：\n– 当前路径是 /Axxt。\n– 将 URL 状态传递给 Routes。 Routes 匹配路径：\n– 检查所有的 Route：\n– / 不匹配。\n– /About 不匹配。\n– *（404）匹配 – 都找不到匹配的吧，来吧，我来匹配你 。\n– 找不到 /Axxt 的匹配规则，那就匹配到: *。 Route 渲染组件：\n– 根据 * 的规则，渲染 \u0026lt;NotFound /\u0026gt; 组件。 最终，浏览器显示 NotFound 页面内容。\n3. 更改 导航组件 – 重要 我们需要更改导航组件 src/components/Navbar.jsx 的跳转地址和方式，目前跳转的地址是 # ，并且使用 \u0026lt;a href='#' \u0026gt;(a标签) 跳转，这是不可取的，因为 a 标签跳转，是会刷新页面的，这并不符合我们对于页面不刷新切换路由的需要\nimport React, { useState } from \u0026#34;react\u0026#34;; import { Link } from \u0026#34;react-router-dom\u0026#34;; // 导入 Link 组件，用于代替会刷新页面的 a 标签 // 子组件：箭头图标 const ArrowRightIcon = () =\u0026gt; ( \u0026lt;svg xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; viewBox=\u0026#34;0 0 20 20\u0026#34; fill=\u0026#34;currentColor\u0026#34; className=\u0026#34;w-5 h-5 ml-1\u0026#34; \u0026gt; \u0026lt;path fillRule=\u0026#34;evenodd\u0026#34; d=\u0026#34;M3 10a.75.75 0 01.75-.75h10.638L10.23 5.29a.75.75 0 111.04-1.08l5.5 5.25a.75.75 0 010 1.08l-5.5 5.25a.75.75 0 11-1.04-1.08l4.158-3.96H3.75A.75.75 0 013 10z\u0026#34; clipRule=\u0026#34;evenodd\u0026#34; /\u0026gt; \u0026lt;/svg\u0026gt; ); // 子组件：菜单图标 const MenuIcon = () =\u0026gt; ( \u0026lt;svg className=\u0026#34;block h-6 w-6\u0026#34; xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; fill=\u0026#34;none\u0026#34; viewBox=\u0026#34;0 0 24 24\u0026#34; stroke=\u0026#34;currentColor\u0026#34; aria-hidden=\u0026#34;true\u0026#34; \u0026gt; \u0026lt;path strokeLinecap=\u0026#34;round\u0026#34; strokeLinejoin=\u0026#34;round\u0026#34; strokeWidth=\u0026#34;2\u0026#34; d=\u0026#34;M4 6h16M4 12h16M4 18h16\u0026#34; /\u0026gt; \u0026lt;/svg\u0026gt; ); // 子组件：关闭图标 const CloseIcon = () =\u0026gt; ( \u0026lt;svg className=\u0026#34;block h-6 w-6\u0026#34; xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; fill=\u0026#34;none\u0026#34; viewBox=\u0026#34;0 0 24 24\u0026#34; stroke=\u0026#34;currentColor\u0026#34; aria-hidden=\u0026#34;true\u0026#34; \u0026gt; \u0026lt;path strokeLinecap=\u0026#34;round\u0026#34; strokeLinejoin=\u0026#34;round\u0026#34; strokeWidth=\u0026#34;2\u0026#34; d=\u0026#34;M6 18L18 6M6 6l12 12\u0026#34; /\u0026gt; \u0026lt;/svg\u0026gt; ); function Navbar() { const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); // 导航链接 ///////// 需要更改 ///////// const navLinks = [ { name: \u0026#34;主页\u0026#34;, path: \u0026#34;/\u0026#34; }, // 更改为前端路由页面的URL地址 { name: \u0026#34;关于我们\u0026#34;, path: \u0026#34;/About\u0026#34; }, // 与 App.js 相关，path 指向 { name: \u0026#34;测试页面\u0026#34;, path: \u0026#34;/NotFound\u0026#34; }, // 这里 NotFound 路径其实不存在，因为 App.js 里面定义的是 * 号，不过 * 匹配了所有地址，之后就返回了 404(NotFound)模版(路由)页面。 // 所以这里爱啥啥，但是因为是测试页面，所以直接采用 /NotFound 路径来保持美观罢了。 ]; // 公共样式 const linkStyle = \u0026#34;text-gray-700 hover:text-indigo-600 px-3 py-2 rounded-md text-sm font-medium\u0026#34;; // 渲染导航链接 ///////// 需要更改 ///////// const renderNavLinks = () =\u0026gt; navLinks.map((link) =\u0026gt; ( // 为了避免 原生a标签 的跳转(刷新)网页问题 \u0026lt;Link key={link.name} to={link.path} className={linkStyle}\u0026gt; {link.name} \u0026lt;/Link\u0026gt; // 这里的 a 标签换成 React-Route 提供的 Link 标签 )); return ( \u0026lt;nav className=\u0026#34;bg-white shadow-sm\u0026#34;\u0026gt; \u0026lt;div className=\u0026#34;max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\u0026#34;\u0026gt; \u0026lt;div className=\u0026#34;flex items-center justify-between h-16\u0026#34;\u0026gt; {/* 左侧导航链接 */} \u0026lt;div className=\u0026#34;hidden md:flex space-x-4\u0026#34;\u0026gt;{renderNavLinks()}\u0026lt;/div\u0026gt; {/* 中间 Logo */} \u0026lt;div className=\u0026#34;flex-shrink-0\u0026#34;\u0026gt; \u0026lt;Link to=\u0026#34;/\u0026#34; aria-label=\u0026#34;Home\u0026#34;\u0026gt; \u0026lt;img src=\u0026#34;/public/logo.png\u0026#34; alt=\u0026#34;Logo\u0026#34; className=\u0026#34;h-8 w-auto object-contain\u0026#34; /\u0026gt; \u0026lt;/Link\u0026gt; \u0026lt;/div\u0026gt; {/* 右侧登录链接 */} \u0026lt;div className=\u0026#34;hidden md:flex items-center\u0026#34;\u0026gt; \u0026lt;Link to=\u0026#34;#\u0026#34; className={`\u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;{linkStyle} flex items-center`}\u0026gt; Log in \u0026lt;ArrowRightIcon /\u0026gt; \u0026lt;/Link\u0026gt; \u0026lt;/div\u0026gt; {/* 移动端菜单按钮 */} \u0026lt;div className=\u0026#34;md:hidden\u0026#34;\u0026gt; \u0026lt;button onClick={() =\u0026gt; setIsMobileMenuOpen(!isMobileMenuOpen)} className=\u0026#34;bg-white p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500\u0026#34; aria-expanded={isMobileMenuOpen} \u0026gt; \u0026lt;span className=\u0026#34;sr-only\u0026#34;\u0026gt;Toggle menu\u0026lt;/span\u0026gt; {isMobileMenuOpen ? \u0026lt;CloseIcon /\u0026gt; : \u0026lt;MenuIcon /\u0026gt;} \u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; {/* 移动端菜单 */} {isMobileMenuOpen \u0026amp;\u0026amp; ( \u0026lt;div className=\u0026#34;md:hidden\u0026#34;\u0026gt; \u0026lt;div className=\u0026#34;px-2 pt-2 pb-3 space-y-1\u0026#34;\u0026gt;{renderNavLinks()}\u0026lt;/div\u0026gt; \u0026lt;div className=\u0026#34;border-t border-gray-200 pt-4 pb-3\u0026#34;\u0026gt; \u0026lt;Link to=\u0026#34;#\u0026#34; className={`\u0026lt;/span\u0026gt;{linkStyle} flex items-center`}\u0026gt; Log in \u0026lt;ArrowRightIcon /\u0026gt; \u0026lt;/Link\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; )} \u0026lt;/nav\u0026gt; ); } export default Navbar; 4. 挂载 React 应用 在 src/main.jsx 中使用 ReactDOM.createRoot 将 React 应用挂载到真实的 DOM 节点。\nmain.jsx import React from \u0026#34;react\u0026#34;; import ReactDOM from \u0026#34;react-dom/client\u0026#34;; import App from \u0026#34;./App\u0026#34;; import \u0026#34;./index.css\u0026#34;; ReactDOM.createRoot(document.getElementById(\u0026#34;root\u0026#34;)).render( \u0026lt;React.StrictMode\u0026gt; \u0026lt;App /\u0026gt; \u0026lt;/React.StrictMode\u0026gt; ); // 下面代码同样可行，不完整导入 React 和 ReactDOM ，只导入需要用的 React.StrictMode 和 ReactDOM.createRoot // import { StrictMode } from \u0026#39;react\u0026#39; // import { createRoot } from \u0026#39;react-dom/client\u0026#39; // import App from \u0026#39;./App.jsx\u0026#39; // import \u0026#39;./index.css\u0026#39; // createRoot(document.getElementById(\u0026#39;root\u0026#39;)).render( // \u0026lt;StrictMode\u0026gt; // \u0026lt;App /\u0026gt; // \u0026lt;/StrictMode\u0026gt;, // ); 总结说明一下 React Router 的路由挂载 – 路由标签 BrowserRouter: 是 React Router 的路由上下文组件，必须包裹在应用的最外层，负责管理路由的状态和历史记录。 通过它，子组件可以使用路由功能。 Routes: 用于定义路由规则，包含多个 Route。 Route: 定义每个路由的路径和对应的组件。 path 属性表示 URL 路径，element 属性表示要渲染的组件。 Link: 用于创建导航链接，类似于 HTML 的 \u0026lt;a\u0026gt; 标签，但不会刷新页面，而是通过 React Router 实现页面切换。 ReactDOM 的作用 – 挂载标签 ReactDOM 是 React 提供的一个库，用于将 React 组件渲染到真实的 DOM 节点中。\nReactDOM.createRoot:\n– 是 React 18 提供的新 API，用于创建 React 应用的根节点。\n– 替代了 React 17 中的 ReactDOM.render 方法，支持并发模式并提升性能。 挂载流程:\n– ReactDOM.createRoot(document.getElementById(\u0026quot;root\u0026quot;))：\n– 找到 HTML 页面中 id=\u0026quot;root\u0026quot; 的 DOM 节点，作为 React 应用的挂载点。\n– .render(\u0026lt;App /\u0026gt;)：\n– 将 React 的根组件（如 App）渲染到 root 节点中，启动整个应用。 作用:\n– 将 React 的虚拟 DOM 转换为真实 DOM，显示在浏览器中。\n– 实现 React 状态与真实 DOM 的同步更新，确保用户界面动态响应数据变化。 登陆路由 理解完成上面的东西，基本上也就已经入门前端开发了(吧？)\n所以下面就过一遍，写一个登陆路由和注册来对接后端，之后把修改一下 Home.jsx ，列出一下最近10条文章。\n⚠️警告：对于 后端路由 admin.js ，值得注意的是，管理员和普通用户在一个表，且没有区分的列。意味着普通用户 = 管理员\n所以我们要对数据库做出一些更改：下面有两个方案\n方案 1：管理员和普通用户放在同一个表 在用户表中添加一个 角色字段（如 role 或 is_admin），用来区分用户的身份。\n表结构示例 users +----+----------+----------------+----------+ | id | username | email | role | +----+----------+----------------+----------+ | 1 | Alice | alice@test.com | user | | 2 | Bob | bob@test.com | admin | | 3 | Carol | carol@test.com | user | +----+----------+----------------+----------+ role 字段： 可以存储用户的角色（如 user、admin 等）。 如果只是简单区分管理员和普通用户，可以用布尔值字段（如 is_admin）代替。 is_admin +----+----------+----------------+----------+ | id | username | email | is_admin | +----+----------+----------------+----------+ | 1 | Alice | alice@test.com | false | | 2 | Bob | bob@test.com | true | +----+----------+----------------+----------+ 优点 简单易维护： 所有用户信息都在一个表中，便于查询和管理。 便于扩展： 如果需要支持更多角色（如 editor, moderator 等），只需扩展 role 字段即可。 性能优化： 避免多表查询或关联操作，查询性能较高。 缺点 字段语义复杂： 如果用户的角色逻辑复杂，可能会导致 role 字段值过于多样，增加维护难度。 权限控制逻辑复杂： 需要在代码中根据 role 或 is_admin 动态判断权限，权限管理可能比较分散。 方案 2：管理员和普通用户分两个表 将管理员和普通用户分开存储在不同的表中，例如 users 和 admins。\n表结构示例 users +----+----------+----------------+ | id | username | email | +----+----------+----------------+ | 1 | Alice | alice@test.com | | 2 | Carol | carol@test.com | +----+----------+----------------+ admins +----+----------+----------------+ | id | username | email | +----+----------+----------------+ | 1 | Bob | bob@test.com | +----+----------+----------------+ 优点 数据结构清晰： 管理员和普通用户分开存储，逻辑更直观，权限逻辑更容易管理。 便于权限扩展： 如果管理员的数据结构与普通用户差异较大，可以为管理员单独设计专属字段，而不影响普通用户的结构。 缺点 查询复杂度增加： 查询用户数据时需要判断是在 users 表还是 admins 表，增加开发复杂度。 冗余字段： 两个表可能会有大量重复字段（如 username、email 等），导致数据冗余。 扩展不灵活： 如果需要支持更多的角色（如 editor, moderator），可能需要创建更多的表，数据设计变得不够灵活。 正文 – 认证 你是你(管理员) 如果你希望有更多的鉴权应用场景，那多用户身份无疑是最好的选择。但是我的网站只会有用户和管理员之分，所以我用的是 「真或假」。\n我们取用 方案一 ，我们需要在 Users 这个数据库里面加入一个列(role)，直接采用 布尔值 的方式区分管理员和普通用户(毕竟只是小项目)，但是我建议直接通过 字段(列 – role) 的值来判断是否是管理员或者其他用户，这样的数据库和代码，对于后期权限的扩展，更加灵活。\nALTER TABLE users ADD COLUMN role BOOLEAN NOT NULL DEFAULT FALSE; 之后，把管理员的 role 字段，改成 1(True) 即可。\n我们需要在 /models/User.js 模型中增加这个列\n/models/User.js\nconst { DataTypes } = require(\u0026#39;sequelize\u0026#39;); const sequelize = require(\u0026#39;../database/database\u0026#39;); const User = sequelize.define(\u0026#39;User\u0026#39;, { id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true, }, username: { type: DataTypes.STRING, allowNull: false, unique: true, }, password: { type: DataTypes.STRING, allowNull: false, }, email: { type: DataTypes.STRING, allowNull: false, unique: true, }, is_member: { type: DataTypes.BOOLEAN, defaultValue: true, }, role: { // 追加刚刚增加的列 type: DataTypes.BOOLEAN, defaultValue: false, } }, { timestamps: true, }); module.exports = User; /routes/login.js 这个登陆路由是普通用户的…但是，我们也可以用。\n但是，/admin 需要改变了，可不能让那群低等的用户，进入我们高雅的上层大厅\n不好意思，刚刚被顶号了。好了，其实现代的网页程序，一般都不在后台再整一个登陆页面出来了，而是直接用户和管理员走一个登陆通道，但不同的是，普通用户访问「私人路由」，如「后台路由」这样的路由，会直接返回 404-NotFound 的页面，或者直接告诉你权限不足(前者明显安全性更高)。\n上面是笼统的说法，如果你希望深入了解为什么，可以往下看，否则跳过。\n1. 用户和管理员通常共用一个登录通道\n在现代的网页程序中，用户和管理员通常通过 同一个登录界面 进行认证。以下是这样设计的原因：\n为什么不区分登录页面？\n简化开发：\n– 使用统一的接口和登录逻辑可以减少重复开发工作，不需要为管理员单独设计一个登录页面或接口。\n– 用户和管理员的认证流程本质上是一样的（验证用户名/密码），区别在于登录后的权限控制。 安全性：\n– 如果管理员有独立的登录页面，会暴露出管理员专属的入口，可能会吸引攻击者进行暴力破解或其他攻击。\n– 共用一个登录入口，可以隐藏管理员角色，减少攻击的可能性。 用户体验：\n– 对于拥有多种角色的用户（如既是普通用户又是管理员），共用一个登录界面可以避免混淆。\n如何区分用户和管理员？\n– 登录成功后，后端会根据用户的 角色信息（如 role 字段或权限列表）返回对应的权限。\n– 前端根据返回的权限信息动态展示不同的页面或功能。例如：\n– 普通用户可以访问「用户中心」。\n– 管理员可以访问「后台管理」。 2. 权限管理：私人路由与后台路由的处理方式\n权限管理是现代网页程序中的核心部分，重点是如何处理 普通用户访问管理员路由 的情况。\n两种常见处理方式\n返回 404 页面（更安全的做法）：\n– 如果普通用户尝试访问管理员的专属路由（如 /admin/dashboard），直接返回 404 页面。\n– 这样可以隐藏管理员路由的存在，避免暴露后台系统的入口。\n– 优点：\n– 提高安全性，攻击者无法轻易探测出哪些路由是管理员路由。\n– 实现方式：\n– 后端根据用户权限控制是否返回路由内容。\n– 前端通过路由守卫（如 React Router 的 PrivateRoute）拦截未授权用户。\n**示例（前端实现）**： const PrivateRoute = ({ component: Component, ...rest }) =\u0026gt; { const userRole = getUserRole(); // 从全局状态或后端获取用户角色 return ( \u0026lt;Route {...rest} render={(props) =\u0026gt; userRole === \u0026#34;admin\u0026#34; ? ( \u0026lt;Component {...props} /\u0026gt; ) : ( \u0026lt;Redirect to=\u0026#34;/404\u0026#34; /\u0026gt; // 未授权用户重定向到 404 页面 ) } /\u0026gt; ); }; 返回权限不足提示： 如果普通用户访问了管理员路由，显示一个提示页面（如「权限不足」或「无权访问」）。 优点： 提供明确的信息反馈，用户知道自己没有权限访问该页面。 \u0026lt;li\u0026gt; \u0026lt;strong\u0026gt;缺点\u0026lt;/strong\u0026gt;： \u0026lt;ul\u0026gt; \u0026lt;li\u0026gt; 通过返回权限不足的页面，攻击者可以推测出后台路由的存在，从而增加暴力破解的风险。 \u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;/li\u0026gt; \u0026lt;li\u0026gt; \u0026lt;strong\u0026gt;实现方式\u0026lt;/strong\u0026gt;： \u0026lt;ul\u0026gt; \u0026lt;li\u0026gt; 后端在返回路由数据时，校验用户权限，如果无权限，返回 403 错误（Forbidden）。\u0026lt;br /\u0026gt; \u0026lt;strong\u0026gt;哪种方式更好？\u0026lt;/strong\u0026gt; \u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; – 返回 404 页面：适用于对安全性要求较高的系统（如企业管理系统、金融系统）。\n– 返回权限不足提示：适用于对用户体验要求更高的系统（如电商平台、内容管理系统）。\n3. 前端与后端的权限分工\n后端权限控制\n– 后端是权限控制的核心，因为前端的代码可能会被攻击者反编译和修改。\n– 后端需要对每个请求进行严格的权限校验：\n– 验证用户的登录状态和角色。\n– 如果用户无权访问某个资源，直接返回 403（权限不足）或 404（资源不存在）。\n前端权限控制\n– 前端的权限控制主要是提升用户体验，避免不必要的页面加载和请求。\n– 常见的前端权限控制方法：\n– 路由守卫：\n– 在用户访问某些路由前，校验其权限，如果无权限则重定向到 404 页面或显示权限不足提示。\n– 动态菜单渲染：\n– 根据用户权限动态渲染页面菜单和功能按钮，普通用户看不到管理员特有的功能。\n4. 补充：如何提升安全性？\n隐藏敏感路由：\n– 不要在前端代码中暴露管理员路由（如 /admin/dashboard）。\n– 使用后端返回的动态路由表，让前端根据权限动态渲染路由。 使用中间件校验权限：\n– 在后端通过中间件（如 Express 中的 middleware）统一校验请求权限，避免每个路由都重复实现权限逻辑。 限制敏感信息的暴露：\n– 后端接口只返回当前用户有权限访问的数据。\n– 对管理员的接口请求进行严格的身份验证（如 IP 白名单、二次认证等）。 5. 总结\n现代网页程序中通常设计为：\n用户和管理员共用一个登录通道，登录后通过权限字段区分身份。 对普通用户访问管理员专属路由的处理：\n– 安全性优先：返回 404 页面，隐藏路由存在。\n– 用户体验优先：显示权限不足提示。\n推荐：\n– 如果安全性是首要考虑（如管理系统），优先选择返回 404 页面。\n– 如果用户体验更重要（如电商、内容管理系统），可以返回权限不足提示。 通过后端权限校验 + 前端路由守卫的组合，既能保证安全性，又能提升用户体验。\n方案一 首先，我们更改一下 routes/login.js 下发的 Token ，因为它只携带了，user.id 和 user.username ，我们需要多携带一个 role ，之后「私人路由」解析Token，就可以获得一个 role 值，之后对这个值进行判断就行。\nconst express = require(\u0026#39;express\u0026#39;); const bcrypt = require(\u0026#39;bcrypt\u0026#39;); const { User } = require(\u0026#39;../models\u0026#39;); const { generateToken } = require(\u0026#39;../utils/jwt\u0026#39;); const router = express.Router(); router.post(\u0026#39;/\u0026#39;, async (req, res) =\u0026gt; { const { username, password } = req.body; try { // 查找用户 const user = await User.findOne({ where: { username } }); if (!user) { return res.status(404).json({ error: \u0026#39;User not found\u0026#39; }); } // 验证密码 const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { return res.status(401).json({ error: \u0026#39;Invalid credentials\u0026#39; }); } // 生成 JWT ////////// 追加 user.role ////////// const token = generateToken({ id: user.id, username: user.username, role: user.role }); res.json({ token }); } catch (error) { console.error(error); res.status(500).json({ error: \u0026#39;Something went wrong\u0026#39; }); } }); module.exports = router; 之后我解释一下 routes/admin.js\nconst express = require(\u0026#39;express\u0026#39;); const { authenticateJWT } = require(\u0026#39;../middlewares/auth\u0026#39;); const router = express.Router(); // authenticateJWT 这个中间件解密了 Token, 并通过了 req.user = decoded 这种方式赋值 // 我们通过 req.user.role 的方式去解析出 role 字段 router.get(\u0026#39;/\u0026#39;, authenticateJWT, (req, res) =\u0026gt; { console.log(req.user.role); // 可以打印出 role 情况 res.json({ message: \u0026#39;Welcome to the admin panel\u0026#39;, user: req.user }); }); module.exports = router; 带 Token(管理员) 访问后，返回的 数据 就是这样的：\n{ \u0026#34;message\u0026#34;: \u0026#34;Welcome to the admin panel\u0026#34;, \u0026#34;user\u0026#34;: { \u0026#34;id\u0026#34;: 1, \u0026#34;username\u0026#34;: \u0026#34;admin\u0026#34;, \u0026#34;role\u0026#34;: true, \u0026#34;iat\u0026#34;: 174xxxx359, \u0026#34;exp\u0026#34;: 174xxxx759 } } 带 普通用户Token 访问后，返回的 数据 就是这样的：\n{ \u0026#34;error\u0026#34;: \u0026#34;Pages Not Found.\u0026#34; } 下面是 routes/admin.js 布尔值版本(因为我的数据库设计是布尔值的)：\nconst express = require(\u0026#39;express\u0026#39;); const { authenticateJWT } = require(\u0026#39;../middlewares/auth\u0026#39;); const router = express.Router(); // authenticateJWT 这个中间件解密了 Token router.get(\u0026#39;/\u0026#39;, authenticateJWT, (req, res) =\u0026gt; { // 对什么都不携带的嗅探返回 404 if (!req.user || !req.user.role) { return res.status(404).json({ error: \u0026#39;Pages Not Found.\u0026#39; }); }; // 对管理员返回 正常页面 if (req.user.role == true) { res.json({ message: \u0026#39;Welcome to the admin panel\u0026#39;, user: req.user }); }; // 对普通用户返回 404 if (req.user.role == false) { return res.status(404).json({ error: \u0026#39;Pages Not Found.\u0026#39; }); }; }); module.exports = router; 如果你希望有更多的鉴权应用场景，那多用户身份无非是最好的选择。但是我的网站只会有用户和管理员之分，所以我用的是 「真或假」。\n方案二 const express = require(\u0026#39;express\u0026#39;); const { authenticateJWT } = require(\u0026#39;../middlewares/auth\u0026#39;); const router = express.Router(); // authenticateJWT 中间件解密 Token router.get(\u0026#39;/\u0026#39;, authenticateJWT, (req, res) =\u0026gt; { // 检查 req.user 和 req.user.role 是否存在 if (!req.user || !req.user.role) { return res.status(403).json({ error: \u0026#39;Unauthorized: Invalid token or role missing\u0026#39; }); } // 判断角色 if (req.user.role === \u0026#39;admin\u0026#39;) { // 管理员角色 return res.json({ message: \u0026#39;Welcome to the admin panel\u0026#39;, user: req.user }); } else if (req.user.role === \u0026#39;user\u0026#39;) { // 普通用户角色 return res.status(404).json({ error: \u0026#39;Pages Not Found.\u0026#39; }); } // 其他未知角色 return res.status(403).json({ error: \u0026#39;Forbidden: Role not recognized\u0026#39; }); }); module.exports = router; React 登陆页面 我们需要写一个登陆页面，使用测试工具测试 Token 都要自己手动带上，我们写一个前端的登陆页面，需要完成 Token 的自携带才行。\n自动携带 Token 可以通过 「Axios」请求拦截器 自动将 Token 添加到每次请求的 Authorization 请求头中。但是我选择用原生的 Fetch ，但是使用它，我们需要自己封装一个功能出来。\n设置 Token 到 Cookie 因为把 Token 设置到 LocalStorage 不安全，容易受到 XSS 攻击，所以为了支持 HttpOnly Cookie ，我们需要在服务器设置Cookie。\n之前的代码，直接 res.json({ token }); 是为了方便查看到返回的 Token ，之后做测试 ，而代码 投入使用后 ，直接 res.cookie 设置完 Token 到 Cookie 之后，返回 status:200, message:\u0026lsquo;Login Successful!\u0026rsquo; 就行了 。 routes/login.js\n...... ... // 验证密码 const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { return res.status(401).json({ error: \u0026#39;Invalid credentials\u0026#39; }); } // 生成 JWT const token = generateToken({ id: user.id, username: user.username, role: user.role }); // res.json({ token }); // 设置 HttpOnly Cookie res.cookie(\u0026#34;authToken\u0026#34;, token, { httpOnly: true, // 禁止 JavaScript 访问 secure: true, // 仅在 HTTPS 上传输 maxAge: 14400000, // Cookie 有效期 (4 小时 - 以毫秒为单位) sameSite: \u0026#34;Strict\u0026#34;, // 防止 CSRF 攻击 }); res.json({ message:`Login successful! ${user.username}` }) ... ...... 上面返回的一个 请求头 有 Set-Cookie ，浏览器就是靠这个设置 Cookie 的，之后会自己携带，如果失效了，就抛弃。\n重新设置 CORS 策略 以及 设置 Cookie-parser 如果不重新设置，将会被拦截，因为本来的 CORS 设置全局太过逆天，并且没有设置 Cookie 放行。如果不设置的话，浏览器将会拦截你的请求，故登陆失败。\n为什么 req.cookie 是未定义 Express 默认不会解析 cookie，需要手动使用 cookie-parser 中间件。 cookie-parser 会将 req.headers.cookie 中的原始字符串解析为一个对象，并将其挂载到 req.cookies。 安装：npm install cookie-parser 后端 app.js\nconst express = require(\u0026#39;express\u0026#39;); const bodyParser = require(\u0026#39;body-parser\u0026#39;); const cookieParser = require(\u0026#39;cookie-parser\u0026#39;); // Cookie 解析 const indexData = require(\u0026#39;./routes/siteintro\u0026#39;); const indexRoutes = require(\u0026#39;./routes/index\u0026#39;); const articleRoutes = require(\u0026#39;./routes/article\u0026#39;); const registerRoutes = require(\u0026#39;./routes/register\u0026#39;); const authRoutes = require(\u0026#39;./routes/login\u0026#39;); const adminRoutes = require(\u0026#39;./routes/admin\u0026#39;); const cors = require(\u0026#34;cors\u0026#34;); const app = express(); app.use(bodyParser.json()); app.use(cookieParser()); // 使用 cookie-parser 中间件 // // 全局启用 CORS // app.use(cors()); // CORS 配置 app.use( cors({ origin: \u0026#34;http://localhost:5173\u0026#34;, // 前端地址 credentials: true, // 允许携带 Cookie }) ); // 路由 app.use(\u0026#39;/\u0026#39;, indexRoutes); app.use(\u0026#39;/data\u0026#39;, indexData); app.use(\u0026#39;/article\u0026#39;, articleRoutes); app.use(\u0026#39;/register\u0026#39;, registerRoutes); app.use(\u0026#39;/login\u0026#39;, authRoutes); app.use(\u0026#39;/admin\u0026#39;, adminRoutes); const PORT = 3000; app.listen(PORT, async () =\u0026gt; { console.log(`Server running on http://localhost:${PORT}`); }); 前端 示例代码 当访问需要身份验证的后端路由时，例如 /api/protected，前端仍然需要设置 credentials: \u0026quot;include\u0026quot;，让浏览器自动携带 HttpOnly Cookie。\n登陆请求示例代码： const login = async (username, password) =\u0026gt; { try { const response = await fetch(\u0026#34;http://localhost:3000/login\u0026#34;, { method: \u0026#34;POST\u0026#34;, headers: { \u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;, }, body: JSON.stringify({ username, password }), // 发送用户名和密码 credentials: \u0026#34;include\u0026#34;, // 确保携带和存储 HttpOnly Cookie }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || \u0026#34;Login failed\u0026#34;); } // 登录成功 alert(\u0026#34;Login successful!\u0026#34;); } catch (error) { console.error(\u0026#34;Login error:\u0026#34;, error.message); alert(error.message); } }; 访问受保护的接口示例代码： const fetchProtectedData = async () =\u0026gt; { try { const response = await fetch(\u0026#34;http://localhost:3000/api/protected\u0026#34;, { method: \u0026#34;GET\u0026#34;, credentials: \u0026#34;include\u0026#34;, // 自动携带 HttpOnly Cookie (重要) }); if (response.status === 401) { // 如果服务器返回 401，说明 Token 已过期 alert(\u0026#34;Session expired. Redirecting to login...\u0026#34;); window.location.href = \u0026#34;/login\u0026#34;; // 跳转到登录页面 return; } if (!response.ok) { throw new Error(\u0026#34;Failed to fetch protected data\u0026#34;); } const data = await response.json(); console.log(\u0026#34;Protected data:\u0026#34;, data); } catch (error) { console.error(\u0026#34;Error fetching protected data:\u0026#34;, error.message); } }; 全局处理 Token 过期问题 封装 fetch 工具 比如说前端的 src/api/customFetch.js\nconst customFetch = async (url, options = {}) =\u0026gt; { try { const response = await fetch(url, { ...options, credentials: \u0026#34;include\u0026#34;, // 自动携带 HttpOnly Cookie (重要) }); if (response.status === 401) { // Token 过期处理 alert(\u0026#34;Session expired. Redirecting to login...\u0026#34;); window.location.href = \u0026#34;/login\u0026#34;; // 跳转到登录页面 return; } return response; } catch (error) { console.error(\u0026#34;Network error:\u0026#34;, error.message); throw error; } }; export default customFetch; 使用封装的工具 在需要的路由页面，导入一下上面封装的工具\n... import { customFetch } from \u0026#34;../api/customFetch\u0026#34;; const fetchProtectedData = async () =\u0026gt; { const response = await customFetch(\u0026#34;http://localhost:3000/api/protected\u0026#34;, { method: \u0026#34;GET\u0026#34;, }); if (response.ok) { const data = await response.json(); console.log(\u0026#34;Protected data:\u0026#34;, data); } }; ... 后端获取 Token // 记得安装上面的 Cookie-parser // 因为 cookie 存在于 req.headers.cookie 中，而不是直接挂载在 req.cookie 上 const token = req.cookies.authToken; // 从 HttpOnly Cookie 中获取 Token 改写代码 – 后端 我们的中间件依赖 Authorization 头中的 Bearer Token。如果你使用的是 HttpOnly Cookie 传递 Token，那么中间件将无法从 Authorization 头中获取 Token。会直接 undefined ，之后报错401。\n所以，如果前后端通过 HttpOnly Cookie 传递 Token，可以在 req.cookies 中查找 Token\nApp.js 文件一览\nconst express = require(\u0026#39;express\u0026#39;); const bodyParser = require(\u0026#39;body-parser\u0026#39;); const cookieParser = require(\u0026#39;cookie-parser\u0026#39;); // Cookie 解析 const indexData = require(\u0026#39;./routes/siteintro\u0026#39;); const indexRoutes = require(\u0026#39;./routes/index\u0026#39;); const articleRoutes = require(\u0026#39;./routes/article\u0026#39;); const registerRoutes = require(\u0026#39;./routes/register\u0026#39;); const authRoutes = require(\u0026#39;./routes/login\u0026#39;); const adminRoutes = require(\u0026#39;./routes/admin\u0026#39;); const cors = require(\u0026#34;cors\u0026#34;); const app = express(); app.use(bodyParser.json()); app.use(cookieParser()); // 使用 cookie-parser 中间件 // CORS 配置 app.use( cors({ origin: \u0026#34;http://localhost:5173\u0026#34;, // 前端地址 credentials: true, // 允许携带 Cookie }) ); // 路由 app.use(\u0026#39;/\u0026#39;, indexRoutes); app.use(\u0026#39;/data\u0026#39;, indexData); app.use(\u0026#39;/article\u0026#39;, articleRoutes); app.use(\u0026#39;/register\u0026#39;, registerRoutes); app.use(\u0026#39;/login\u0026#39;, authRoutes); app.use(\u0026#39;/admin\u0026#39;, adminRoutes); const PORT = 3000; app.listen(PORT, async () =\u0026gt; { console.log(`Server running on http://localhost:${PORT}`); }); 修改一下中间件的 Token 获取方式\nmiddlewares/auth.js\nconst { verifyToken } = require(\u0026#39;../utils/jwt\u0026#39;); // 中间件：验证 JWT const authenticateJWT = (req, res, next) =\u0026gt; { const authHeader = req.headers.authorization; const token = authHeader \u0026amp;\u0026amp; authHeader.startsWith(\u0026#39;Bearer \u0026#39;) ? authHeader.split(\u0026#39; \u0026#39;)[1] // 从 Authorization 头中获取 Token : req.cookies?.authToken; // 或从 Cookie 中获取 Token // 因为我希望上面两种情况都支持，所以直接用三元表达式，如果获取不到 Authorization 的 Token ，再从 Cookie 中获取 if (!token) { return res.status(401).json({ error: \u0026#39;Unauthorized: No token provided\u0026#39; }); } try { const decoded = verifyToken(token); // 验证 JWT req.user = decoded; // 将用户信息附加到请求对象 next(); // 放行 } catch (err) { return res.status(401).json({ error: \u0026#39;Unauthorized: Invalid token\u0026#39; }); } }; module.exports = { authenticateJWT }; middlewares/verify.js\nconst { verifyToken } = require(\u0026#39;../utils/jwt\u0026#39;); function verifyLogin(req, res, next) { const authHeader = req.headers.authorization; const token = authHeader \u0026amp;\u0026amp; authHeader.startsWith(\u0026#39;Bearer \u0026#39;) ? authHeader.split(\u0026#39; \u0026#39;)[1] // 从 Authorization 头中获取 Token : req.cookies?.authToken; // 或从 Cookie 中获取 Token // 如果没有 Authorization 请求头，则视为未登录 if (!token) { console.log(\u0026#39;No Authorization header found\u0026#39;); req.user = null; return next(); }; // 提取 token（处理 Bearer 前缀） // const token = authHeader.startsWith(\u0026#39;Bearer \u0026#39;) ? authHeader.split(\u0026#39; \u0026#39;)[1] : authHeader; try { // 验证并解码 token const decoded = verifyToken(token); // 检查解码后的信息是否完整 if (!decoded || !decoded.username) { console.error(\u0026#39;Decoded token missing required fields:\u0026#39;, decoded); req.user = null; return next(); } req.user = decoded; // 将解码后的用户信息传递到后续中间件 next(); } catch (err) { console.error(\u0026#39;Token verification failed:\u0026#39;, err.message); // 打印具体错误信息 req.user = null; next(); }; }; module.exports = { verifyLogin }; 改写代码 – 前端 App.jsx 文件一览\nimport React from \u0026#39;react\u0026#39;; import { BrowserRouter as Router, Routes, Route } from \u0026#34;react-router-dom\u0026#34;; import Home from \u0026#39;./pages/Home\u0026#39;; import About from \u0026#39;./pages/About\u0026#39;; import NotFound from \u0026#39;./pages/NotFound\u0026#39; import LoginPage from \u0026#39;./pages/Login\u0026#39;; import Admin from \u0026#39;./pages/Admin\u0026#39;; import \u0026#39;./App.css\u0026#39;; const App = () =\u0026gt; { return ( \u0026lt;\u0026gt; \u0026lt;Router\u0026gt; \u0026lt;div\u0026gt; \u0026lt;Routes\u0026gt; \u0026lt;Route path=\u0026#39;/\u0026#39; element={\u0026lt;Home /\u0026gt;} /\u0026gt; \u0026lt;Route path=\u0026#39;/About\u0026#39; element={\u0026lt;About /\u0026gt;} /\u0026gt; \u0026lt;Route path=\u0026#39;/Login\u0026#39; element={\u0026lt;LoginPage /\u0026gt;} /\u0026gt; \u0026lt;Route path=\u0026#39;/Admin\u0026#39; element={\u0026lt;Admin /\u0026gt;} /\u0026gt; \u0026lt;Route path=\u0026#39;/*\u0026#39; element={\u0026lt;NotFound /\u0026gt;} /\u0026gt; \u0026lt;/Routes\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/Router\u0026gt; \u0026lt;/\u0026gt; ); }; export default App; /src/api/customFetch.js\nconst customFetch = async (url, options = {}) =\u0026gt; { try { const response = await fetch(url, { ...options, credentials: \u0026#34;include\u0026#34;, // 自动携带 HttpOnly Cookie (重要) }); if (!response.ok) { if (response.status === 401) { alert(\u0026#34;Session expired. Redirecting to login...\u0026#34;); window.location.href = \u0026#34;/login\u0026#34;; return; } else if (response.status === 403) { throw new Error(\u0026#34;Access denied: You do not have permission to access this resource.\u0026#34;); } else if (response.status === 404) { throw new Error(\u0026#34;Resource not found.\u0026#34;); } else { throw new Error(`Unexpected error: ${response.statusText}`); } } // 自动解析 JSON 数据 return await response.json(); } catch (error) { console.error(\u0026#34;Network error:\u0026#34;, error.message); throw error; } }; export default customFetch; /src/pages/Admin.jsx\nimport React, { useEffect, useState } from \u0026#34;react\u0026#34;; import customFetch from \u0026#34;../api/customFetch\u0026#34;; const Admin = () =\u0026gt; { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() =\u0026gt; { const getUserProfile = async () =\u0026gt; { try { // 用我们封装的 Fetch 去访问需要认证的 私人路由 (它的逻辑明确了带上Cookie，如果失效或者未找到，即重定向到 /Login 路由) const data = await customFetch(\u0026#34;http://localhost:3000/admin\u0026#34;, { method: \u0026#34;GET\u0026#34; }); if (!data || !data.user || data.user.role !== true) { throw new Error(\u0026#34;Access denied: You do not have permission to access this page.\u0026#34;); } setUser(data.user); } catch (error) { setError(error.message); if (error.message.includes(\u0026#34;Session expired\u0026#34;)) { window.location.href = \u0026#34;/login\u0026#34;; } } finally { setLoading(false); } }; getUserProfile(); }, []); if (loading) return \u0026lt;div\u0026gt;Loading...\u0026lt;/div\u0026gt;; if (error) return \u0026lt;div className=\u0026#34;text-red-500\u0026#34;\u0026gt;Error: {error}\u0026lt;/div\u0026gt;; return ( \u0026lt;div\u0026gt; \u0026lt;h1\u0026gt;Admin Dashboard\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;Welcome, {user?.username}!\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;Your role: {user?.role ? \u0026#34;Admin\u0026#34; : \u0026#34;User\u0026#34;}\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; ); }; export default Admin; 后记 我相信代码已经没什么好说的了，并且我在网站已经提供了整个项目的文件打包，包括代码。\n由 Dontalk 会员 – 【葵】 耗时4天编写 – 如有不对，敬请原谅和指正。\n如果你希望下载本文 PDF 以及配套代码，请前往「dl.dontalk.org」找到「现代网页开发入门指南书 – 从数据库设计，到后端，再到前端」即可下载。或者博文提到的 GitHub地址 也可以下载到副本 (包括本指南书的PDF档)。\nDontalk 地址：dontalk.org\nGithub 仓库地址(25年5月中)：https://github.com/infoabcd/Modern-Web-Development-Beginner-s-Guide/\n但是请注意，本作品采用 [署名-非商业性使用 4.0 国际 (CC BY-NC 4.0)] 协议发布。\n你可以自由转载、分发、分享，但必须注明作者及出处，不得用于任何商业用途。\n否则在沟通无果后，Dontalk 团队不介意采用法律武器捍卫权益。\n协议详情请见：https://creativecommons.org/licenses/by-nc/4.0/deed.zh\n","permalink":"https://blog.dontalk.org/posts/%E7%8E%B0%E4%BB%A3%E7%BD%91%E9%A1%B5%E5%BC%80%E5%8F%91%E5%85%A5%E9%97%A8%E6%8C%87%E5%8D%97%E4%B9%A6-react%E5%89%8D%E7%AB%AF/","summary":"前端 – React 本文对 React 基本语法不作任何解释，如幽灵标签与闭合等等。。只解释疑难杂症和项目上手\n前端使用 React 挂载数据。使用 Tailwind CSS框架。 创建项目 – 官方(不推荐) 先不要跟着做\n❯ npx create-next-app@latest Need to install the following …","title":"现代网页开发入门指南书 – React(前端)"},{"content":"后端 – NodeJS 后端用 Express 制作 API 。Sequelize 操作 数据库。 项目目录结构： ├── app.js # 整合文件 ├── database # 数据库-文件夹 │ └── database.js # 数据库初始化 ├── middlewares # 中间件-文件夹 │ └── auth.js # 认证中间件 ├── models # 数据库模型-文件夹 │ ├── Article.js # 文章模型 │ ├── User.js # 用户模型 │ └── index.js # 合并索引 ├── routes # 路由-文件夹 │ ├── admin.js # admin(后台)路由 │ └── login.js # login(登录)路由 ├── uploads # 上传-文件夹 │ └── image # 图片-文件夹 │ └── \u0026#34;Title\u0026#34; # 图片-分夹 │ ├── 12.jpg # 图片 │ ├── 34.jpg # 图片 │ ├── 56.jpg # 图片 │ └── 78.jpg # 图片 └── utils # 工具-文件夹 └── jwt.js # JWT功能 使用上面的目录结构，使得项目更加清晰，利于维护。但是缺点也很明显，就是东西肉眼可见的多。\n不喜欢这样，完全可以把 middlewares 和 utils 文件夹合并，database 和 models 文件夹合并。极端一点的话，可以把 route 写在一起，甚至是和 app.js 合并，把整个 models 和 database 合并为一。。。\n缺点就是，不方便维护，你需要在一个文件里面找东西，改代码。会有一种牵一发而动全身的即视感。\n不过也比所有东西写在一个 app.js 好。\n代码\u0026amp;说明 1. Sequelize 操作数据库 1.1 安装 Sequelize 和相关依赖 在项目中安装 Sequelize 和数据库驱动（以 MySQL 为例）：\nnpm install sequelize mysql2 1.2 初始化 Sequelize 创建一个 database.js 文件，用于配置和初始化 Sequelize。\nconst { Sequelize } = require(\u0026#39;sequelize\u0026#39;); // 创建 Sequelize 实例 const sequelize = new Sequelize(\u0026#39;database_name\u0026#39;, \u0026#39;username\u0026#39;, \u0026#39;password\u0026#39;, { host: \u0026#39;localhost\u0026#39;, // 数据库地址 dialect: \u0026#39;mysql\u0026#39;, // 数据库类型（mysql、postgres、sqlite 等） logging: false // 禁用 SQL 查询日志（可选） }); // 测试数据库连接 (async () =\u0026gt; { try { await sequelize.authenticate(); console.log(\u0026#39;Database connected successfully.\u0026#39;); } catch (error) { console.error(\u0026#39;Unable to connect to the database:\u0026#39;, error); } })(); // 下面代码是同步数据库的代码，如果你的数据库表没有创建的话，使用下面代码，那么ORM库会自动帮你“同步(创建)”出你定义模型的表。 (async () =\u0026gt; { try { await sequelize.sync({ alter: true }); // 根据模型更新表结构 console.log(\u0026#39;Database synchronized successfully.\u0026#39;); } catch (error) { console.error(\u0026#39;Error synchronizing database:\u0026#39;, error); } })(); //////// 不过由于已经手动创建了表，并插入了测试数据，所以请忽略。 //////// module.exports = sequelize; 1.3 定义模型 在 models/ 文件夹中创建模型文件，例如 models/User.js 和 models/Article.js。\n先简单引用说明一下：\nSTRING 默认长度就是 255，所以不需要再写 (255)。INTEGER 亦不需要加 (255)，整数类型在 Sequelize 中不会受长度限制，MySQL 8.0 也已废弃显示宽度。\n注意：DataTypes.INTEGER 接受的参数是 MySQL 的整数显示宽度（如 (12)），但 MySQL 8.0 及以上版本已经废弃了显示宽度，它不再影响存储或行为。 所以下面，STRING等，之后不再需要(255).\n在 Sequelize 中，使用 DataTypes.DATE 表示 DATETIME 类型。\n布尔值的默认行为：\nis_active: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true, // 默认值为 true }, 好了，我们切入正题\n用户模型：User 值得注意的是，后台路由 /admin 没有加入判断管理员的逻辑 ，并且这个用户模型也没有明确区分出管理员和普通用户的列(字段)，所以 /admin 可以直接被该表的用户登陆。可能会对系统造成不可估量的损失。\n解决方案：\n数据库\n– 把在该表后面增加多一个列(字段)，来判断是否管理员。（推荐）\n– 把管理员放在新的表。 路由\n– 在后台路由增加一个验证 (前端篇 后台路由 – 作进一步说明和解决)\nconst { DataTypes } = require(\u0026#39;sequelize\u0026#39;); const sequelize = require(\u0026#39;../database/database\u0026#39;); const User = sequelize.define(\u0026#39;User\u0026#39;, { id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true, }, username: { type: DataTypes.STRING, allowNull: false, unique: true, }, password: { type: DataTypes.STRING, allowNull: false, }, email: { type: DataTypes.STRING, allowNull: false, unique: true, }, // 如果填写不存在的列(因为没有使用sync同步)，所以会报错，导致查不出数据 is_member: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true, } }, { timestamps: true, // 自动创建 createdAt 和 updatedAt 字段 (由于没有书写 sync代码 所以需要手动在数据库里加入，之前加入的是created_at, 所以需要改名，否则不认，报错。再不然就false掉它(默认不填写 timestamps是true)) }); module.exports = User; 更改方式：\nALTER TABLE users CHANGE COLUMN created_at createdAt datetime; 文章模型：Article const { DataTypes } = require(\u0026#39;sequelize\u0026#39;); const sequelize = require(\u0026#39;../database/database\u0026#39;); const Article = sequelize.define(\u0026#39;Article\u0026#39;, { id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true, }, title: { type: DataTypes.STRING, allowNull: false, }, content: { type: DataTypes.TEXT, allowNull: false, }, publisher_id: { type: DataTypes.INTEGER, allowNull: false, } }, { timestamps: true, }); module.exports = Article; 图片模型：ArticleImage const { DataTypes } = require(\u0026#39;sequelize\u0026#39;); const sequelize = require(\u0026#39;../database/database\u0026#39;); const ArticleImage = sequelize.define(\u0026#39;ArticleImage\u0026#39;, { id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true, }, article_id: { type: DataTypes.INTEGER, allowNull: false, }, image_url: { type: DataTypes.STRING, allowNull: false, }, }, { timestamps: true, // 自动创建 createdAt 和 updatedAt 字段 }); module.exports = ArticleImage; 建立数据库关联 (v1.0版本 – 倾向理论) (v1.3说：非常建议读一下官方文档 – https://sequelize.org/docs/v6/core-concepts/assocs/ (或者中文文档))\n在 models/index.js 文件中定义模型间的关联关系，并且将它们一起暴露出去，给 app.js 使用。\n我们先来说说背景，可以看到上面有两个模型，分别是 User \u0026amp; Article 它们分别对应了 用户模型 \u0026amp; 文章模型 。那么我们如何将文章绑定给发布者呢？\n文章模型(Article) 处有一个 列 : 叫做 publisher_id 。而 用户模型(User) 处有一个 列 : 叫做 id(也是该表的主键)。用户名，email不可重复，每个用户都是独一无二的(这不废话吗)，所以每个用户只有一个 主键ID ，这个ID也因为用户只可能在这个表出现一次，从而是唯一的。\n那么这个 ID 就可以作为“快速识别码”，和文章模型(Article)的 publisher_id 相对应。比如说有 10 篇文章，publisher_id 都为 1，在 User 这个表里面，1 是 管理员的ID。那显而易见，这十篇文章都是管理员发的。\n我们要做的，就是在 index.js 整合它们，把它们一次性暴露给 app.js 。虽然 app.js 也可以一个个导入 Models 文件夹里面的模型，但是这种面对大型项目情况下，就会显得力不从心。\nconst User = require(\u0026#39;./User\u0026#39;); const Article = require(\u0026#39;./Article\u0026#39;); const ArticleImage = require(\u0026#39;./ArticleImage\u0026#39;); // 用户与文章：1对多关系 User.hasMany(Article, { foreignKey: \u0026#39;publisher_id\u0026#39;, onDelete: \u0026#39;CASCADE\u0026#39; }); Article.belongsTo(User, { foreignKey: \u0026#39;publisher_id\u0026#39; }); // 文章与图片：一对多关系 Article.hasMany(ArticleImage, { // 因为每个文章都是唯一的，图片应该和文章的 id 相对应，而非作者id。所以直接填写 article_id，直接使其匹配到 id 上。 foreignKey: \u0026#39;article_id\u0026#39;, onDelete: \u0026#39;CASCADE\u0026#39;, }); ArticleImage.belongsTo(Article, { foreignKey: \u0026#39;article_id\u0026#39;, }); module.exports = { User, Article, ArticleImage }; 或许你会有疑问：“users这个表里面没有publisher_id这一列啊，请问，上面代码的User的id是如何和publisher_id绑定在一起的呢？”\n虽然 users 表中没有 publisher_id 列，但这并不会影响外键绑定，因为外键绑定的核心不是字段是否存在于主表（users 表），而是从属表（articles 表）中的外键字段如何引用主表的主键。\n外键绑定的核心原理 在 Sequelize 中，外键绑定是通过以下逻辑实现的：\n主表（users）提供主键： users 表的主键是 id，这是默认的 Sequelize 主键字段。 主表（users）的主键将作为从属表（articles）中外键的目标。 从属表（articles）包含外键列： 在 articles 表中有一个列 publisher_id，用于存储与 users 表 id 主键对应的值。 articles.publisher_id 是从属表中的外键。 关联定义： 在 Sequelize 中使用 foreignKey 指定从属表的外键字段名（publisher_id），并将其绑定到主表的主键（默认是 id）。 例如： User.hasMany(Article, { foreignKey: \u0026#39;publisher_id\u0026#39; }); Article.belongsTo(User, { foreignKey: \u0026#39;publisher_id\u0026#39; }); 代码解释：\nUser.hasMany(Article, { foreignKey: \u0026#39;publisher_id\u0026#39;, onDelete: \u0026#39;CASCADE\u0026#39; }); Article.belongsTo(User, { foreignKey: \u0026#39;publisher_id\u0026#39; }); User.hasMany(Article)： 告诉 Sequelize，User 表的主键（id）将被关联到 Article 表的外键 publisher_id。 每个 User 可以有多个 Article。 Article.belongsTo(User)： 告诉 Sequelize，Article 表中的 publisher_id 字段引用了 User 表的主键（id）。 每个 Article 只能属于一个 User。 这种叫做「一对多关系」。 如果 User 模型没有 publisher_id 字段，而 Article 模型( articles表 )中有一个 publisher_id 外键列，Sequelize 会默认将 User 模型( users表 )的主键 id 与 Article.publisher_id 进行关联。\nCASCADE 是什么 这是个外键约束行为，onDelete: ‘CASCADE’ 表明了，如果这个用户删除(从数据库)，那么另外一个表下面的，有关他的所有文章，都一并删除掉。\n如果 users.userid 是随机生成的，如何与 articles.id 绑定？ 在这种情况下，需要将 articles 表中的外键字段设置为 userid，并通过 Sequelize 的 foreignKey 选项明确指定两者之间的关系。\n我们来假设一下情况：\nusers 表：主键是 userid，随机生成。\nuserid (主键) | username ------------------------ abcd1234 | Alice efgh5678 | Bob articles 表：外键是 userid，引用 users 表中的 userid。\nid (主键) | title | content | userid (外键) -------------------------------------------------------- 1 | First Article | Hello World | abcd1234 2 | Second Post | More Content | abcd1234 Sequelize 模型定义：通过 foreignKey 明确指定外键字段名称。\nconst User = require(\u0026#39;./User\u0026#39;); const Article = require(\u0026#39;./Article\u0026#39;); // 用户与文章：一对多关系 User.hasMany(Article, { foreignKey: \u0026#39;userid\u0026#39;, // 指定外键为 articles 表中的 userid onDelete: \u0026#39;CASCADE\u0026#39;, // 如果用户被删除，相关文章也会被删除 }); Article.belongsTo(User, { foreignKey: \u0026#39;userid\u0026#39;, // 指定外键为 articles 表中的 userid }); 发现什么问题了吗？外键必须设置和受到关联那个表的列的列名一样，使其保持一致 在数据库设计中，外键的列名应与其所引用的主表的主键保持一致，或者至少让它们的逻辑含义明确对应。\n建立数据库关联 (v1.3版本 – 独立的, 方便理解) 代码放在一个新的文件夹 – Operational Database\n(非常建议读一下官方文档 – https://sequelize.org/docs/v6/core-concepts/assocs/ (或者中文文档))\n数据库表结构(SQL语句)与(目录结构)： 我们有两个表，我们要对其进行关联。把表2(article_image) 关联给表1(article)\n其中，表1(article)的表结构是：\n+------------+------------------+------+-----+---------------------+---------+ | Field | Type | Null | Key | Default | Extra | +------------+------------------+------+-----+---------------------+---------+ | id | int(10) unsigned | NO | PRI | NULL | auto_increment | | created_at | timestamp | NO | | current_timestamp() | | | title | varchar(255) | NO | | NULL | | | content | text | NO | | NULL | | +------------+------------------+------+-----+---------------------+---------+ 4 rows in set (0.006 sec) --- SQL 语句 --- CREATE TABLE article ( id INT(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY, -- 主键，因为文章唯一，id也就肯定是唯一的，所以直接选用id作为主键。 created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, title VARCHAR(255) NOT NULL, content TEXT NOT NULL ); 其次，表2(article_image)的表结构是：\n+------------+------------------+------+-----+---------------------+---------+ | Field | Type | Null | Key | Default | Extra | +------------+------------------+------+-----+---------------------+---------+ | id | int(10) unsigned | NO | PRI | NULL | auto_increment | | created_at | timestamp | NO | | current_timestamp() | | | article_id | int(10) unsigned | NO | MUL | NULL | | | image_url | varchar(255) | NO | | NULL | | +------------+------------------+------+-----+---------------------+---------+ 4 rows in set (0.006 sec) --- SQL 语句 --- CREATE TABLE article_image ( id INT(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY, -- 主键 created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, article_id INT(10) UNSIGNED NOT NULL, -- 外键字段 image_url VARCHAR(255) NOT NULL, FOREIGN KEY (article_id) REFERENCES article(id) -- 引用主键 ON DELETE CASCADE ON UPDATE CASCADE ); 我们需要把 表1(article) 和 表2(article_image)关联在一起，方便我们查询。\n目录结构：\nproject/ ├── database/ │ └── database.js // 数据库连接配置 ├── models/ │ ├── Article.js // Article 模型定义 │ ├── ArticleImage.js // ArticleImage 模型定义 │ └── index.js // 模型入口文件，统一导出 ├── app.js // 主应用入口 把表1(article) – id(主键) 和 表2(article_image) – (article_id) 关联在一起. 代码编号为A1，存放在 Operational Database/A1\ndatabase/database.js:\n负责配置并导出 Sequelize 实例。\nconst { Sequelize } = require(\u0026#39;sequelize\u0026#39;); // 创建 Sequelize 实例 const sequelize = new Sequelize(\u0026#39;数据库名\u0026#39;, \u0026#39;数据库账号\u0026#39;, \u0026#39;数据库密码\u0026#39;, { host: \u0026#39;localhost\u0026#39;, dialect: \u0026#39;mysql\u0026#39;, logging: false, // 禁用 SQL 日志输出 }); // 测试数据库连接 (async () =\u0026gt; { try { await sequelize.authenticate(); console.log(\u0026#39;Database connected successfully.\u0026#39;); } catch (error) { console.error(\u0026#39;Unable to connect to the database:\u0026#39;, error); } })(); module.exports = sequelize; models/Article.js:\n定义 Article 模型。\nconst { DataTypes } = require(\u0026#39;sequelize\u0026#39;); const sequelize = require(\u0026#39;../database/database\u0026#39;); const Article = sequelize.define(\u0026#39;Article\u0026#39;, { id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, primaryKey: true, }, created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW, }, title: { type: DataTypes.STRING, allowNull: false, }, content: { type: DataTypes.TEXT, allowNull: false, }, }, { tableName: \u0026#39;article\u0026#39;, timestamps: false, // 禁用自动生成的 timestamps 字段 }); module.exports = Article; models/ArticleImage.js:\n定义 ArticleImage 模型。\nconst { DataTypes } = require(\u0026#39;sequelize\u0026#39;); const sequelize = require(\u0026#39;../database/database\u0026#39;); const ArticleImage = sequelize.define(\u0026#39;ArticleImage\u0026#39;, { id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, primaryKey: true, }, created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW, }, article_id: { type: DataTypes.INTEGER.UNSIGNED, allowNull: false, }, image_url: { type: DataTypes.STRING, allowNull: false, }, }, { tableName: \u0026#39;article_image\u0026#39;, timestamps: false, // 禁用自动生成的 timestamps 字段 }); module.exports = ArticleImage; models/index.js:\n统一导出所有模型，并设置关联关系。\nconst Article = require(\u0026#39;./Article\u0026#39;); const ArticleImage = require(\u0026#39;./ArticleImage\u0026#39;); // 定义关联关系 Article.hasMany(ArticleImage, { foreignKey: \u0026#39;article_id\u0026#39;, // 外键字段 sourceKey: \u0026#39;id\u0026#39;, // 主键字段 onDelete: \u0026#39;CASCADE\u0026#39;, // 设置级联删除 onUpdate: \u0026#39;CASCADE\u0026#39;, // 设置级联更新 }); ArticleImage.belongsTo(Article, { foreignKey: \u0026#39;article_id\u0026#39;, // 外键字段 targetKey: \u0026#39;id\u0026#39;, // 目标主键字段 }); module.exports = { Article, ArticleImage }; app.js:\n主应用入口文件，调用模型并同步数据库。\nconst sequelize = require(\u0026#39;./database/database\u0026#39;); const { Article, ArticleImage } = require(\u0026#39;./models\u0026#39;); (async () =\u0026gt; { try { // 同步数据库 await sequelize.sync({ alter: true }); // `alter: true` 会更新表结构 console.log(\u0026#39;数据库同步成功\u0026#39;); const newArticle = await Article.create({ title: \u0026#39;Test Article\u0026#39;, content: \u0026#39;This is a test article.\u0026#39;, }); await ArticleImage.create({ article_id: newArticle.id, image_url: \u0026#39;https://example.com/image1.jpg\u0026#39;, }); console.log(\u0026#39;数据插入成功\u0026#39;); // 查询测试 const articles = await Article.findAll({ include: [ { model: ArticleImage, required: false, // 即使没有关联图片，也返回文章 }, ], }); // 打印查询到的内容 console.log(JSON.stringify(articles, null, 2)); } catch (error) { console.error(\u0026#39;数据库同步失败:\u0026#39;, error); } })(); 返回数据： [ { \u0026#34;id\u0026#34;: 1, \u0026#34;created_at\u0026#34;: \u0026#34;2025-05-23T04:43:21.000Z\u0026#34;, \u0026#34;title\u0026#34;: \u0026#34;Test Article\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;This is a test article.\u0026#34;, \u0026#34;ArticleImages\u0026#34;: [ { \u0026#34;id\u0026#34;: 1, \u0026#34;created_at\u0026#34;: \u0026#34;2025-05-23T04:43:21.000Z\u0026#34;, \u0026#34;article_id\u0026#34;: 1, \u0026#34;image_url\u0026#34;: \u0026#34;https://example.com/image1.jpg\u0026#34; } ] } ] 获取文章及其图片\n通过 include 选项查询 Article 及其关联的 ArticleImage。\nconst { Article, ArticleImage } = require(\u0026#39;./models\u0026#39;); (async () =\u0026gt; { try { const articles = await Article.findAll({ include: [ { model: ArticleImage, required: false, // 即使没有关联图片，也返回文章 }, ], }); console.log(JSON.stringify(articles, null, 2)); } catch (error) { console.error(\u0026#39;Error fetching articles with images:\u0026#39;, error); } })(); 查询某个特定文章及其关联的图片\n通过 where 条件查询某篇特定文章及其关联的图片。\n(async () =\u0026gt; { const { Article, ArticleImage } = require(\u0026#39;./models\u0026#39;); try { const articleWithImages = await Article.findOne({ where: { id: 1 }, // 查询 ID 为 1 的文章 include: [ { model: ArticleImage, // 包含其关联的图片 }, ], }); console.log(\u0026#39;Article with Images:\u0026#39;, JSON.stringify(articleWithImages, null, 2)); } catch (error) { console.error(\u0026#39;Error fetching article by ID:\u0026#39;, error); } })(); 查询某张图片及其对应的文章\n通过 include 查询 ArticleImage 及其关联的 Article。\n(async () =\u0026gt; { const { Article, ArticleImage } = require(\u0026#39;./models\u0026#39;); try { const imageWithArticle = await ArticleImage.findOne({ where: { id: 1 }, // 查询 ID 为 1 的图片 include: [ { model: Article, // 包含其关联的文章 }, ], }); console.log(\u0026#39;Image with Article:\u0026#39;, JSON.stringify(imageWithArticle, null, 2)); } catch (error) { console.error(\u0026#39;Error fetching image by ID:\u0026#39;, error); } })(); 核心步骤 定义模型： Article 模型： 表名为 article，包含字段 id（主键）、created_at（创建时间）、title（文章标题）、content（文章内容）。 禁用自动生成的时间戳字段（timestamps: false）。 ArticleImage 模型： 表名为 article_image，包含字段 id（主键）、created_at（创建时间）、article_id（外键，关联到 Article 的 id）、image_url（图片 URL）。 同样禁用自动生成时间戳字段。 定义模型关联： Article 和 ArticleImage 通过 article_id 字段建立外键关联。 关系定义： Article.hasMany(ArticleImage)：表示一个文章可以有多张图片。 ArticleImage.belongsTo(Article)：表示每张图片属于一个文章。 设置外键约束： onDelete: 'CASCADE'：当文章被删除时，自动删除关联的图片。 onUpdate: 'CASCADE'：当文章的主键被更新时，自动更新关联的外键。 同步数据库： 使用 sequelize.sync({ alter: true }) 创建或更新数据库表结构。 确保模型定义和数据库结构保持一致。 数据插入和查询： 插入一篇文章后，插入与该文章关联的图片。 使用 findAll 方法查询文章及其关联的图片，支持即使没有图片也返回文章的结果（required: false）。 把表1(article) – release_level(主键) 和 表2(article_image) – (release_level) 关联在一起. 如果希望不是id，而在两个表都有一个“Release level”的列，通过里面的唯一值(随机6位数)来进行关联的话。\n代码编号为A2，存放在 Operational Database/A2\nSQL语句 – 不要一次性全部执行(会报错)\nDROP TABLE IF EXISTS article; -- 创建 Article 表 CREATE TABLE article ( id INT(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, title VARCHAR(255) NOT NULL, content TEXT NOT NULL, release_level CHAR(6) NOT NULL UNIQUE -- 唯一的 release_level 列 ); DROP TABLE IF EXISTS article_image; -- 创建 ArticleImage 表 CREATE TABLE article_image ( id INT(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, release_level CHAR(6) NOT NULL, -- 与 article.release_level 关联 image_url VARCHAR(255) NOT NULL, FOREIGN KEY (release_level) REFERENCES article(release_level) ON DELETE CASCADE ON UPDATE CASCADE ); database/database.js: – 不变\nmodels/Article.js\nconst { DataTypes } = require(\u0026#39;sequelize\u0026#39;); const sequelize = require(\u0026#39;../database/database\u0026#39;); const Article = sequelize.define(\u0026#39;Article\u0026#39;, { id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, primaryKey: true, }, created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW, }, title: { type: DataTypes.STRING, allowNull: false, }, content: { type: DataTypes.TEXT, allowNull: false, }, release_level: { type: DataTypes.CHAR(6), // 固定6字符长度 allowNull: false, unique: true, // 保证每个 release_level 唯一 }, }, { tableName: \u0026#39;article\u0026#39;, timestamps: false, }); module.exports = Article; models/ArticleImage.js\nconst { DataTypes } = require(\u0026#39;sequelize\u0026#39;); const sequelize = require(\u0026#39;../database/database\u0026#39;); const ArticleImage = sequelize.define(\u0026#39;ArticleImage\u0026#39;, { id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, primaryKey: true, }, created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW, }, release_level: { type: DataTypes.CHAR(6), // 固定6字符长度 allowNull: false, }, image_url: { type: DataTypes.STRING, allowNull: false, }, }, { tableName: \u0026#39;article_image\u0026#39;, timestamps: false, }); module.exports = ArticleImage; models/index.js\nconst Article = require(\u0026#39;./Article\u0026#39;); const ArticleImage = require(\u0026#39;./ArticleImage\u0026#39;); // 定义关联关系 Article.hasMany(ArticleImage, { foreignKey: \u0026#39;release_level\u0026#39;, // 通过 release_level 进行关联 sourceKey: \u0026#39;release_level\u0026#39;, // article 的 release_level onDelete: \u0026#39;CASCADE\u0026#39;, onUpdate: \u0026#39;CASCADE\u0026#39;, }); ArticleImage.belongsTo(Article, { foreignKey: \u0026#39;release_level\u0026#39;, // article_image 的 release_level targetKey: \u0026#39;release_level\u0026#39;, // article 的 release_level }); module.exports = { Article, ArticleImage }; app.js\nconst sequelize = require(\u0026#39;./database/database\u0026#39;); const { Article, ArticleImage } = require(\u0026#39;./models\u0026#39;); (async () =\u0026gt; { try { await sequelize.sync({ alter: true }); // 自动同步表结构 console.log(\u0026#39;数据库同步成功\u0026#39;); const releaseLevel = Math.random().toString(36).substring(2, 8).toUpperCase(); // 生成随机6位 release_level // 插入文章 const newArticle = await Article.create({ title: \u0026#39;Test Article\u0026#39;, content: \u0026#39;This is a test article.\u0026#39;, release_level: releaseLevel, // 插入随机 release_level }); // 插入关联图片 await ArticleImage.create({ release_level: releaseLevel, // 使用相同的 release_level 进行关联 image_url: \u0026#39;https://example.com/image1.jpg\u0026#39;, }); console.log(\u0026#39;数据插入成功\u0026#39;); } catch (error) { console.error(\u0026#39;数据库同步失败:\u0026#39;, error); } })(); 查询相关数据\nconst articlesWithImages = await Article.findAll({ include: [ { model: ArticleImage, required: false, // 即使没有关联图片也返回文章 }, ], }); console.log(JSON.stringify(articlesWithImages, null, 2)); 查询返回： [ { \u0026#34;id\u0026#34;: 1, \u0026#34;created_at\u0026#34;: \u0026#34;2025-05-23T04:32:56.000Z\u0026#34;, \u0026#34;title\u0026#34;: \u0026#34;Test Article\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;This is a test article.\u0026#34;, \u0026#34;release_level\u0026#34;: \u0026#34;V5D9SU\u0026#34;, \u0026#34;ArticleImages\u0026#34;: [ { \u0026#34;id\u0026#34;: 1, \u0026#34;created_at\u0026#34;: \u0026#34;2025-05-23T04:32:56.000Z\u0026#34;, \u0026#34;release_level\u0026#34;: \u0026#34;V5D9SU\u0026#34;, \u0026#34;image_url\u0026#34;: \u0026#34;https://example.com/image1.jpg\u0026#34; } ] } ] 总结： release_level 是一个随机生成的 6 位唯一标识符，用于关联 article 和 article_image 表。 在 Sequelize 中，通过 hasMany 和 belongsTo 关联两个模型，使用 release_level 作为外键字段。 外键的 ON DELETE CASCADE 和 ON UPDATE CASCADE 确保关联数据一致性。 把表1(article) – release_level(主键) 和 表2(article_image) – (level) 关联在一起. 假如Article表里面叫“Release level”，ArticleImage表里面叫“level”又如何关联\n代码编号为A3，存放在 Operational Database/A3\nSQL语句 – 不要一次性全部执行(会报错)\nDROP TABLE IF EXISTS article; -- 创建 Article 表 CREATE TABLE article ( id INT(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY, `Release level` CHAR(6) NOT NULL UNIQUE, -- 注意字段名带空格 title VARCHAR(255) NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); DROP TABLE IF EXISTS article_image; -- 创建 ArticleImage 表 CREATE TABLE article_image ( id INT(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY, level CHAR(6) NOT NULL, -- 关联到 article 的 `Release level` image_url VARCHAR(255) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (level) REFERENCES article(`Release level`) -- 外键关联 ON DELETE CASCADE ON UPDATE CASCADE ); database/database.js: – 不变\nmodels/Article.js\nconst { DataTypes } = require(\u0026#39;sequelize\u0026#39;); const sequelize = require(\u0026#39;../database/database\u0026#39;); const Article = sequelize.define(\u0026#39;Article\u0026#39;, { id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, primaryKey: true, }, releaseLevel: { // 映射到数据库中的 `Release level` type: DataTypes.CHAR(6), allowNull: false, unique: true, field: \u0026#39;Release level\u0026#39;, // 映射数据库字段名 }, title: { type: DataTypes.STRING, allowNull: false, }, content: { type: DataTypes.TEXT, allowNull: false, }, }, { tableName: \u0026#39;article\u0026#39;, timestamps: false, }); module.exports = Article; models/ArticleImage.js\nconst { DataTypes } = require(\u0026#39;sequelize\u0026#39;); const sequelize = require(\u0026#39;../database/database\u0026#39;); const ArticleImage = sequelize.define(\u0026#39;ArticleImage\u0026#39;, { id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, primaryKey: true, }, level: { // 映射到数据库中的 `level` type: DataTypes.CHAR(6), allowNull: false, }, imageUrl: { type: DataTypes.STRING, allowNull: false, field: \u0026#39;image_url\u0026#39;, // 映射数据库字段名 }, }, { tableName: \u0026#39;article_image\u0026#39;, timestamps: false, }); module.exports = ArticleImage; models/index.js\nconst Article = require(\u0026#39;./Article\u0026#39;); const ArticleImage = require(\u0026#39;./ArticleImage\u0026#39;); // 定义关联关系 Article.hasMany(ArticleImage, { foreignKey: \u0026#39;level\u0026#39;, // ArticleImage 的外键 sourceKey: \u0026#39;releaseLevel\u0026#39;, // Article 的目标键 onDelete: \u0026#39;CASCADE\u0026#39;, onUpdate: \u0026#39;CASCADE\u0026#39;, }); ArticleImage.belongsTo(Article, { foreignKey: \u0026#39;level\u0026#39;, // ArticleImage 的外键 targetKey: \u0026#39;releaseLevel\u0026#39;, // Article 的目标键 }); module.exports = { Article, ArticleImage }; app.js\nconst sequelize = require(\u0026#39;./database/database\u0026#39;); const { Article, ArticleImage } = require(\u0026#39;./models\u0026#39;); (async () =\u0026gt; { try { await sequelize.sync({ alter: true }); // 更新数据库表结构 console.log(\u0026#39;数据库同步成功\u0026#39;); const releaseLevel = \u0026#39;ABC123\u0026#39;; // 插入 Article 数据 const newArticle = await Article.create({ releaseLevel, // 自动映射到数据库的 `Release level` title: \u0026#39;Test Article\u0026#39;, content: \u0026#39;This is a test article.\u0026#39;, }); // 插入关联的 ArticleImage 数据 await ArticleImage.create({ level: releaseLevel, // 与 Article 的 releaseLevel 保持一致 imageUrl: \u0026#39;https://example.com/image1.jpg\u0026#39;, }); console.log(\u0026#39;数据插入成功\u0026#39;); } catch (error) { console.error(\u0026#39;数据库同步失败:\u0026#39;, error); } })(); 查询关联数据\nconst articlesWithImages = await Article.findAll({ include: [ { model: ArticleImage, required: false, // 即使没有关联图片也返回文章 }, ], }); console.log(JSON.stringify(articlesWithImages, null, 2)); 返回数据： [ { \u0026#34;id\u0026#34;: 1, \u0026#34;releaseLevel\u0026#34;: \u0026#34;ABC123\u0026#34;, \u0026#34;title\u0026#34;: \u0026#34;Test Article\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;This is a test article.\u0026#34;, \u0026#34;ArticleImages\u0026#34;: [ { \u0026#34;id\u0026#34;: 1, \u0026#34;level\u0026#34;: \u0026#34;ABC123\u0026#34;, \u0026#34;imageUrl\u0026#34;: \u0026#34;https://example.com/image1.jpg\u0026#34; } ] } ] 总结： 通过 field 属性，Sequelize 可以将模型字段与数据库中的字段名（即带空格的或不同的字段名）进行映射。 在模型关联中，通过设置 foreignKey 和 sourceKey 或 targetKey，可以灵活实现不同字段名的关联逻辑。 数据库中的 Release level 对应模型中的 releaseLevel，level 直接对应模型中的 level。 同步数据库 在主入口文件（如 app.js 或 index.js）中同步模型到数据库：\n看是否需要，在上面已经说过了，这里只做(再次)展示\nconst sequelize = require(\u0026#39;./database/database\u0026#39;); const { User, Article } = require(\u0026#39;./models\u0026#39;); // 同步数据库 (async () =\u0026gt; { try { await sequelize.sync({ alter: true }); // 根据模型更新表结构 console.log(\u0026#39;Database synchronized successfully.\u0026#39;); } catch (error) { console.error(\u0026#39;Error synchronizing database:\u0026#39;, error); } })(); 2. 添加 JWT 认证 2.1 安装 JWT 相关依赖 使用 jsonwebtoken 包进行 JWT 生成和验证：\nnpm install jsonwebtoken 2.2 配置 JWT 请注意，无论如何都不要泄漏密钥，密钥泄漏会导致 身份伪造，绕过，数据篡改，会话挟持等等问题！\n在项目中创建一个 utils/jwt.js 文件，封装 JWT 的生成和验证逻辑。\nconst jwt = require(\u0026#39;jsonwebtoken\u0026#39;); // 定义密钥和过期时间 const JWT_SECRET = \u0026#39;your_secret_key\u0026#39;; // 密钥 - 这个不应该被外人知道，否则会被伪造JWT const JWT_EXPIRES_IN = \u0026#39;1h\u0026#39;; // Token 有效期 // 生成 JWT const generateToken = (payload) =\u0026gt; { return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN }); }; // 验证 JWT const verifyToken = (token) =\u0026gt; { try { return jwt.verify(token, JWT_SECRET); } catch (error) { throw new Error(\u0026#39;Invalid or expired token\u0026#39;); } }; module.exports = { generateToken, verifyToken }; 2.3 创建认证中间件 在项目中创建一个 middlewares/auth.js 文件，定义 JWT 验证的中间件。\nconst { verifyToken } = require(\u0026#39;../utils/jwt\u0026#39;); // 中间件：验证 JWT const authenticateJWT = (req, res, next) =\u0026gt; { const authHeader = req.headers.authorization; // 检查是否存在 Bearer Token if (authHeader \u0026amp;\u0026amp; authHeader.startsWith(\u0026#39;Bearer \u0026#39;)) { const token = authHeader.split(\u0026#39; \u0026#39;)[1]; try { const decoded = verifyToken(token); // 验证 JWT req.user = decoded; // 将用户信息附加到请求对象 next(); // 放行(反弹回去路由代码那里)|记住，后面要考 } catch (err) { return res.status(401).json({ error: \u0026#39;Unauthorized: Invalid token\u0026#39; }); } } else { return res.status(401).json({ error: \u0026#39;Unauthorized: No token provided\u0026#39; }); } }; module.exports = { authenticateJWT }; 2.4 登录接口：生成 JWT 在路由文件(routes/login.js)中添加登录接口，用于生成 JWT。\n注意： 路由路径为 /，因为在 app.js 中，你已经将 authRoutes 挂载到 /login\nconst express = require(\u0026#39;express\u0026#39;); const bcrypt = require(\u0026#39;bcrypt\u0026#39;); const { User } = require(\u0026#39;../models\u0026#39;); const { generateToken } = require(\u0026#39;../utils/jwt\u0026#39;); const router = express.Router(); // 用户登录接口 router.post(\u0026#39;/\u0026#39;, async (req, res) =\u0026gt; { // 注意！！！这里路径为 ‘/’，因为在 整合文件 app.js 里面已经挂载到了login！ const { username, password } = req.body; try { // 查找用户 const user = await User.findOne({ where: { username } }); if (!user) { return res.status(404).json({ error: \u0026#39;User not found\u0026#39; }); } // 验证密码 const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { return res.status(401).json({ error: \u0026#39;Invalid credentials\u0026#39; }); } // 生成 JWT const token = generateToken({ id: user.id, username: user.username }); res.json({ token }); } catch (error) { res.status(500).json({ error: \u0026#39;Something went wrong\u0026#39; }); } }); module.exports = router; // 下面代码会发现，即便输入正确，但是都和数据库的密码匹配不上，因为每次的生成都不一样(加盐哈希)，所以不要手动加密再去比较。直接使用 bcrypt.compare 作比较就行。 // const saltRounds = 10; // 密码加盐次数 // const hashedPassword = await bcrypt.hash(password, saltRounds); // const isPasswordValid = await bcrypt.compare(hashedPassword, user.password); // 上面是错误的 // console.log(`输入的密码\u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;{password}`) // console.log(`输入的密码哈希\u0026lt;/span\u0026gt;{hashedPassword}`) // console.log(`数据库里的密码${user.password}`) //下面是正确的 //const isPasswordValid = await bcrypt.compare(password, user.password); 2.5 保护 /admin 接口 在 /admin 接口中使用 JWT 中间件，确保只有经过认证的用户可以访问。\n下面范例是，直接挂载到 /admin 这将意味着，你必须在 整合文件 app.js 挂到 /。(当然，挂载到其他地方也行，比如说 abc，那么情况就是：abc/admin)\nconst express = require(\u0026#39;express\u0026#39;); const { authenticateJWT } = require(\u0026#39;../middlewares/auth\u0026#39;); const router = express.Router(); // 受保护的 /admin 路由 router.get(\u0026#39;/admin\u0026#39;, authenticateJWT, (req, res) =\u0026gt; { res.json({ message: \u0026#39;Welcome to the admin panel\u0026#39;, user: req.user }); }); module.exports = router; // 上面的 authenticateJWT 的意思是，调用中间件，对应了 JWT 认证(auth.js)里面的 next()； 放行，返回到路由。 // 如果验证没有通过呢？当然就是不返回后面的 JSON - Welcome to the admin panel。 // 而是直接返回 auth.js 里面的错误信息 - 如: 验证不通过/不合法 3. 主文件整合 在 app.js 或 index.js 中整合上述功能：\nconst express = require(\u0026#39;express\u0026#39;); const bodyParser = require(\u0026#39;body-parser\u0026#39;); const authRoutes = require(\u0026#39;./routes/login\u0026#39;); const adminRoutes = require(\u0026#39;./routes/admin\u0026#39;); const app = express(); // 中间件 app.use(bodyParser.json()); // JSON 化数据，和数据库返回数据做比较用 // 路由 | 这里的挂载仅作演示 app.use(\u0026#39;/login\u0026#39;, authRoutes); // 登录路由 app.use(\u0026#39;/\u0026#39;, adminRoutes); // 受保护路由 // 启动服务器 const PORT = 3000; app.listen(PORT, async () =\u0026gt; { console.log(`Server running on http://localhost:${PORT}`); }); 登录认证 通过 PostMan 之类的程序进行测试，我们对 /login 路由进行登录测试看看。\n这样做：POST – localhost:3000/login\n之后在 Body 传入 raw – JSON\n{ \u0026#34;username\u0026#34;: \u0026#34;admin\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;admin\u0026#34; } 我已经在数据库手动插入了一个用户，账号密码都是 admin。\n之后我们将包发出，就会返回一个 Token。\n{ \u0026#34;token\u0026#34;: \u0026#34;eyJhbGciOiJIUxxxxxxxxxxxxCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hxxxxxxxxxxxxbiIsImlhdCI6MTc0NzM2NDg3MywiZXhwIjoxNzQ3Mzc5MjczfQ.NuAY7IN5OjDZWQxMeTxxxxxxxxxxxxorQrc7gu5GAcc\u0026#34; } 但是当我们再去访问 /admin 的时候，会发现未被授权，这是为什么呢。\n是因为我们虽然得到了下发的 Token，但是我们没有加进去 Authorization，所以报错。 平常的网站之所以不需要手动添加，是因为前端和浏览器帮你完成了这些工作。 而你自己写的后端API，测试阶段，都需要自己手动加上(为了测试API)。或者你可以直接搓一个前端出来测试，不过一般都是写完后端，再写前端的。\n我们在 Authorization 添加一个 Bearer Token ，之后把 Token填进去就行: eyJhbGciOiJIUxxxxxxxxxxxxCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hxxxxxxxxxxxxbiIsImlhdCI6MTc0NzM2NDg3MywiZXhwIjoxNzQ3Mzc5MjczfQ.NuAY7IN5OjDZWQxMeTxxxxxxxxxxxxorQrc7gu5GAcc\n这个时候再 GET /admin 就一切大吉了。\n{ \u0026#34;message\u0026#34;: \u0026#34;Welcome to the admin panel\u0026#34;, \u0026#34;user\u0026#34;: { \u0026#34;id\u0026#34;: 1, \u0026#34;username\u0026#34;: \u0026#34;admin\u0026#34;, \u0026#34;iat\u0026#34;: 174xxxx873, \u0026#34;exp\u0026#34;: 174xxxx273 } } 暴露文章 – Tittle和Content可见，Images需登录 想要实现这个功能，游客可以看到文章 title 和 content，但 images 需要注册登录才能看到，可以通过后端的权限控制和前端的条件渲染来实现。\n我们必须先温习一下 – 什么是中间件？ 在 Express.js 中，中间件是一个函数，用于处理请求（req）和响应（res）之间的逻辑。它还可以通过调用 next() 将控制权传递给下一个中间件。\n中间件的用途 处理请求和响应： 修改请求对象（req）或响应对象（res），例如解析请求体、添加用户信息等。 执行逻辑： 验证用户身份（如 verifyToken）。 检查权限。 日志记录。 控制请求流程： 决定请求是否继续（调用 next()）或终止（返回响应）。 中间件的意义 逻辑复用：避免在每个路由中重复写相同的逻辑（如身份验证）。 代码清晰：将复杂的功能分成小的、可维护的模块。 请求处理链：通过多个中间件组合，形成灵活的请求处理流程。 总结 用于处理请求、添加逻辑、控制流程，从而提高代码的复用性和可维护性。\n1. 后端实现权限控制 示例：基于 JWT 或 Session 验证 在登录完成时，会下发一个JWT，登录成功后，可以返回 username 之类的信息给浏览器(前端程序)去携带，通过判断是否有这个头，就可以判断用户是否登录，如果登录再展示这些信息。\n我们在 middlewares/verify.js 下面尝试读取用户是否有登录(是否有JWT，有则分离)\n// 复用 verifyToken 作为 Token 合法性校验和解密。 const { verifyToken } = require(\u0026#39;../utils/jwt\u0026#39;); function verifyLogin(req, res, next) { const authHeader = req.headers.authorization; // 如果没有 Authorization 请求头，则视为未登录 if (!authHeader) { console.log(\u0026#39;No Authorization header found\u0026#39;); req.user = null; return next(); } // 提取 token（处理 Bearer 前缀） const token = authHeader.startsWith(\u0026#39;Bearer \u0026#39;) ? authHeader.split(\u0026#39; \u0026#39;)[1] : authHeader; try { // 验证并解码 token const decoded = verifyToken(token); console.log(\u0026#39;Decoded Token:\u0026#39;, decoded); // 检查解码后的信息是否完整 if (!decoded || !decoded.username) { console.error(\u0026#39;Decoded token missing required fields:\u0026#39;, decoded); req.user = null; return next(); } req.user = decoded; // 将解码后的用户信息传递到后续中间件 next(); } catch (err) { console.error(\u0026#39;Token verification failed:\u0026#39;, err.message); // 打印具体错误信息 req.user = null; next(); } } module.exports = { verifyLogin }; routes/article.js 是无法复用之前的 verifyToken 中间件的，为什么？因为你会因为没有 Token 而被拦截，不放行。因为那个中间件是用来判断你是否登录，有无有效 Token ，否则不允许你访问被保护的 API 。如后台\nconst express = require(\u0026#39;express\u0026#39;); const { verifyLogin } = require(\u0026#39;../middlewares/verify\u0026#39;); // 验证登录中间件 const { Article } = require(\u0026#39;../models\u0026#39;) const router = express.Router(); // 获取文章详情 router.get(\u0026#39;/articles/:id\u0026#39;, verifyLogin, async (req, res) =\u0026gt; { const { id } = req.params; const user = req.user; // 登录信息通过 中间件 解析到 req.user try { const article = await Article.findByPk(id, { attributes: user ? [\u0026#39;title\u0026#39;, \u0026#39;content\u0026#39;, \u0026#39;images\u0026#39;] // 登录用户可以看到所有字段｜如果列(字段)不存在，会报500 : [\u0026#39;title\u0026#39;, \u0026#39;content\u0026#39;], // 游客只能看到 title 和 content }); if (!article) { return res.status(404).json({ message: \u0026#39;Article not found\u0026#39; }); } res.json(article); } catch (err) { res.status(500).json({ message: \u0026#39;Internal server error\u0026#39; }); } }); module.exports = router; 之后我们需要更新一下 app.js\nconst express = require(\u0026#39;express\u0026#39;); const bodyParser = require(\u0026#39;body-parser\u0026#39;); const authRoutes = require(\u0026#39;./routes/login\u0026#39;); const adminRoutes = require(\u0026#39;./routes/admin\u0026#39;); const articleRoutes = require(\u0026#39;./routes/article\u0026#39;) // 添加这行 const app = express(); // 中间件 app.use(bodyParser.json()); // 路由 app.use(\u0026#39;/login\u0026#39;, authRoutes); // 登录路由 app.use(\u0026#39;/\u0026#39;, adminRoutes); // 受保护路由 app.use(\u0026#39;/\u0026#39;, articleRoutes) // 添加这行 // 启动服务器 const PORT = 3000; app.listen(PORT, async () =\u0026gt; { console.log(`Server running on http://localhost:${PORT}`); }); 假如 Article 模型里对应的 articles 库存在一条数据：\nid: 1 title: hello content: neo images: /data/xxx/1.jpg 之后请求 GET /article/1 ，如果没登录，那么它应该返回\n{ \u0026#34;title\u0026#34;: \u0026#34;hello\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;neo\u0026#34; } 带上 Token 去 GET，则返回\n{ \u0026#34;title\u0026#34;: \u0026#34;hello\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;neo\u0026#34;, \u0026#34;images\u0026#34;: \u0026#34;/data/xxx/1.jpg\u0026#34; } 2. 前端实现条件渲染 示例：React API请求函数\nasync function fetchArticle(id, token = null) { const headers = token ? { Authorization: `Bearer \u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;{token}` } : {}; const response = await fetch(`/api/articles/\u0026lt;/span\u0026gt;{id}`, { headers }); return response.json(); } React组件\nimport React, { useEffect, useState } from \u0026#39;react\u0026#39;; function ArticlePage({ articleId, isLoggedIn, token }) { const [article, setArticle] = useState(null); useEffect(() =\u0026gt; { fetchArticle(articleId, isLoggedIn ? token : null).then(setArticle); }, [articleId, isLoggedIn, token]); if (!article) { return \u0026lt;div\u0026gt;Loading...\u0026lt;/div\u0026gt;; } return ( \u0026lt;div\u0026gt; \u0026lt;h1\u0026gt;{article.title}\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;{article.content}\u0026lt;/p\u0026gt; {isLoggedIn \u0026amp;\u0026amp; article.images \u0026amp;\u0026amp; ( \u0026lt;div\u0026gt; \u0026lt;h2\u0026gt;Images:\u0026lt;/h2\u0026gt; \u0026lt;img src={article.images} alt=\u0026#34;Article\u0026#34; /\u0026gt; \u0026lt;/div\u0026gt; )} {!isLoggedIn \u0026amp;\u0026amp; \u0026lt;p\u0026gt;Login to see the images.\u0026lt;/p\u0026gt;} \u0026lt;/div\u0026gt; ); } export default ArticlePage; 不过最好不要前端去做，因为抓包等一些原因，有可能会流出来。在一开始，也就是API，不要放出数据，更稳妥 —— 也就是通过中间件。\nAPI 加入注册和注册码校验 我们先创建两个文件，一个是 models/ActivationCode.js，一个是 routes/register.js。一个数据库模型，一个注册路由。\n以及需要在 models/index.js 追加内容，以暴露 ActivationCode 数据库模型。再在 app.js 挂载 routes/register.js 路由。\n数据库模型 models/ActivationCode.js const { DataTypes } = require(\u0026#39;sequelize\u0026#39;); const sequelize = require(\u0026#39;../database/database\u0026#39;); const ActivationCode = sequelize.define(\u0026#39;activation_code\u0026#39;, { id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true, }, code: { type: DataTypes.STRING, allowNull: false, unique: true, }, valid_day: { type: DataTypes.INTEGER, allowNull: false, }, is_permanent: { type: DataTypes.BOOLEAN, allowNull: true, defaultValue: false, }, is_used: { type: DataTypes.BOOLEAN, allowNull: true, }, user_by: { type: DataTypes.INTEGER, allowNull: true, }, }, { timestamps: true, }); module.exports = ActivationCode; 关于 数据表模型：\n– 邀请码数据模型 user_by 通过 用户ID 来标记。\n– is_used 的 1/0 用来表示 注册码 使用与否\n路由文件 routes/register.js const express = require(\u0026#39;express\u0026#39;); const { ActivationCode, User } = require(\u0026#39;../models\u0026#39;); const bcrypt = require(\u0026#39;bcrypt\u0026#39;); // 用于加密密码 const { Op } = require(\u0026#39;sequelize\u0026#39;); // 引入 Sequelize 运算符 const router = express.Router(); router.post(\u0026#39;/\u0026#39;, async (req, res) =\u0026gt; { const { username, password, email, code } = req.body; // 检查是否提供了激活码 if (!code) { return res.status(400).json({ message: \u0026#34;The invitation code is empty.\u0026#34; }); } // 下面提供了两个思路 // 用户名 直接用递交过来的 username(用户名) 为条件查询，看看是否存在。 // 激活码 先查询所有的激活码，之后与递交过来的 code(激活码) 做匹配。 try { // 检查用户名或邮箱是否已存在 const existingUser = await User.findOne({ where: { [Op.or]: [ // Sequelize 运算符，用于 OR 条件 { username: username }, { email: email } ] } }); // 如果用户已存在 if (existingUser) { return res.status(400).json({ message: \u0026#39;Username or email already exists\u0026#39; }); }; // 查询所有未使用的激活码 const aCode = await ActivationCode.findAll({ attributes: [\u0026#39;code\u0026#39;], where: { is_used: 0 }, }); // 提取激活码列表 const codes = aCode.map(item =\u0026gt; item.code); // 检查提供的激活码是否在未使用的激活码列表中 if (codes.includes(code)) { // 匹配成功，返回成功响应 // return res.json({ message: \u0026#39;DONE\u0026#39; }); // 匹配成功，激活码有效，保存用户信息 const hashedPassword = await bcrypt.hash(password, 10); // 加密密码 const newUser = await User.create({ username, password: hashedPassword, // 存储加密后的密码 email, }); // 更新激活码状态为已使用 await ActivationCode.update( { is_used: 1 }, // 设置为已使用 { where: { code: code } } ); return res.json({ message: \u0026#39;Successful registration!\u0026#39;, user: { // 返回新用户的相关信息，强化客户端体验 id: newUser.id, username: newUser.username, email: newUser.email, } }); } else { // 匹配失败，返回错误信息 return res.status(404).json({ message: \u0026#34;The invitation code is invalid or used.\u0026#34; }); } } catch (error) { console.error(error); res.status(500).json({ error: \u0026#39;Server Error\u0026#39; }); } }); module.exports = router; 关于代码的话，上面的注释已经够清楚，提供了两个判断思路，但是我认为前者更加合适。\n需要注意的是，在逻辑完成后，如果没有状态码(或直接return)，而直接 send json 回去的话，代码会继续执行下去的。所以一些 if 完毕后，要返回状态码。不要只是 send。\n但是如果没有任何 send 返回，也会导致测试工具(页面) 一直处于加载状态。所以在代码正确地走下去(一路畅通 if )的情况下，也要记得 send / return 数据回去。\n对于 非json对象 ，比如说一些普通的 “通告” ，要使用 message 。如：res.json({ message: 'Successful.' });\n绑定事务： 我认为这已经不算是 注意 了，应该是 ⚠️警告⚠️\n为什么需要警告？因为在创建用户和更新激活码的操作是独立的，如果在保存用户成功后，更新激活码状态失败，会导致激活码仍然可用，但用户已经注册成功。这种情况会产生数据不一致的问题。\n使用 Sequelize 的事务（transaction）确保两个操作要么同时成功，要么同时失败。\n路由文件 routes/register.js – 带绑定事务(这样才对) // 追加内容 const transaction = await sequelize.transaction(); // 开启事务 try { // 保存用户信息 const newUser = await User.create({ username, password: hashedPassword, email, }, { transaction }); // 更新激活码状态 await ActivationCode.update( { is_used: 1 }, { where: { code: code }, transaction } ); await transaction.commit(); // 提交事务 return res.status(201).json({ message: \u0026#39;User created successfully\u0026#39;, user: newUser }); } catch (error) { await transaction.rollback(); // 回滚事务 console.error(error); return res.status(500).json({ error: \u0026#39;Server Error\u0026#39; }); } 整体代码就是：\nconst express = require(\u0026#39;express\u0026#39;); const { ActivationCode, User, sequelize } = require(\u0026#39;../models\u0026#39;); const bcrypt = require(\u0026#39;bcrypt\u0026#39;); const { Op } = require(\u0026#39;sequelize\u0026#39;); const router = express.Router(); router.post(\u0026#39;/\u0026#39;, async (req, res) =\u0026gt; { const { username, password, email, code } = req.body; if (!code) { return res.status(400).json({ message: \u0026#34;The invitation code is empty.\u0026#34; }); } const transaction = await sequelize.transaction(); try { // 检查用户名或邮箱是否已存在 const existingUser = await User.findOne({ where: { [Op.or]: [ { username: username }, { email: email } ] } }); if (existingUser) { return res.status(409).json({ message: \u0026#39;Username or email already exists\u0026#39; }); } // 检查激活码是否有效 const validCode = await ActivationCode.findOne({ where: { code: code, is_used: 0 } }); if (!validCode) { return res.status(400).json({ message: \u0026#34;The invitation code is invalid or used.\u0026#34; }); } // 验证密码强度 if (password.length \u0026lt; 8) { return res.status(400).json({ message: \u0026#39;Password must be at least 8 characters long\u0026#39; }); } // 加密密码 const hashedPassword = await bcrypt.hash(password, 10); // 创建用户 const newUser = await User.create({ username, password: hashedPassword, email, }, { transaction }); // 更新激活码状态 await ActivationCode.update( { is_used: 1 }, { where: { code: code }, transaction } ); await transaction.commit(); // 提交事务 res.status(201).json({ message: \u0026#39;User created successfully\u0026#39;, user: { id: newUser.id, username: newUser.username, email: newUser.email, } }); } catch (error) { await transaction.rollback(); // 回滚事务 console.error(error); res.status(500).json({ error: \u0026#39;Server Error\u0026#39; }); } }); module.exports = router; 之后我们需要在 models/index.js 和 app.js 追加些东西\n追加模型暴露 models/index.js const User = require(\u0026#39;./User\u0026#39;); const Article = require(\u0026#39;./Article\u0026#39;); const ArticleImage = require(\u0026#39;./ArticleImage\u0026#39;); const ActivationCode = require(\u0026#39;./ActivationCode\u0026#39;); // 后添加 // 用户与文章：一对多关系 User.hasMany(Article, { foreignKey: \u0026#39;publisher_id\u0026#39;, onDelete: \u0026#39;CASCADE\u0026#39;, }); Article.belongsTo(User, { foreignKey: \u0026#39;publisher_id\u0026#39;, }); // 文章与图片：一对多关系 Article.hasMany(ArticleImage, { foreignKey: \u0026#39;article_id\u0026#39;, // 因为每个文章都是唯一的，图片应该和文章的 id 相对应，而非作者id。所以直接填写 article_id，直接使其匹配到 id 上。 onDelete: \u0026#39;CASCADE\u0026#39;, }); ArticleImage.belongsTo(Article, { foreignKey: \u0026#39;article_id\u0026#39;, }); // 后添加 - 激活码功能 // user_by 与 主键ID 绑定 User.hasMany(ActivationCode, { foreignKey: \u0026#39;user_by\u0026#39;, onDelete: \u0026#39;SET NULL\u0026#39;, }); ActivationCode.belongsTo(User, { foreignKey: \u0026#39;user_by\u0026#39;, }); module.exports = { User, Article, ArticleImage, ActivationCode }; // 追加暴露 追加路由挂载 app.js const express = require(\u0026#39;express\u0026#39;); const bodyParser = require(\u0026#39;body-parser\u0026#39;); const authRoutes = require(\u0026#39;./routes/login\u0026#39;); const adminRoutes = require(\u0026#39;./routes/admin\u0026#39;); const articleRoutes = require(\u0026#39;./routes/article\u0026#39;) // 添加这行 const app = express(); // 中间件 app.use(bodyParser.json()); // 路由 app.use(\u0026#39;/login\u0026#39;, authRoutes); // 登录路由 app.use(\u0026#39;/\u0026#39;, adminRoutes); // 受保护路由 app.use(\u0026#39;/\u0026#39;, articleRoutes) // 添加这行 // 启动服务器 const PORT = 3000; app.listen(PORT, async () =\u0026gt; { console.log(`Server running on http://localhost:${PORT}`); }); indexRoutes – 主页路由 之后我们写一个主页路由(routes/index.js)，显示最近 5 条最新的数据出来，供游客无偿浏览。只提供核心代码，来试试手：\n...... ......... try { const indexData = await Article.findAll({ order: [[\u0026#39;id\u0026#39;, \u0026#39;DESC\u0026#39;]], // 按 id 降序排列 limit: 5, // 限制为 10 条数据 }); if(!indexData) { return res.status(404).json({ message: \u0026#39;Article not found\u0026#39; }); }; res.json(indexData); ... ......... ...... 网站基本信息 – 通过读取JSON返回 这里提供一个 读取 JSON 文件来返回 网站详细 信息的方案，如 “标题”，”副标题”，”公告” 等等..\n我们接着在 models 文件夹创建一个文件，如 models/siteintro.js 之后创建一个文件夹用来存放 JSON ，如 config/SiteIntro.json models/siteintro.js 文件内容\nconst fs = require(\u0026#39;fs\u0026#39;).promises; // 读取 JSON 文件(异步) const path = require(\u0026#39;path\u0026#39;); // 可有可无，只是方便管理 JSON 路径 const express = require(\u0026#39;express\u0026#39;); const router = express.Router(); // 使用 path 管理 JSON 文件路径 const jsonFilePath = path.join(__dirname, \u0026#39;../config/SiteIntro.json\u0026#39;); router.get(\u0026#39;/\u0026#39;, async (req, res) =\u0026gt; { try { const data = await fs.readFile(jsonFilePath, \u0026#39;utf8\u0026#39;); // 读取文件内容 const readData = JSON.parse(data); // 解析 JSON res.send({ readData }); // 返回解析后的数据 } catch (error) { console.error(error); res.status(500).json({ message: \u0026#39;Internal server error\u0026#39; }); } }); module.exports = router; config/SiteIntro.json 文件内容\n{ \u0026#34;title\u0026#34;: \u0026#34;xxxx\u0026#34;, \u0026#34;subtitle\u0026#34;: \u0026#34;xxxxxx\u0026#34;, \u0026#34;announcement\u0026#34;: \u0026#34;Welcome\u0026#34; } 之后在 app.js 进行挂载\nconst express = require(\u0026#39;express\u0026#39;); const bodyParser = require(\u0026#39;body-parser\u0026#39;); const indexData = require(\u0026#39;./routes/siteintro\u0026#39;) // 追加 ... const cors = require(\u0026#34;cors\u0026#34;); ... // 全局启用 CORS app.use(cors()); // 路由 app.use(\u0026#39;/\u0026#39;, indexRoutes); app.use(\u0026#39;/data\u0026#39;, indexData); // 追加 app.use(\u0026#39;/article\u0026#39;, articleRoutes); ... 之后访问接口 /data 就会返回数据\n{ \u0026#34;readData\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;xxxx\u0026#34;, \u0026#34;subtitle\u0026#34;: \u0026#34;xxxxxx\u0026#34;, \u0026#34;announcement\u0026#34;: \u0026#34;Welcome\u0026#34; } } 之后前端 fetch 后就可以直接剥洋葱式使用了。\nCORS – 无法获取 API 数据问题 或许你在对接前端的时候，会发现访问 API 不被授权/不安全。这是因为 CORS 跨域问题，浏览器把你的流量截断了，通过这个方式可以放行所有的接口。\n安装 cors\nnpm install cors 往 App.js 内追加代码\nconst express = require(\u0026#39;express\u0026#39;); const bodyParser = require(\u0026#39;body-parser\u0026#39;); ... const cors = require(\u0026#34;cors\u0026#34;); // 追加 const app = express(); ... // 全局启用 CORS app.use(cors()); // 追加 // 路由 app.use(\u0026#39;/\u0026#39;, indexRoutes); ... 即可解决。也可以按照文档，自己选择允许哪些 IP/地址 访问。\n","permalink":"https://blog.dontalk.org/posts/%E7%8E%B0%E4%BB%A3%E7%BD%91%E9%A1%B5%E5%BC%80%E5%8F%91%E5%85%A5%E9%97%A8%E6%8C%87%E5%8D%97%E4%B9%A6-express%E5%90%8E%E7%AB%AF/","summary":"后端 – NodeJS 后端用 Express 制作 API 。Sequelize 操作 数据库。 项目目录结构： ├── app.js # 整合文件 ├── database # 数据库-文件夹 │ └── database.js # 数据库初始化 ├── middlewares # 中间件-文件夹 │ └── auth.js # 认证中间件 ├── …","title":"现代网页开发入门指南书 – Express(后端)"},{"content":"前言 如今的Web，早已不再是前后端混合的 PHP 世界。我们早已进入了前后端分离的，Web3.0的 – 未来。\n欢迎你阅读本文 – 「现代网页开发入门指南书 – 从数据库设计，到后端，再到前端」\n由 Dontalk 会员 – 【葵】 耗时4天编写 – 如有不对，敬请原谅和指正。\n如果你希望下载本文 PDF 以及配套代码，请前往「dl.dontalk.org」找到「现代网页开发入门指南书 – 从数据库设计，到后端，再到前端」即可下载。或者博文提到的 GitHub地址 也可以下载到副本 (包括本指南书的PDF档)。\nDontalk 地址：dontalk.org\nGithub 仓库地址(25年5月中)：https://github.com/infoabcd/Modern-Web-Development-Beginner-s-Guide/\n但是请注意，本作品采用 [署名-非商业性使用 4.0 国际 (CC BY-NC 4.0)] 协议发布。\n你可以自由转载、分发、分享，但必须注明作者及出处，不得用于任何商业用途。\n否则在沟通无果后，Dontalk 团队不介意采用法律武器捍卫权益。\n协议详情请见：https://creativecommons.org/licenses/by-nc/4.0/deed.zh\n评价： 这份《现代网页开发入门指南书 v1.3》作为一份旨在引导初学者进入全栈Web开发的材料，展现了其覆盖广泛知识领域的雄心。它试图带领读者从后端的数据库设计，经过API开发，最终到前端的实现，并采用了当前较为流行的技术栈（如Node.js, Express, Sequelize, React, Vite）。文档通过代码示例来阐述概念，这对于初学者动手实践具有一定的积极意义。\n然而，从技术严谨性和最佳实践指导的角度来看，这份指南在一些关键环节存在较为重要的改进空间(缺少动机阐述和不建议声明)(下面声明)，初学者若不加辨别地采纳，可能在实际项目中引入风险或不良实践。\n值得肯定的方面：\n内容覆盖面广：指南尝试覆盖Web开发的整个生命周期，从数据存储到用户交互，有助于初学者建立整体概念。 技术栈选择现代：采用了Node.js、Express、React、Vite等现代Web开发中常用的技术和工具，使学习者能够接触到当前主流的开发模式。 实践性导向：提供了大量的代码片段，涉及数据库模型定义、API路由、JWT认证、前端组件等，鼓励读者通过实践学习。 核心概念涉及：介绍了诸如ORM（Sequelize）、RESTful API设计、JWT认证机制、前端路由（React Router）、组件化开发等Web开发的核心概念。 安全意识初显：在某些方面体现了对安全性的关注，例如提到了使用bcrypt进行密码哈希，以及在JWT存储中讨论了HttpOnly Cookie的应用。 现代网页开发入门指南书 v1.3 存在的问题 你可以粗略阅读此处，之后再回看\n“现代网页开发入门指南书 – 从数据库设计，到后端，再到前端 v1.3版本.pdf”的摘录内容，我来分析其中可能存在的严重技术性问题，并提供相应的解读。\n从整体上看，这份指南覆盖了从数据库设计到前后端实现的关键环节，并提供了一些实践代码。然而，在一些技术细节和最佳实践方面，确实存在一些可以被认为是“严重”或至少是“重要”的技术问题或改进点。\n存在的一些问题(下文不好更改-此处声明(填坑))： 1. 数据库设计与 SQL 问题 问题 1.1: article 表中不必要的 article_id 字段 (用户已指出并解决) 原文描述: 用户提供的 SQL 中 article 表同时有 id (主键) 和 article_id。 严重性: 中等。这会导致数据冗余和潜在的逻辑混乱。外键应该引用主键 id。 状态: 用户在后续的交流中已经指出了这个问题，并通过移除 article.article_id 并让 article_image.article_id 引用 article.id 来修正。 问题 1.2: 使用 CHAR(6) 作为自定义关联键 (Release level) 原文描述: 提出使用名为 “Release level” (在 article 表) 和 “level” (在 article_image 表) 的 CHAR(6) 字段，通过随机6位数进行关联。 严重性: 中高。 唯一性风险: “随机6位数”如果生成算法不够健壮，有碰撞（重复）的风险，尤其是在数据量大时。UNIQUE 约束可以防止插入重复值，但不能解决生成算法本身的问题。 性能: 字符串类型的键通常比整数类型的键在索引和连接操作上性能稍差。 可读性和维护性: 虽然在某些特定场景下自定义关联键有其用途，但通常情况下，使用自增整数主键 (id) 作为外键引用目标更简单、高效且符合常规实践。 SQL字段名: Release level 这样的带空格的字段名在 SQL 中需要用反引号 `Release level` 包裹，容易出错且不推荐。 改进建议: 坚持使用 article.id (主键) 和 article_image.article_id (外键) 进行关联。如果确实需要一个人类可读的、唯一的发布标识，可以额外增加一个 release_code 字段，并确保其唯一性，但不一定用它作为主外键关联。 问题 1.3: ON UPDATE CASCADE 的普遍使用 原文描述: 在外键定义中同时使用了 ON DELETE CASCADE 和 ON UPDATE CASCADE。 严重性: 低到中等。 ON DELETE CASCADE 是常见的，表示删除主表记录时级联删除从表相关记录。 ON UPDATE CASCADE 表示更新主表主键值时，级联更新从表外键值。虽然在某些数据库和场景下支持，但主键本身通常是不应该被频繁更新的。如果主键是自增整数，几乎永远不会更新。如果主键是可能变更的业务字段（不推荐），ON UPDATE CASCADE 才显得更有意义。过度依赖此特性可能掩盖了主键设计不当的问题。 改进建议: 仔细评估是否真的需要 ON UPDATE CASCADE。对于自增主键，它几乎没有作用。 2. Sequelize ORM 使用问题 问题 2.1: DataTypes.STRING(255) 的写法 (用户已提问) 原文描述: 代码中使用了 DataTypes.STRING(255)。 严重性: 低。这不是一个“错误”，但正如用户指出的，Sequelize v6+ 中 DataTypes.STRING 默认映射到数据库的 VARCHAR(255) (或数据库的默认字符串长度)，所以显式写 (255) 通常不是必需的，除非有特定长度要求。 状态: 已在交流中澄清。 问题 2.2: timestamps: false 与手动 created_at 原文描述: 模型定义中设置 timestamps: false，然后手动定义 created_at 字段。 严重性: 低。这是一种可行的做法，但 Sequelize 的 timestamps: true (默认) 会自动管理 createdAt 和 updatedAt 字段，通常更方便。如果只需要 createdAt，可以配置 timestamps: true, updatedAt: false。手动管理意味着在更新操作时也需要手动更新 updatedAt (如果需要的话)。 改进建议: 除非有特殊理由，否则利用 Sequelize 内建的时间戳管理功能可能更简洁。 问题 2.3: sequelize.sync({ alter: true }) 的滥用 原文描述: 在 app.js 或入口文件中使用 await sequelize.sync({ alter: true }); 来同步数据库。 严重性: 高（在生产环境中）。 { alter: true } 会尝试修改现有表以匹配模型定义，这在开发初期可能很方便，但在生产环境中非常危险，可能导致数据丢失或表结构意外更改。 { force: true } (未在示例中直接出现，但与 sync 相关) 会先删除表再重建，同样会导致数据丢失。 改进建议: 生产环境中应使用数据库迁移 (Migrations) 工具 (如 Sequelize CLI 提供的迁移功能) 来管理数据库模式的变更。开发环境中 sync({ alter: true }) 可酌情使用，但需谨慎。 问题 2.4: 模型关联中 sourceKey 和 targetKey 的理解 原文描述: 在 Article.hasMany(ArticleImage, { foreignKey: 'article_id', sourceKey: 'id' }) 和 ArticleImage.belongsTo(Article, { foreignKey: 'article_id', targetKey: 'id' }) 中，sourceKey 和 targetKey 的使用。 严重性: 低。这里的用法是正确的，因为它们都指向了 Article 表的 id 字段。sourceKey 是源模型（Article）中与外键关联的键，targetKey 是目标模型（Article）中被外键引用的键。当外键引用的不是目标模型的主键时，或者源模型的关联键不是其主键时，这些选项才更有区分度。对于标准的主键-外键关联，有时可以省略。 说明: 当关联字段名与默认规则一致时 (例如，ArticleImage 有 articleId 字段引用 Article 的 id)，很多配置可以省略。显式配置更清晰，但需要确保理解每个选项的含义。 3. 后端 API 与安全问题 (基于 JWT 部分) 问题 3.1: JWT 密钥管理 原文描述: const JWT_SECRET = 'your_secret_key'; 严重性: 高 (如果直接用于生产)。 密钥硬编码在代码中是非常不安全的。 改进建议: 密钥必须通过环境变量 (如 .env 文件配合 dotenv 库) 或安全的配置服务来管理，并且密钥本身应该是强随机字符串。 问题 3.2: 错误处理和信息泄露 原文描述: 登录失败时返回 res.status(404).json({ error: '用户未找到' }); 或 res.status(401).json({ error: '无效的凭证' });。 严重性: 中等。 区分“用户未找到”和“密码错误”会给攻击者提供枚举有效用户名的信息。 改进建议: 对于登录失败，应返回统一的、模糊的错误信息，例如 res.status(401).json({ error: '用户名或密码错误' });。 问题 3.3: HttpOnly Cookie 的 secure 和 sameSite 属性 原文描述: 提到了将 JWT 存储在 HttpOnly Cookie 中，并给出了配置示例。 res.cookie(\u0026ldquo;authToken\u0026rdquo;, token, { httpOnly: true, // 防止客户端JS访问 secure: process.env.NODE_ENV === \u0026lsquo;production\u0026rsquo;, // 仅在HTTPS下传输 maxAge: 3600000 * 1, // Cookie有效期 (例如1小时) sameSite: \u0026ldquo;Strict\u0026rdquo;, // 防CSRF });\n* **严重性**: 低 (因为示例代码考虑到了这些)。这是一个好的实践。 * **关键点**: 确保 `secure: process.env.NODE_ENV === \u0026#39;production\u0026#39;` 的逻辑正确，并且生产环境强制 HTTPS。`sameSite: \u0026#34;Strict\u0026#34;` 或 `\u0026#34;Lax\u0026#34;` 是防止 CSRF 的重要措施。 * **问题 3.4: 权限校验的粒度** * **原文描述**: `authorizeRole` 中间件示例 `if (!req.user || req.user.role !== requiredRole)`。 * **严重性**: 中等 (取决于应用复杂度)。 * 这种基于单一角色的校验对于简单应用尚可，但复杂应用可能需要更细粒度的权限控制（例如，基于操作的权限，或者用户同时拥有多个角色）。 * **改进建议**: 考虑引入更完善的权限管理方案，如 RBAC (Role-Based Access Control) 或 ABAC (Attribute-Based Access Control)。 ### **4. 前端 React 与 Vite 问题** * **问题 4.1: API 地址硬编码** * **原文描述**: `fetch(\u0026#34;http://localhost:3000/data\u0026#34;)` * **严重性**: 中等 (在需要部署到不同环境时)。 * API 地址硬编码不利于在不同环境（开发、测试、生产）中部署。 * **改进建议**: 使用环境变量配置 API 基地址。例如，在 Vite 中通过 `.env` 文件和 `import.meta.env.VITE_API_URL`。 * **问题 4.2: 错误处理和用户反馈** * **原文描述**: `catch (error) { console.error(\u0026#34;Failed to fetch announcement:\u0026#34;, error); setAnnouncement(\u0026#34; \u0026#34;); }` * **严重性**: 中等。 * 仅在控制台打印错误对用户不友好。设置为空字符串可能也不是最佳的用户体验。 * **改进建议**: 应该向用户显示明确的错误信息（例如，使用 Toast 通知、错误提示组件），并考虑重试机制。 * **问题 4.3: 状态管理选择** * **原文描述**: 提到了 Context API 和 Redux/Zustand 等。 * **严重性**: 低 (这是一个架构选择问题)。 * **说明**: 指南中提到了多种状态管理方案是好的。选择哪种取决于应用规模和团队偏好。对于小型应用，`useState` 和 `useReducer` + Context API 可能足够。大型应用则可能从 Redux、Zustand 等库中受益。 ### **总结** 指南中提供的内容基本覆盖了现代Web开发的一些核心概念和技术栈，并且在一些地方（如 HttpOnly Cookie 的使用）体现了较好的安全意识。 主要的“严重”技术性问题集中在： 1. **生产环境中 `sequelize.sync({ alter: true })` 的使用**：这是最危险的一点，可能导致生产数据丢失。 2. **JWT 密钥的硬编码**：严重的安全隐患。 3. **登录错误信息过于具体**：可能泄露用户信息。 4. **自定义关联键 `CHAR(6)` 的设计**：相比标准整数主外键，风险更高，性能可能更差。 其他问题更多是关于“最佳实践”、“代码健壮性”和“可维护性”的改进点。这份指南作为一个“入门指南”，在简化概念的同时，也需要在关键的生产安全和最佳实践方面给予更明确的警告和指导。 * * * # 数据库 \u0026amp;#8211; MariaDB ```sql -- 用户管理表 CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, -- 用户唯一标识 username VARCHAR(50) NOT NULL UNIQUE, -- 用户名，唯一 password VARCHAR(255) NOT NULL, -- 用户密码（加密存储） email VARCHAR(100) NOT NULL UNIQUE, -- 用户邮箱，唯一 is_member TINYINT(1) DEFAULT 1, -- 用户是否会员（1=激活，0=未激活） created_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- 创建时间 updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -- 更新时间 ); -- 文章管理表 CREATE TABLE articles ( id INT AUTO_INCREMENT PRIMARY KEY, -- 文章唯一标识 publisher_id INT NOT NULL, -- 发布文章的用户ID title VARCHAR(255) NOT NULL, -- 文章标题 content TEXT NOT NULL, -- 文章内容 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- 创建时间 updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 更新时间 FOREIGN KEY (id) REFERENCES users (id) ON DELETE CASCADE -- 用户删除时删除其文章 ); -- 文章图片表 CREATE TABLE article_images ( id INT AUTO_INCREMENT PRIMARY KEY, -- 图片唯一标识 article_id INT NOT NULL, -- 关联文章的ID image_url VARCHAR(255) NOT NULL, -- 图片路径或URL created_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- 创建时间 FOREIGN KEY (article_id) REFERENCES articles (id) ON DELETE CASCADE -- 文章删除时级联删除图片 ); 用户管理表的 最后更新时间 最后更新时间的意义和应用场景是什么呢？\n意义 updated_at 字段的作用是记录一条数据的最后更新时间。它能够自动更新为数据被修改时的时间戳。这是一个非常实用的字段，尤其在需要追踪数据变化的场景中，可以轻松知道数据最近一次被操作的时间。\n在设计数据库表结构中，updated_at 通常用于以下场景：\n– 数据变更的记录： 用来标记一条数据最后被修改的时间。\n– 数据同步： 在需要同步不同系统或服务器上的数据时，可以通过 updated_at 找到比某时间戳之后更新的数据。\n– 调试和审计： 跟踪数据的更新历史，快速定位问题或分析系统行为。\n应用场景 在用户表中，updated_at 可用于记录用户信息的最后修改时间：\n用户修改资料： 当用户修改个人信息（如用户名、邮箱等）时，自动更新 updated_at 字段。 会员续费： 当会员续费或到期时间更改时，updated_at 会更新为最新时间，方便追踪会员状态的变更。 调试问题： 如果用户报告登录或账户问题，可以检查 updated_at 了解最近的变更时间，协助排查问题。 一般网站数据库如何存储图片呢？ 方案 1：文件系统/云存储 + 数据库记录路径或 URL 优点： 性能更高： 图片文件不存储在数据库中，数据库的读取和写入性能不会因图片数据增大。 易于扩展： 图片可以通过 CDN 或云存储加速分发，提高访问速度。 维护简单： 图片可以独立于数据库进行备份和管理。 存储成本低： 文件存储（尤其是云存储）比数据库存储更加经济。 缺点： 数据一致性问题： 图片文件和数据库记录可能不同步（如文件被意外删除，但数据库记录仍然存在）。 额外的存储配置： 需要搭建文件服务器或对接云存储服务。 方案 2：将图片直接存储在数据库中 描述：\n图片文件以二进制数据的形式存储在数据库中，使用 BLOB（Binary Large Object）或 TEXT 类型字段。 优点： 数据一致性： 图片和其他文章数据存储在同一数据库中，管理和备份更加方便。 易于迁移： 数据库迁移时，图片数据会一并迁移，无需额外操作。 安全性更高： 图片数据直接存储在数据库中，比文件系统更容易保护敏感信息。 缺点： 性能较差： 数据库的读写性能可能会因存储大量图片而显著下降。 备份和恢复速度慢： 数据库变得非常庞大后，备份和恢复会更加耗时。 存储成本高： 数据库通常使用更昂贵的存储方案，存储大量图片会增加成本。 为什么不把图片放在和文章一个表里呢 **之后，用 逗号 隔开它们。\n在一个表中保存多张图片是可以做到的，但这取决于实际需求和场景。如果直接在一个表的字段中保存多张图片路径（例如用英文逗号分隔），虽然实现简单，但在扩展性、维护性和性能方面存在一些限制，因此通常不推荐。\n下面是详细的分析，为什么不建议在一个字段中保存多张图片路径，以及可以这样做的场景。\n1. 数据库设计原则 数据库设计遵循 “第一范式（1NF）” 的范式规则： 每个字段应该只存储一个值，而不是一组值。 如果一个字段存储了多个值（如用逗号分隔的图片路径），会违反第一范式，使得数据结构冗杂，难以管理和查询。 2. 查询复杂性 当数据存储为逗号分隔的形式时，查询和操作会变得困难。例如： 查询某篇文章是否包含某张特定的图片路径需要用 LIKE 或 FIND_IN_SET，效率较低。 统计或筛选包含特定图片的记录时，无法利用数据库的索引。\n示例：查询包含 /uploads/images/image2.jpg 的文章 SELECT * FROM articles WHERE FIND_IN_SET(\u0026#39;/uploads/images/image2.jpg\u0026#39;, image_urls); 这种查询无法利用索引，性能较差。 3.维护困难 当需要对单张图片进行操作时（比如删除、更新或替换图片路径），操作会变得复杂，容易出错。 例如，如果某张图片路径需要更新，必须解析整个字段并重新构建路径字符串。 示例：删除某张图片路径 – 如果字段 image_urls 的值为： /uploads/images/image1.jpg,/uploads/images/image2.jpg,/uploads/images/image3.jpg 需要删除 /uploads/images/image2.jpg：\n必须在应用层解析字符串，删除对应路径后再更新字段内容。 操作繁琐，容易遗漏或出错。 4.扩展性不足 如果以后需要为每张图片添加额外信息（如图片的标题、描述、排序顺序等），逗号分隔的形式很难支持。 在单个字段下，图片的额外信息没法单独存储，无法满足复杂业务需求。 使用场景： 1. 图片数量较少\n– 如果每篇文章的图片数量固定且较少（如 2-3 张），存储在一个字段中可能是可接受的。\n2. 简单项目\n– 在小型项目或实验性开发中，数据结构简单且无需复杂查询时，可以使用逗号分隔的形式存储图片路径。\n3. 仅用于展示\n– 如果图片仅用于简单展示，不需要复杂的查询、筛选或操作，逗号分隔的形式可以满足需求。\n","permalink":"https://blog.dontalk.org/posts/%E7%8E%B0%E4%BB%A3%E7%BD%91%E9%A1%B5%E5%BC%80%E5%8F%91%E5%85%A5%E9%97%A8%E6%8C%87%E5%8D%97%E4%B9%A6-mariadb%E6%95%B0%E6%8D%AE%E5%BA%93/","summary":"前言 如今的Web，早已不再是前后端混合的 PHP 世界。我们早已进入了前后端分离的，Web3.0的 – 未来。\n欢迎你阅读本文 – 「现代网页开发入门指南书 – 从数据库设计，到后端，再到前端」\n由 Dontalk 会员 – 【葵】 耗时4天编写 – 如有不对，敬请原谅和指正。\n如果你希望下载本文 PDF 以及配套代码，请前往 …","title":"现代网页开发入门指南书 – MariaDB(数据库)"},{"content":"新安装的的MariaDB(MySQL)死活登陆不进去，即便是完全卸载重装，更抽象的是，我都没设置密码，第一次安装，居然管我要密码，而且tm修改密码也不起作用。\n之后的重新安装，问题似乎显现了出来，一直提示我 ERROR 1698 (28000): Access denied for user 'root'@'localhost'\n这个问题在新安装的MariaDB(MySQL)算是比较常见了。大概是什么引起的呢。\n本文针对 MariaDB / MySQL\n前言 新安装的 MariaDB(MySQL) 一直报错：ERROR 1698 (28000): Access denied for user ‘root’@’localhost’\n这个错误通常意味着 MariaDB 为 root@localhost 用户配置了 unix_socket (或 auth_socket) 身份验证插件。这种插件允许您在满足以下条件时无需密码即可登录：\n1.您正在以操作系统中的 root 用户身份运行 mysql 客户端。(sudo)\n2.或者，您正在以 MariaDB 服务运行时所使用的同一 Unix 用户身份通过 Unix 套接字文件连接。\n为什么我完全卸载了 MariaDB(MySQL) 还是要求我输入root密码？\n当您“完全卸载”并重新安装 MariaDB 时，Homebrew 会执行 MariaDB 的标准初始化过程。许多现代的数据库系统，包括 MariaDB，出于安全考虑，在新的、干净的安装中会为 root@localhost 用户默认启用类似 unix_socket 的身份验证。这样做是为了防止在初始设置完成前 root 账户没有密码而容易受到攻击。\n所以，即使您清除了所有旧数据，新安装的 MariaDB 也会遵循其默认的安全设置，这就解释了为什么您会遇到这个错误。您之前的操作（完全卸载）是正确的，现在遇到的问题是新安装的默认行为。\n解决方法 所以，普通用户下的 mysql -u root 就一直报错。\n而我通过 brew services list 可以看到，User处是我的普通用户用户名 (如果是Linux: systemctl status mariadb或mysql )。而非 root。\n使用下面命令登陆到 MySQL\nsudo mysql -u root 使用下面命令，你大概能看到类似的东西：\nUSE mysql; SELECT User, Host, plugin FROM mysql.user; # unix_socket (在 MariaDB 中有时也称为 auth_socket) +-------------+--------------------------+-----------------------+ | User | Host | plugin | +-------------+--------------------------+-----------------------+ | mariadb.sys | localhost | mysql_native_password | | root | localhost | auth_socket | | xxxx | localhost | auth_socket | ....... +-------------+--------------------------+-----------------------+ 6 rows in set (0.001 sec) (在Linux服务器，且没有什么问题的情况下，只需要保证 #(root用户) 登陆数据库就行，因为 问题一般出在用户终端机 )\n可以看到 root 它们使用的都是 auth_socket 认证。\n我们需要更改成 mysql_native_password ，之后顺便改个 root 密码\nALTER USER \u0026#39;root\u0026#39;@\u0026#39;localhost\u0026#39; IDENTIFIED BY \u0026#39;123456\u0026#39;; # 更改 root@localhost 用户的身份验证插件并设置密码 FLUSH PRIVILEGES; # 刷新权限 EXIT; # 退出 之后我们登陆看看:\nmysql -u root -p 输入刚刚设置的密码 123456 后，成功登陆。\n我们可以再看看目前验证方式。\nUSE mysql; SELECT User, Host, plugin FROM mysql.user; +-------------+--------------------------+-----------------------+ | User | Host | plugin | +-------------+--------------------------+-----------------------+ | mariadb.sys | localhost | mysql_native_password | | root | localhost | mysql_native_password | | xxxx | localhost | mysql_native_password | ....... +-------------+--------------------------+-----------------------+ 6 rows in set (0.001 sec) 总算是正常了。\n如果使用unix_socket，那么我的登陆密码是什么？ 如果您的用户（例如 dinmi）在数据库中也存在一个同名用户 dinmi，并且该数据库用户配置了 unix_socket 认证，那么您以操作系统用户 dinmi 身份运行 mysql -u dinmi 时，不需要密码。\n如果您尝试以数据库用户 root 登录，并且 root@localhost 配置了 unix_socket 认证，那么您不需要密码，但您必须以操作系统的 root 用户身份运行 mysql 客户端 (通常通过 sudo mysql -u root)。\n所以，答案是：如果你的 MariaDB(MySQL) 用户使用 unix_socket 认证，那么对于该用户，登录时它不检查你输入的密码，而是检查你当前操作系统的用户身份。\n这种认证方式的核心思想是：依赖于「操作系统用户」的身份来验证「数据库用户」。\n如何完全卸载 MariaDB(MySQL) 一般不完全卸载的 MariaDB(MySQL) 很会膈应人，还会复用之前的文件配置(密码一般记录在数据库 mysql 里面，但是有时候会意料之外被使用到之前的密码)所以我们有时候需要完全卸载掉 MariaDB(MySQL)。\n下面教你怎么完全卸载掉 MariaDB(MySQL) – 用 MariaDB 作演示\nMariaDB 和 MySQL 完全(几乎)一样(本文配置几乎通用)，既生瑜何生亮，因为 MariaDB 完全免费，开源驱动，是那帮搞 MySQL 的人开发的，而 MySQL 已经被商业公司「甲骨文」买断了。\nMacOS / OS X 系统\n# Apple Silicon CPU: brew services stop mariadb brew uninstall mariadb ## 移除数据目录 sudo rm -rf /opt/homebrew/var/mysql ## 移除配置文件 sudo rm -f /opt/homebrew/etc/my.cnf sudo rm -rf /opt/homebrew/etc/my.cnf.d sudo rm -f /opt/homebrew/etc/my.cnf.default rm -f ~/.my.cnf rm -rf ~/.mariadb ## 删除启动项和清理 rm -f ~/Library/LaunchAgents/homebrew.mxcl.mariadb.plist brew cleanup # ---------------------------------------- # # Intel CPU Mac: brew services stop mariadb brew uninstall mariadb # 移除数据目录 sudo rm -rf /usr/local/var/mysql # 移除配置文件 sudo rm -f /usr/local/etc/my.cnf sudo rm -rf /usr/local/etc/my.cnf.d sudo rm -f /usr/local/etc/my.cnf.default rm -f ~/.my.cnf rm -rf ~/.mariadb ## 删除启动项和清理 rm -f ~/Library/LaunchAgents/homebrew.mxcl.mariadb.plist brew cleanup Debian/Ubuntu 系统：\n# 记得先进入root用户下 systemctl stop mariadb apt purge mariadb-server mariadb-client mariadb-common mariadb-backup libmariadb3 # 根据实际安装的包调整 rm -rf /var/lib/mysql/ rm -rf /etc/mysql/ # 检查是否已被 purge 删除 rm -rf /var/log/mysql/ /var/log/mariadb/ apt autoremove apt autoclean apt clean rm -f ~/.my.cnf # 可选，用户特定配置 CentOS/RHEL 系统 (使用 dnf 示例)：\nsudo systemctl stop mariadb sudo dnf remove mariadb-server mariadb mariadb-libs # 根据实际安装的包调整 sudo rm -rf /var/lib/mysql/ sudo rm -f /etc/my.cnf sudo rm -rf /etc/my.cnf.d/ sudo rm -rf /var/log/mariadb/ /var/log/mysql/ sudo rm -f /var/log/mysqld.log sudo dnf clean all rm -f ~/.my.cnf # 可选，用户特定配置 ","permalink":"https://blog.dontalk.org/posts/%E6%96%B0%E5%AE%89%E8%A3%85%E7%9A%84mariadbmysql%E6%97%A0%E6%B3%95%E7%99%BB%E5%85%A5root-error-1698-28000-access-denied-for-user-root@localhost/","summary":"新安装的的MariaDB(MySQL)死活登陆不进去，即便是完全卸载重装，更抽象的是，我都没设置密码，第一次安装，居然管我要密码，而且tm修改密码也不起作用。\n之后的重新安装，问题似乎显现了出来，一直提示我 ERROR 1698 (28000): Access denied for user 'root'@'localhost'\n这个问题在新安装的 …","title":"新安装的MariaDB(MySQL)无法登入root – ERROR 1698 (28000): Access denied for user ‘root’@’localhost’"},{"content":"在我所处的网络中，受到了管制，我必须登陆才能使用我的网络。但是我的Linux没有桌面系统，甚至没有外接的显示器，这非常麻烦。虽然我设置了每次开机自动连接到我的移动热点。但是这明显不实际。\n所以我抓包了一个登陆认证的带参URL，只需要设置定时任务在每天登陆认证重置后一分钟，请求这个URL即可。\n但是，可能我的终端机会在某个时间段关机，之后一段时间再开机，而这个时间远远未到定时任务的时间，所以我的主机会离线，直到触发定时任务。或者我不厌其烦找来显示器，又搬来键盘，触发验证。\n所以，我必须要设置开机执行一条命令，那条命令就是 curl 登陆认证的带参URL。之后长时间开机就交给定时任务了。\n正文 方法 1：使用 /etc/rc.local（传统-不推荐） /etc/rc.local 是一个传统的方式，用于在系统启动时运行命令。虽然在某些较新的系统中可能需要手动启用该功能，但它仍然是一个简单的方法。\n步骤： 编辑 /etc/rc.local 文件： ```shell vim /etc/rc.local 2. 在 `exit 0` 之前加入你的命令。例如： ```shell #!/bin/sh -e sleep 10 # 添加一个延迟，等待网络服务启动 curl https://xxxxxx.com.... exit 0 确保 /etc/rc.local 文件有可执行权限： ```shell chmod +x /etc/rc.local **如果 `/etc/rc.local` 不存在或未启用：** 1. 检查服务是否存在： ```shell systemctl status rc-local 如果服务不存在，创建一个服务文件： vim /etc/systemd/system/rc-local.service 内容如下： [Unit] Description=/etc/rc.local Compatibility ConditionPathExists=/etc/rc.local [Service] Type=forking ExecStart=/etc/rc.local start TimeoutSec=0 StandardOutput=tty RemainAfterExit=yes SysVStartPriority=99 [Install] WantedBy=multi-user.target 启用并启动服务： systemctl enable rc-local systemctl start rc-local 方法 2：使用 crontab 的 @reboot（推荐） cron 可以使用 @reboot 关键字在系统启动时运行命令。\n步骤： 编辑当前用户的 crontab： ```shell crontab -e 2. 添加一条 `@reboot` 任务。例如： ```shell @reboot sleep 10 \u0026amp;\u0026amp; curl https://xxxxxxx.com.... # 添加了延迟10s，等待网络加载连接 (已有定时任务的话，在第二行追加即可)\n保存并退出编辑器，重启系统测试。 注意：如果命令需要以特定用户身份运行，请确保编辑的是对应用户的 crontab。\n方法 3：创建 systemd 服务（比较麻烦-推荐） systemd 是现代 Linux 系统的初始化系统，可以通过创建服务文件来配置开机启动任务。\n步骤： 创建一个 sh 文件 ```shell vim /path/to/your/script.sh # 对应下面的守护进程配置 之后填东西进去： ```shell #!/bin/sh sleep 10 curl https://xxxxxxxx.com.... 之后记得给一下执行权限\nchmod +x /path/to/your/script.sh 创建一个自定义服务文件： ```shell vim /etc/systemd/system/my-startup.service 3. 添加以下内容（将 `ExecStart` 替换为你的命令）： ```shell [Unit] Description=My Startup Script After=network.target [Service] Type=oneshot ExecStart=/path/to/your/script.sh # 想要执行的bash脚本地址，涉及到curl认证的话，记得sleep一下，参考上面，防止网络未加载 RemainAfterExit=true [Install] WantedBy=multi-user.target 保存文件后，启用并启动服务： ```shell systemctl enable my-startup.service systemctl start my-startup.service 5. 测试服务是否在开机时运行： ```shell systemctl status my-startup.service 方法 4：编辑 Shell 启动文件（取决于你启动的是哪一个终端） 如果命令仅需要在某个用户登录后执行，可以将其添加到用户的 Shell 启动文件中，例如 .bashrc 或 .bash_profile。\n步骤： 编辑 .bashrc 文件： ```shell vim ~/.bashrc # （取决于你启动的是哪一个终端） 2. 添加你的命令，例如： ```shell sleep 10 curl https://xxxxxxx.com.... 保存文件并退出。注意，这种方式只会在该用户登录时执行。 我的方案 (Crontab + Systemd) 首先，我创建了一个 ba.sh 文件，在 /root/ 下面，给予了 +x 的权限。\n我写了一个守护进程(Systemd)，调用与执行 ba.sh ，之后设置开机自启动。\n使用 crontab -e 在每日早上7点，自动触发一次认证，这里直接复用 ba.sh 即可。\n0 7 * * * bash /root/ba.sh 通过 /etc/network/interfaces 文件配置完 无线网络 的连接配置(到管制网络)。重启网络服务和终端机 (请确保已完成数据持久化(保存文件)，因为一会需要手动硬重启) \u0026#8211; 因为我使用的是无线网络。有线网络一般接上线即可，而无需配置 /etc/network/interfaces ，除非你需要静态设置。 systemctl restart networking # 此时 SSH 就已经和终端机断开连接了，手动重启。 后台看到终端机上线，完成。 总结 如果需要 系统级别 的开机命令，推荐使用 /etc/rc.local 或 systemd 服务。\n如果是 用户级别 的命令，可以使用 crontab @reboot 或 ~/.bashrc。\n系统级别比用户级别要稳定些，也比较麻烦。\n","permalink":"https://blog.dontalk.org/posts/linux%E8%AE%BE%E7%BD%AE%E5%BC%80%E6%9C%BA%E6%89%A7%E8%A1%8C%E4%B8%80%E6%9D%A1%E5%91%BD%E4%BB%A4%E5%BC%80%E6%9C%BA%E8%87%AA%E5%90%AF%E5%8A%A8/","summary":"在我所处的网络中，受到了管制，我必须登陆才能使用我的网络。但是我的Linux没有桌面系统，甚至没有外接的显示器，这非常麻烦。虽然我设置了每次开机自动连接到我的移动热点。但是这明显不实际。\n所以我抓包了一个登陆认证的带参URL，只需要设置定时任务在每天登陆认证重置后一分钟，请求这个URL即可。\n但是，可能我的终端机会在某个时间段关机，之后一段时间再开机，而这个 …","title":"Linux设置开机执行一条命令(开机自启动)"},{"content":"有时候需要将某个目录暴露出去，供给访问者访问，使用Nginx会很方便。但是有时也会有需求，避免某些目录(无密码)被所有人访问。\n下面是可以参考的 Nginx 配置文件\n普通配置 这个是一个普通的暴露配置\nserver { server_name domain.com; # 你的域名 location / { alias /var/www/xxx; # 想要暴露的地址 sendfile on; # 不经过用户态缓冲区，直接发给访问，可以提高静态文件传输性能，减少 CPU 占用 autoindex on; # 没有index的目录，Nginx自动生成返回index autoindex_exact_size off; # 让空间占用大小更加可读(on是字节) autoindex_localtime on; # 文件时间格式，显示服务器的本地时间(off是GMT) charset utf-8,gbk; # 返回内容的字符编码为 utf-8 和 gbk } } 密码验证 安装插件与创建凭证 如果我们希望加上密码验证功能，就需要安装点额外的插件。\napt install apache2-utils # 如果是 Yum 系的话，安装稍有不同 yum install httpd-tools 假如我们想要创建一个名为 test 的用户。\nhtpasswd -c /etc/nginx/.htpasswd test 此时，会要求你为 test 这个用户设置密码\n完成后，在 /etc/nginx/ 目录下面，就会创建一个名为 .htpasswd 的文件。\n追加用户：\n如果你 希望追加用户，而不是覆盖它 ，创建新的文件， 那么就一定不要 加 -c 参数。 删除用户：\nhtpasswd -D /etc/nginx/.htpasswd test # 你也可以直接编辑 `/etc/nginx/.htpasswd` 这个文件，手动删除那一行 Nginx 追加配置 假设我们想要加密，/var/www/xxx/ 目录下的 adb 这个目录 (/var/www/xxx/adb)\n之后，我们给我们的 Nginx 配置文件追加下面的内容即可\nlocation /adb/ { alias /var/www/xxx/adb; # 具体目录 auth_basic \u0026#34;Restricted Area\u0026#34;; # 浏览器弹窗提示信息，可以自定义 auth_basic_user_file /etc/nginx/.htpasswd; # 指定密码文件路径(刚刚创建那个) autoindex on; # 启用目录列表功能 autoindex_exact_size off; autoindex_localtime on; charset utf-8,gbk; } 完整配置 那么他们的完整配置就会是这样的\nserver { server_name domain.com; # 你的域名 location / { alias /var/www/xxx; sendfile on; autoindex on; autoindex_exact_size off; autoindex_localtime on; charset utf-8,gbk; } # 为 adb 目录启用账号密码验证 location /adb/ { alias /var/www/xxx/adb; # adb 指向的具体目录 auth_basic \u0026#34;Restricted Area\u0026#34;; # 浏览器弹窗提示信息 auth_basic_user_file /etc/nginx/.htpasswd; # 指定密码文件路径 autoindex on; autoindex_exact_size off; autoindex_localtime on; charset utf-8,gbk; } } 完整配置的 精简写法 只需要，将公共部分提取出来放到上层 location 或 server 块中，只保留每个 location 块的差异化配置：\nserver { server_name domain.com; # 你的域名 # 通用配置，适用于所有 location sendfile on; autoindex on; autoindex_exact_size off; autoindex_localtime on; charset utf-8,gbk; # / 根路径 配置 location / { alias /var/www/xxx; # 想要暴露的地址 } # adb 路径 启用账号密码验证 location /adb/ { alias /var/www/xxx/adb; # adb 指向的具体目录 auth_basic \u0026#34;Restricted Area\u0026#34;; # 浏览器弹窗提示信息 auth_basic_user_file /etc/nginx/.htpasswd; # 指定密码文件路径 } } 说明 算是 Nginx 常识了\nlocation /adb/ { # 这里是自己取的，假如目录是 adb，这里是 xdb ，那么访问 domain.com/xdb ，其对应的目录其实是 adb。目录只与下面的 alias 有关。 alias /var/www/xxx/adb; # 具体目录 ··· ","permalink":"https://blog.dontalk.org/posts/nginx%E5%88%97%E5%87%BA%E6%9F%90%E4%B8%AA%E7%9B%AE%E5%BD%95%E7%9A%84%E6%96%87%E4%BB%B6-%E7%BB%99%E6%9F%90%E4%B8%AA%E7%9B%AE%E5%BD%95%E5%8A%A0%E4%B8%8A%E5%AF%86%E7%A0%81%E9%AA%8C%E8%AF%81/","summary":"有时候需要将某个目录暴露出去，供给访问者访问，使用Nginx会很方便。但是有时也会有需求，避免某些目录(无密码)被所有人访问。\n下面是可以参考的 Nginx 配置文件\n普通配置 这个是一个普通的暴露配置\nserver { server_name domain.com; # 你的域名 location / { alias /var/www/xxx; # 想要暴 …","title":"Nginx列出某个目录的文件 \u0026 给某个目录加上密码验证"},{"content":"我使用过很多流媒体播放器，我实在喜欢Spotify。但是我也希望给我离线30G(目前来说)音乐库一个家。我尝试过不少离线播放器，实在牵强人意，而且在我使用Linux和Mac之后，选择性就变得更少(实则不然，只是我太挑剔，很多用过觉得不好就删了)。而最终，为何不试试在终端播放音乐呢？反正我已经泡在终端里出不来。\n正文 所以，我发了一个博文。我比较喜欢的两个终端播放器分别是：\n【Cmus】和【Ncmpcpp】\n(还有一些，如moc、sayonara-terminal)\nCmus 它长这样：\n事实上，Cmus 是不错的，因为它支持的系统很多，而且开箱即用，只需要包管理器安装好: brew install cmus 就可以直接打开 cmus，我也使用过一段时间，但是我发现我有些Flac音乐播放卡卡的沙沙声音，总是会吓人一跳，我也懒得找问题，因为有固定几首歌会这样。\n但是我们今天的主题并不是 Cmus ，但是我依旧会提供一些 Cmus 的快捷键指南：\nx 播放 c 暂停/播放 b 下一首 z 上一首 v 归零 +/= 音量 +10% - 音量 -10% , 退后 -1m . 快进 +1m h 退后 -5 l 快进 +5 left 退后 -5 right 快进 +5 更多在这里：https://github.com/cmus/cmus/blob/master/Doc/cmus.txt\n让我们回到我们的重点 —— 正题 【Ncmpcpp】\nNcmpcpp (经过美化设置)它长这样：\n来吧，废话不多说了，让我们开始安装： (本文 Ncmpcpp 版本为 ncmpcpp 0.10.1)\n第一步 – 安装 MPD MPD（Music Player Daemon）是 ncmpcpp 的后端服务，它负责管理和播放音乐。\nbrew install mpd # Linux 自行换掉 Brew 就行 第二步 – 创建 MPD 的配置文件目录 mkdir -p ~/.mpd/playlists touch ~/.mpd/{mpd.conf,mpd.db,mpd.log,mpd.pid} 第三步 – 编辑配置文件 vim ~/.mpd/mpd.conf music_directory \u0026#34;~/Music\u0026#34; # 音乐目录 playlist_directory \u0026#34;~/.mpd/playlists\u0026#34; db_file \u0026#34;~/.mpd/mpd.db\u0026#34; log_file \u0026#34;~/.mpd/mpd.log\u0026#34; pid_file \u0026#34;~/.mpd/mpd.pid\u0026#34; state_file \u0026#34;~/.mpd/mpdstate\u0026#34; bind_to_address \u0026#34;127.0.0.1\u0026#34; # 服务地址绑定到本机(如果你需要让 MPD 在局域网中被其他设备访问，可以将 bind_to_address 设置为 0.0.0.0) port \u0026#34;6600\u0026#34; # MPD 服务使用的端口 audio_output { # macOS 上选择 osx，如果需要输出到其他设备可自行调整。 type \u0026#34;osx\u0026#34; name \u0026#34;Mac Audio\u0026#34; buffer_time \u0026#34;12\u0026#34; } 如果是Linux的话：(在安装完毕后，7 页面可以查看，支持开关输出)\n....同上 audio_output { type \u0026#34;alsa\u0026#34; # 对应 Linux 的音频系统 name \u0026#34;My ALSA Output\u0026#34; } 第四步 – 启动MPD mpd # 设置开机自启动 # Mac 立即启动 mpd 并在登录时重新启动： brew services start mpd # 如果你不想/不需要后台服务，可以直接运行： /opt/homebrew/opt/mpd/bin/mpd --no-daemon # Linux systemctl enable --now mpd # 重启 MPD brew services restart mpd # MacOS systemctl restart mpd # Linux 第五步 – 安装Ncmpcpp brew install ncmpcpp # 验证安装 ncmpcpp --version # 输出内容： ncmpcpp 0.10.1 第六步 – 配置Ncmpcpp 因为 Ncmpcpp 是依赖 MPD 的，所以需要为 Ncmpcpp 设置配置文件，以便它能够正确连接到 MPD。\n在 ~/.ncmpcpp/config 中创建 ncmpcpp 的配置文件：\nmkdir -p ~/.ncmpcpp vim ~/.ncmpcpp/config 添加下面内容：\n# MPD 服务器地址 mpd_host = \u0026#34;127.0.0.1\u0026#34; # MPD 服务器端口 mpd_port = \u0026#34;6600\u0026#34; # 指定 mpd 音乐库路径 mpd_music_dir = \u0026#34;~/Music\u0026#34; # 播放列表设置 playlist_disable_highlight_delay = \u0026#34;0\u0026#34; 美化指南在结尾\n配置文件要确保的内容：\nmpd_host 和 mpd_port：确保与 MPD 的设置一致（默认是 127.0.0.1:6600）。\nmpd_music_dir：指定你的音乐文件夹路径。 第七步 – 启动Ncmpcpp 启动步骤：\n1.确保 MPD 正在运行：\nmpd 2.启动 Ncmpcpp：\nncmpcpp 3.如果一切正常，你应该会看到 Ncmpcpp 的主界面，类似于下图：\n如果没看到音乐 即便刷新了还是没看到，但是我们看一下日志，确实是导入成功了。\n这是为什么呢，是因为 1 是播放历史，默认是空白的，你需要切换到 2 文件浏览器，选择音乐的目录，选择播放后，才会在 1 播放历史里看到。\n至此，所有的工作就完成了，可以开始听歌了。而详细的美化指南在结尾\n常用操作和快捷键 基本操作： 添加音乐到 MPD 数据库：\n要确保音乐文件已放在 music_directory 指定的路径中（如上文配置的 ~/Music）。\n之后在 ncmpcpp 中按下 u，更新 MPD 数据库。\n播放音乐： 使用 箭头键 导航到歌曲或专辑，按 Enter开始/重新播放。\n切换窗口： 1：主播放窗口(播放历史)。 2：文件浏览器。 3：搜索。 4：音乐库。 5：播放列表编辑。 6：标签设置。 7：输出接口。 8：音乐波频。 播放控制： Enter：开始/重新播放。 \u0026gt;/\u0026lt;：下一首/上一首(这个是括号,不是方向键) p/s：暂停/终止播放。 方向左右 - 调整音量 r/z/y：重复/随机/循环播放模式。 c/S：清除/保存播放列表。 n/m：移动播放列表音乐。 搜索： 按 /，然后输入搜索关键词（如歌曲名、艺术家）。\n退出： 按 q 退出 ncmpcpp。\n更多快捷键 https://linux-in-the-house.pages.dev/ncmpcpp\n美化指南 实现 FIFO 频谱图可视化显示。 报错前言 在 v0.9 – v0.10 后的 MPD 新版本里, mpd.conf 配置文件 已经弃用 下面的 设置\nReading configuration from ~/.ncmpcpp/config... Unknown option: visualizer_fifo_path Unknown option: visualizer_sync_interval visualizer_fifo_path , 新版本请使用 visualizer_data_source。\n之后可以在 mpd.conf 配置中【指定】 buffer_time，并在 ncmpcpp 配置中【移除】 visualizer_sync_interval。\n如果你希望使用大家的(旧)配置而不报错，可以编译安装 v0.8 或之前的版本\n配置 FIFO 什么是FIFO呢？\nFIFO（First In First Out）管道文件 是一种特殊的文件类型，用于在程序间传递数据。在 MPD 和 ncmpcpp 中，FIFO 通常用于传递音乐的可视化数据（如频谱图）。MPD 将音频数据输出到 FIFO 文件，而 ncmpcpp 读取该文件并显示频谱效果。\n下面，我们来配置。\n第一步 – 创建 FIFO 文件 mkfifo /tmp/mpd.fifo 第二步 – 给予 FIFO 读写权限 chmod 666 /tmp/mpd.fifo 第三步 – 在 MPD 配置追加点东西 vim ~/.mpd/mpd.conf ### 追加下面内容 # FIFO 输出 audio_output { type \u0026#34;fifo\u0026#34; # 使用 FIFO 类型 name \u0026#34;Visualizer\u0026#34; # 输出名称，可自定义 path \u0026#34;/tmp/mpd.fifo\u0026#34; # FIFO 文件的路径 format \u0026#34;44100:16:2\u0026#34; # 音频格式（采样率:位深度:声道数） } type \u0026quot;fifo\u0026quot;：指定输出类型为 FIFO。\npath \u0026quot;/tmp/mpd.fifo\u0026quot;：与前面创建的 FIFO 文件路径一致。\nformat \u0026quot;44100:16:2\u0026quot;：指定音频数据的格式（44.1 kHz、16 位深度、双声道）。\n第四步 – 保存文件后，(每次修改都要)重新启动 MPD： brew services restart mpd # MacOS systemctl restart mpd # Linux 之后就没问题了。编辑 ncmpcpp 的配置文件(~/.ncmpcpp/config)：\nvisualizer_output_name = \u0026#34;FIFO\u0026#34; visualizer_data_source = \u0026#34;/tmp/mpd.fifo\u0026#34; visualizer_type = \u0026#34;spectrum\u0026#34; visualizer_in_stereo = \u0026#34;yes\u0026#34; visualizer_look = \u0026#34;●▋\u0026#34; visualizer_output_name：指定可视化的输出名称，与 MPD 配置一致。\nvisualizer_data_source：FIFO 文件路径，必须与 MPD 的配置一致。\nvisualizer_type：\nspectrum：频谱图。\nwave：波形图。 visualizer_in_stereo：启用立体声显示（可选）。\nvisualizer_look：设置频谱柱字符样式，例如 ●▋。\n进入 Ncmpcpp 开启波频图 首先要播放一首歌，之后打开 7 输出设备管理，可以看到一个新的设备，回车打开\n之后在 8 音乐可视化页面，就可以看到波频图了，否则是看不到的\n之后就完成了。\n我的配置 下面是我使用的一些美化设置：\n来自 – vim ~/.mpd/mpd.conf music_directory \u0026#34;~/Music\u0026#34; playlist_directory \u0026#34;~/.mpd/playlists\u0026#34; db_file \u0026#34;~/.mpd/mpd.db\u0026#34; log_file \u0026#34;~/.mpd/mpd.log\u0026#34; pid_file \u0026#34;~/.mpd/mpd.pid\u0026#34; state_file \u0026#34;~/.mpd/mpdstate\u0026#34; bind_to_address \u0026#34;127.0.0.1\u0026#34; port \u0026#34;6600\u0026#34; audio_output { type \u0026#34;osx\u0026#34; name \u0026#34;Mac Audio\u0026#34; buffer_time \u0026#34;12\u0026#34; } audio_output { type \u0026#34;fifo\u0026#34; # 使用 FIFO 类型 name \u0026#34;Visualizer\u0026#34; # 输出名称，可自定义 path \u0026#34;/tmp/mpd.fifo\u0026#34; # FIFO 文件的路径 format \u0026#34;44100:16:2\u0026#34; # 音频格式（采样率:位深度:声道数） } 来自 – vim ~/.ncmpcpp/config mpd_music_dir = \u0026#34;~/Music\u0026#34; lyrics_directory = ~/.ncmpcpp/lyrics ncmpcpp_directory = ~/.ncmpcpp mpd_host = \u0026#34;localhost\u0026#34; mpd_port = \u0026#34;6600\u0026#34; mpd_connection_timeout = \u0026#34;5\u0026#34; mpd_crossfade_time = \u0026#34;5\u0026#34; # Playlist playlist_disable_highlight_delay = \u0026#34;0\u0026#34; playlist_display_mode = \u0026#34;columns\u0026#34; playlist_show_remaining_time = \u0026#34;yes\u0026#34; browser_display_mode = \u0026#34;columns\u0026#34; autocenter_mode = \u0026#34;yes\u0026#34; fancy_scrolling = \u0026#34;yes\u0026#34; follow_now_playing_lyrics = \u0026#34;yes\u0026#34; display_screens_numbers_on_start = \u0026#34;yes\u0026#34; ignore_leading_the = \u0026#34;yes\u0026#34; lyrics_database = \u0026#34;1\u0026#34; song_columns_list_format = \u0026#34;(10)[blue]{l} (30)[green]{a} (30)[magenta]{b} (50)[yellow]{t}\u0026#34; colors_enabled = \u0026#34;yes\u0026#34; main_window_color = \u0026#34;white\u0026#34; main_window_highlight_color = \u0026#34;blue\u0026#34; header_window_color = \u0026#34;cyan\u0026#34; volume_color = \u0026#34;red\u0026#34; progressbar_color = \u0026#34;cyan\u0026#34; statusbar_color = \u0026#34;white\u0026#34; active_column_color = \u0026#34;cyan\u0026#34; active_window_border = \u0026#34;blue\u0026#34; alternative_header_first_line_format = \u0026#34;\u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;0\u0026lt;/span\u0026gt;aqqu\u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;/a {\u0026lt;/span\u0026gt;7%a - \u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;9}{\u0026lt;/span\u0026gt;5%t\u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;9}|{\u0026lt;/span\u0026gt;8%f\u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;9}\u0026lt;/span\u0026gt;0\u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;atqq\u0026lt;/span\u0026gt;/a\u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;9\u0026#34; alternative_header_second_line_format = \u0026#34;{​{\u0026lt;/span\u0026gt;6%b\u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;9}{ [\u0026lt;/span\u0026gt;6%y\u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;9]}​}|{%D}\u0026#34; song_list_format = \u0026#34;{\u0026lt;/span\u0026gt;3%n │ \u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;9}{\u0026lt;/span\u0026gt;7%a - \u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;9}{\u0026lt;/span\u0026gt;5%t\u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;9}|{\u0026lt;/span\u0026gt;8%f\u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;9}\u0026lt;/span\u0026gt;R{\u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;6 │ %b\u0026lt;/span\u0026gt;9}{\u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;3 │ %l\u0026lt;/span\u0026gt;9}\u0026#34; user_interface = \u0026#34;alternative\u0026#34; #user_interface = \u0026#34;classic\u0026#34; default_place_to_search_in = \u0026#34;database\u0026#34; # visualizer visualizer_data_source = \u0026#34;/tmp/mpd.fifo\u0026#34; visualizer_output_name = \u0026#34;my_fifo\u0026#34; #visualizer_type = \u0026#34;wave\u0026#34; (spectrum/wave) visualizer_type = \u0026#34;spectrum\u0026#34; (spectrum/wave) visualizer_in_stereo = \u0026#34;yes\u0026#34; visualizer_look = \u0026#34;●▋\u0026#34; ## Navigation ## cyclic_scrolling = \u0026#34;yes\u0026#34; header_text_scrolling = \u0026#34;yes\u0026#34; jump_to_now_playing_song_at_start = \u0026#34;yes\u0026#34; lines_scrolled = \u0026#34;2\u0026#34; ## Other ## system_encoding = \u0026#34;utf-8\u0026#34; regular_expressions = \u0026#34;extended\u0026#34; ## Selected tracks ## selected_item_prefix = \u0026#34;* \u0026#34; discard_colors_if_item_is_selected = \u0026#34;no\u0026#34; ## Seeking ## incremental_seeking = \u0026#34;yes\u0026#34; seek_time = \u0026#34;1\u0026#34; ## Visivility ## header_visibility = \u0026#34;yes\u0026#34; statusbar_visibility = \u0026#34;yes\u0026#34; titles_visibility = \u0026#34;yes\u0026#34; progressbar_look = \u0026#34;=\u0026gt;-\u0026#34; progressbar_elapsed_color = \u0026#34;white\u0026#34; now_playing_prefix = \u0026#34;\u0026gt; \u0026#34; song_status_format = \u0026#34; \u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;2%a\u0026lt;/span\u0026gt;4⟫\u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;3⟫\u0026lt;/span\u0026gt;8%t \u0026lt;span class=\u0026#34;katex math inline\u0026#34;\u0026gt;4⟫\u0026lt;/span\u0026gt;3⟫ $5%b \u0026#34; autocenter_mode = \u0026#34;yes\u0026#34; centered_cursor = \u0026#34;yes\u0026#34; # Misc display_bitrate = \u0026#34;yes\u0026#34; # enable_window_title = \u0026#34;no\u0026#34; follow_now_playing_lyrics = \u0026#34;yes\u0026#34; ignore_leading_the = \u0026#34;yes\u0026#34; empty_tag_marker = \u0026#34;\u0026#34; 美化参数指南：https://twily.info/.ncmpcpp/config\n中文美化教程：https://blog.yangmame.org/mpd-ncmpcpp%E9%85%8D%E7%BD%AE%E7%BE%8E%E5%8C%96%E6%95%99%E7%A8%8B.html\n完成的美化样式配置大全(有图)：http://dotshare.it/category/mpd/ncmpcpp/\n疑难杂症 专辑 / 音乐名字没有正常显示 如果你的音乐出现这样的问题，其实是音乐的 tag 标签不全。\n你可以在 6 标签编辑器里面补全。\n也可以使用一些好用的，可视化的标签修改器，编辑补全。\n如开源的 – MusicBrainz Picard、Kid3\n","permalink":"https://blog.dontalk.org/posts/mac/linux-%E4%BD%BF%E7%94%A8%E7%BB%88%E7%AB%AF%E6%9D%A5%E6%92%AD%E6%94%BE%E9%9F%B3%E4%B9%90%E5%B9%B6%E4%B8%94%E4%B8%AA%E6%80%A7%E5%8C%96%E6%88%91%E4%BB%AC%E7%9A%84%E6%92%AD%E6%94%BE%E5%99%A8/","summary":"我使用过很多流媒体播放器，我实在喜欢Spotify。但是我也希望给我离线30G(目前来说)音乐库一个家。我尝试过不少离线播放器，实在牵强人意，而且在我使用Linux和Mac之后，选择性就变得更少(实则不然，只是我太挑剔，很多用过觉得不好就删了)。而最终，为何不试试在终端播放音乐呢？反正我已经泡在终端里出不来。\n正文 所以，我发了一个博文。我比较喜欢的两个终端 …","title":"Mac/Linux 使用终端来播放音乐,并且个性化我们的播放器"},{"content":"一个API的标准是什么？这个定位每个人心中，对每个项目都不同，但是有一个确实是需要考虑的，API接口的认证。\n一般来说，除了前端认证，如果API需要认证(重要)，可以从几个方面下手呢\nAPI 不同应用场景 下面是API的一些场景\n开放 – 一般用于开放不敏感数据，如天气预告，新闻等等。\nAPI Key – 一般用于服务器内部沟通，或者对开放不敏感数据添加一些访问限制\nSession – 一般用于登录状态、权限、购物车维持。有状态，无需在每次请求中传递这些信息。\nJWT – 一般用于用户登录认证，无状态，可以携带很多信息(速度会慢)，所以可以按照用户(组)不同，给他们不同的访问策略。\nOAuth 2.0 – 标准化流程，授权细粒度，安全性高。其实就是第三方授权。实现复杂，适配成本高。\n提前具体说说重要的 Session 和 JWT 认证：\nSession： 优点：\n比较安全，因为Session(依赖Cookie)存储在服务器。 适合前后端同源的应用 – Session 依赖于 Cookie，而 Cookie 的自动传递特性使其非常适合传统的 Web 应用（同源系统）。(如PHP前后端混写等情况..)\n支持复杂的用户场景 – 适合需要长期会话、复杂状态管理的场景，例如电商购物车、用户权限控制等。 缺点：\n不适合分布式系统 – 因为 Session 是存储在服务器上的，因此在分布式架构中（多个服务器或容器），需要额外的机制（如共享存储或 Session 集群）来同步 Session 数据。\n不适用于无状态 API – RESTful API 通常是无状态的，Session 的状态依赖性与 REST 的设计原则冲突。\n对服务器压力较大 – 果有大量用户访问，服务器需要存储大量的 Session 数据。\n跨域问题 – Session 依赖于 Cookie，而 Cookie 默认不支持跨域（需要设置 CORS 和 SameSite 属性）。 JWT： 优点：\n无状态 – JWT 是自包含的，所有认证数据（如用户信息、权限等）都存储在 Token 中，服务器不需要维护会话状态。\n适合分布式系统(前后端分离在不同服务器/容器)，多个服务器或微服务可以共享同一个 JWT，无需额外的存储同步。\n服务器压力低，而且JWT扩展性好，可以一次携带如用户角色（role）、权限（scope）、过期时间（exp）等。\n可以跨域，而且安全性也好(和Session一样，它们安全性都好)\n缺点：\nJWT 是无状态的，因此服务器无法强制使已经发放的 Token 失效。如果 JWT 被泄露，攻击者可以在有效期内冒充用户\nJWT 的 Payload 是 Base64 编码的，而不是加密的，任何人都可以解码并查看其中的内容。所以如果 Payload 中包含敏感信息（如密码、个人数据），可能会导致泄露。(如果必须在Payload传递敏感数据，可以对JWT进行加密)\nToken 大小可能较大，因为 JWT 通常包含用户信息和签名，体积可能比简单的 Session ID 或 API Key 大很多。如果 Payload 数据较多，Token 长度会显著增长，增加网络传输的开销。\n不要泄露源代码的”JWT密码”，否则骇客可以通过密钥去伪造JWT。如果密钥需要更新，所有旧的 JWT 都会失效。\n不支持会话上下文，如用户登录后权限变化（如被管理员禁用），JWT 无法反映这些实时变化，除非重新签发新的 Token。可以在 JWT 中加入版本号（version），每次用户权限变更时增加版本号，验证时检查版本号是否匹配。\nSession \u0026amp; API Key \u0026amp; JWT 对比 特性 Session API Key JWT 状态性 有状态（服务器存储用户数据） 无状态（仅通过 Key 验证） 无状态（Token 中包含用户数据） 存储位置 服务器端存储 Session 数据 无需存储（Key 由客户端携带） 无需存储（Token 自包含信息） 安全性 较高，敏感数据存储在服务器端 较低，API Key 曝露后可被滥用 较高，Token 签名保证完整性 适用场景 前后端同源系统，复杂状态管理 简单的服务间通信或公开 API RESTful API，分布式认证，用户授权 性能 服务器压力较大（需存储 Session 数据） 性能高，无需服务器存储 性能高，无需服务器存储 实现复杂性 简单，传统 Web 应用常用 简单，适合快速实现 较复杂，需要签名、解析和 Token 管理 跨域支持 较差（Cookie 默认限制跨域） 较好（适用于多来源的客户端） 较好（Token 可用于任何来源） 会话过期 服务器端控制（主动销毁或过期） 不支持，需要手动更新 Key 支持过期时间，Token 自动失效 实现 Nodejs + Express API Key // app.js const express = require(\u0026#39;express\u0026#39;); const app = express(); app.use(express.json()); // 模拟存储的 API Key 数据库 const apiKeys = [\u0026#39;key1\u0026#39;, \u0026#39;key2\u0026#39;, \u0026#39;key3\u0026#39;]; // 中间件：校验 API Key function checkApiKey(req, res, next) { const apiKey = req.headers[\u0026#39;x-api-key\u0026#39;]; // 从请求头获取 API Key if (!apiKey || !apiKeys.includes(apiKey)) { return res.status(403).json({ message: \u0026#39;Invalid API Key\u0026#39; }); } next(); // 验证通过，继续处理请求 } // 示例 API 路由 app.get(\u0026#39;/api/data\u0026#39;, checkApiKey, (req, res) =\u0026gt; { res.json({ data: \u0026#39;This is protected data.\u0026#39; }); }); // 启动服务 app.listen(3000, () =\u0026gt; console.log(\u0026#39;Server running on http://localhost:3000\u0026#39;)); 测试\n1.请求 URL：http://localhost:3000/api/data\n2.请求头：x-api-key: key1\n3.响应：\n– 成功：\n{ \u0026#34;data\u0026#34;: \u0026#34;This is protected data.\u0026#34; } 失败（无效 Key）： { \u0026#34;message\u0026#34;: \u0026#34;Invalid API Key\u0026#34; } Session const express = require(\u0026#39;express\u0026#39;); const session = require(\u0026#39;express-session\u0026#39;); const app = express(); app.use(express.json()); // 配置 Session app.use( session({ secret: \u0026#39;your_secret_key\u0026#39;, // 用于加密 Session ID 的密钥 resave: false, // 是否强制保存会话 saveUninitialized: false, // 是否为未初始化的会话存储 Session cookie: { httpOnly: true, maxAge: 3600000 }, // 1小时过期 }) ); // 模拟用户数据库 const users = [{ id: 1, username: \u0026#39;user1\u0026#39;, password: \u0026#39;password1\u0026#39; }]; // 登录路由：创建 Session app.post(\u0026#39;/login\u0026#39;, (req, res) =\u0026gt; { const { username, password } = req.body; const user = users.find(u =\u0026gt; u.username === username \u0026amp;\u0026amp; u.password === password); if (!user) { return res.status(401).json({ message: \u0026#39;Invalid credentials\u0026#39; }); } // 将用户 ID 存储到 Session req.session.userId = user.id; res.json({ message: \u0026#39;Login successful\u0026#39; }); }); // 受保护路由：验证 Session app.get(\u0026#39;/protected\u0026#39;, (req, res) =\u0026gt; { if (!req.session.userId) { return res.status(401).json({ message: \u0026#39;Unauthorized\u0026#39; }); } res.json({ message: \u0026#39;Welcome to the protected route\u0026#39; }); }); // 启动服务 app.listen(3000, () =\u0026gt; console.log(\u0026#39;Server running on http://localhost:3000\u0026#39;)); 测试\n1.登录：\n请求 URL：http://localhost:3000/login\n请求体： { \u0026#34;username\u0026#34;: \u0026#34;user1\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;password1\u0026#34; } 响应： { \u0026#34;message\u0026#34;: \u0026#34;Login successful\u0026#34; } 返回的 Cookie 将包含 Session ID。 2.访问受保护资源：\n请求 URL：http://localhost:3000/protected\n请求头：携带之前的 Cookie。\n响应：\n{ \u0026#34;message\u0026#34;: \u0026#34;Welcome to the protected route\u0026#34; } JWT const express = require(\u0026#39;express\u0026#39;); const jwt = require(\u0026#39;jsonwebtoken\u0026#39;); const app = express(); app.use(express.json()); // 模拟用户数据库 const users = [{ id: 1, username: \u0026#39;user1\u0026#39;, password: \u0026#39;password1\u0026#39; }]; // 登录路由：生成 JWT app.post(\u0026#39;/login\u0026#39;, (req, res) =\u0026gt; { const { username, password } = req.body; const user = users.find(u =\u0026gt; u.username === username \u0026amp;\u0026amp; u.password === password); if (!user) { return res.status(401).json({ message: \u0026#39;Invalid credentials\u0026#39; }); } // 生成 JWT const token = jwt.sign( { id: user.id, username: user.username }, // Payload \u0026#39;your_secret_key\u0026#39;, // Secret Key { expiresIn: \u0026#39;1h\u0026#39; } // Token 有效期 ); res.json({ token }); }); // 受保护路由：验证 JWT app.get(\u0026#39;/protected\u0026#39;, (req, res) =\u0026gt; { const authHeader = req.headers[\u0026#39;authorization\u0026#39;]; const token = authHeader \u0026amp;\u0026amp; authHeader.split(\u0026#39; \u0026#39;)[1]; // Bearer \u0026lt;Token\u0026gt; if (!token) { return res.status(401).json({ message: \u0026#39;Unauthorized\u0026#39; }); } // 验证 Token jwt.verify(token, \u0026#39;your_secret_key\u0026#39;, (err, user) =\u0026gt; { if (err) { return res.status(403).json({ message: \u0026#39;Invalid or expired token\u0026#39; }); } res.json({ message: \u0026#39;Welcome to the protected route\u0026#39;, user }); }); }); // 启动服务 app.listen(3000, () =\u0026gt; console.log(\u0026#39;Server running on http://localhost:3000\u0026#39;)); 测试\n1.登录：\n请求 URL：http://localhost:3000/login\n请求体： { \u0026#34;username\u0026#34;: \u0026#34;user1\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;password1\u0026#34; } 响应： { \u0026#34;token\u0026#34;: \u0026#34;\u0026lt;JWT_TOKEN\u0026gt;\u0026#34; } 2.访问受保护资源：\n请求 URL：http://localhost:3000/protected\n请求头：Authorization: Bearer \u0026lt;JWT_TOKEN\u0026gt;\n响应：\n{ \u0026#34;message\u0026#34;: \u0026#34;Welcome to the protected route\u0026#34;, \u0026#34;user\u0026#34;: { \u0026#34;id\u0026#34;: 1, \u0026#34;username\u0026#34;: \u0026#34;user1\u0026#34; } } Python + FastAPI API Key from fastapi import FastAPI, Header, HTTPException app = FastAPI() # 模拟存储的 API Key 数据库 API_KEYS = {\u0026#34;key1\u0026#34;, \u0026#34;key2\u0026#34;, \u0026#34;key3\u0026#34;} # 校验 API Key 的依赖 def verify_api_key(x_api_key: str = Header(None)): if x_api_key not in API_KEYS: raise HTTPException(status_code=403, detail=\u0026#34;Invalid API Key\u0026#34;) # 示例受保护路由 @app.get(\u0026#34;/protected\u0026#34;, dependencies=[verify_api_key]) async def protected_data(): return {\u0026#34;message\u0026#34;: \u0026#34;This is protected data.\u0026#34;} # 示例公开路由 @app.get(\u0026#34;/\u0026#34;) async def public_data(): return {\u0026#34;message\u0026#34;: \u0026#34;Welcome to the public route.\u0026#34;} 测试\n1.访问受保护路由：\nURL：http://127.0.0.1:8000/protected\n请求头：x-api-key: key1\n响应：\n{ \u0026#34;message\u0026#34;: \u0026#34;This is protected data.\u0026#34; } 2.如果 API Key 无效：\n返回： { \u0026#34;detail\u0026#34;: \u0026#34;Invalid API Key\u0026#34; } Session from fastapi import FastAPI, Depends, HTTPException from fastapi.middleware.cors import CORSMiddleware from starlette.requests import Request from starlette.responses import JSONResponse from fastapi.session import FastAPISessionMiddleware from fastapi_session import MemorySessionBackend app = FastAPI() # 设置 Session 后端（内存存储） app.add_middleware( FastAPISessionMiddleware, backend=MemorySessionBackend(), secret_key=\u0026#34;your_secret_key\u0026#34;, ) # 模拟用户数据库 USERS = {\u0026#34;user1\u0026#34;: \u0026#34;password1\u0026#34;} # 登录接口：创建 Session @app.post(\u0026#34;/login\u0026#34;) async def login(request: Request, username: str, password: str): if USERS.get(username) != password: raise HTTPException(status_code=401, detail=\u0026#34;Invalid credentials\u0026#34;) # 创建 Session 数据 session = await request.session() session[\u0026#34;user\u0026#34;] = username return {\u0026#34;message\u0026#34;: \u0026#34;Login successful\u0026#34;} # 受保护路由：验证 Session @app.get(\u0026#34;/protected\u0026#34;) async def protected_route(request: Request): session = await request.session() if \u0026#34;user\u0026#34; not in session: raise HTTPException(status_code=401, detail=\u0026#34;Unauthorized\u0026#34;) return {\u0026#34;message\u0026#34;: f\u0026#34;Welcome {session[\u0026#39;user\u0026#39;]} to the protected route\u0026#34;} 测试\n1.登录：\nURL：http://127.0.0.1:8000/login\n请求体： { \u0026#34;username\u0026#34;: \u0026#34;user1\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;password1\u0026#34; } 响应： { \u0026#34;message\u0026#34;: \u0026#34;Login successful\u0026#34; } 返回的响应 Cookie 包含 Session ID。 2.访问受保护资源：\nURL：http://127.0.0.1:8000/protected\n请求头：自动携带之前返回的 Cookie。\n响应：\n{ \u0026#34;message\u0026#34;: \u0026#34;Welcome user1 to the protected route\u0026#34; } 3.未登录访问受保护资源：\n– 返回：\n{ \u0026#34;detail\u0026#34;: \u0026#34;Unauthorized\u0026#34; } JWT from fastapi import FastAPI, Depends, HTTPException from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from jose import JWTError, jwt from datetime import datetime, timedelta app = FastAPI() # 密钥配置 SECRET_KEY = \u0026#34;your_secret_key\u0026#34; ALGORITHM = \u0026#34;HS256\u0026#34; ACCESS_TOKEN_EXPIRE_MINUTES = 30 # 模拟用户数据库 USERS = {\u0026#34;user1\u0026#34;: {\u0026#34;username\u0026#34;: \u0026#34;user1\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;password1\u0026#34;}} # 用于 OAuth2 的 Token URL oauth2_scheme = OAuth2PasswordBearer(tokenUrl=\u0026#34;login\u0026#34;) # 生成 JWT Token def create_access_token(data: dict, expires_delta: timedelta = None): to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=15) to_encode.update({\u0026#34;exp\u0026#34;: expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt # 登录接口：生成 JWT @app.post(\u0026#34;/login\u0026#34;) async def login(form_data: OAuth2PasswordRequestForm = Depends()): user = USERS.get(form_data.username) if not user or user[\u0026#34;password\u0026#34;] != form_data.password: raise HTTPException(status_code=401, detail=\u0026#34;Invalid credentials\u0026#34;) access_token = create_access_token(data={\u0026#34;sub\u0026#34;: form_data.username}) return {\u0026#34;access_token\u0026#34;: access_token, \u0026#34;token_type\u0026#34;: \u0026#34;bearer\u0026#34;} # 验证 JWT 的依赖 async def get_current_user(token: str = Depends(oauth2_scheme)): try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username: str = payload.get(\u0026#34;sub\u0026#34;) if username is None: raise HTTPException(status_code=401, detail=\u0026#34;Invalid token\u0026#34;) except JWTError: raise HTTPException(status_code=401, detail=\u0026#34;Invalid token\u0026#34;) return username # 受保护路由 @app.get(\u0026#34;/protected\u0026#34;) async def protected_route(current_user: str = Depends(get_current_user)): return {\u0026#34;message\u0026#34;: f\u0026#34;Welcome {current_user} to the protected route\u0026#34;} 测试\n1.登录：\nURL：http://127.0.0.1:8000/login\n请求体：username=user1\u0026amp;password=password1\n响应：\n{ \u0026#34;access_token\u0026#34;: \u0026#34;\u0026lt;JWT_TOKEN\u0026gt;\u0026#34;, \u0026#34;token_type\u0026#34;: \u0026#34;bearer\u0026#34; } 2.访问受保护资源：\nURL：http://127.0.0.1:8000/protected\n请求头：Authorization: Bearer \u0026lt;JWT_TOKEN\u0026gt;\n响应：\n{ \u0026#34;message\u0026#34;: \u0026#34;Welcome user1 to the protected route\u0026#34; } 3.Token 无效或过期：\n返回： { \u0026#34;detail\u0026#34;: \u0026#34;Invalid token\u0026#34; } ","permalink":"https://blog.dontalk.org/posts/%E7%8E%B0%E4%BB%A3%E7%BD%91%E9%A1%B5%E5%BC%80%E5%8F%91%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB-%E4%B9%8B-api/","summary":"一个API的标准是什么？这个定位每个人心中，对每个项目都不同，但是有一个确实是需要考虑的，API接口的认证。\n一般来说，除了前端认证，如果API需要认证(重要)，可以从几个方面下手呢\nAPI 不同应用场景 下面是API的一些场景\n开放 – 一般用于开放不敏感数据，如天气预告，新闻等等。\nAPI Key – 一般用于服务器内部沟通，或者对开放不敏感数据添加一些 …","title":"【现代网页开发】前后端分离 之 API"},{"content":"创建一个数据库并不难，无非是 Root 进入到数据库后，输入 CREATE DATABASE 数据库名; 的事。但是很多人在输入数据库账号密码时，都默契地给出 Root 的账户。这并不优雅(符合标准流程)，或者说，符合我们的安全需要。所以，如何做才是优雅的呢，符合标准流程，安全需要的呢？\n正文： 1.首先 ，我们先使用 Root(数据库的Root) 进入到 MySQL 去。\nmysql -u root -p 2.之后 ，创建一个数据库，比如说我们的数据库名叫 forumdb。(数据库语法在保证没错的前提下，大小写都行)\nCREATE DATABASE forumdb; 3.之后 ，我们创建一个名为 test 并且只能 本地访问 的数据库用户。(你的网站和数据库一般都在一台机器上，所以这样做。这样做也会安全许多。)\n# 三选一 # 本地访问的用户 - 推荐 (网站和数据库在同一台服务器) CREATE USER \u0026#39;test\u0026#39;@\u0026#39;localhost\u0026#39; IDENTIFIED BY \u0026#39;设置该账户的密码\u0026#39;; # 允许远程访问的用户 - (网站和数据库在不同的服务器) CREATE USER \u0026#39;test\u0026#39;@\u0026#39;%\u0026#39; IDENTIFIED BY \u0026#39;设置该账户的密码\u0026#39;; # 只允许特定IP访问的用户 CREATE USER \u0026#39;test\u0026#39;@\u0026#39;IP地址\u0026#39; IDENTIFIED BY \u0026#39;设置该账户的密码\u0026#39;; 4.之后 ，我们授予我们刚刚创建的账户 test 访问 forumdb 这个数据库的权限。\n# 根据上面的设置 - 三选一 # 给予本地访问的用户权限 - 推荐(网站和数据库在同一台服务器) GRANT ALL PRIVILEGES ON forumdb.* TO \u0026#39;test\u0026#39;@\u0026#39;localhost\u0026#39;; # 给予远程访问的用户权限 - (网站和数据库在不同的服务器) GRANT ALL PRIVILEGES ON forumdb.* TO \u0026#39;test\u0026#39;@\u0026#39;%\u0026#39;; # 给予特定IP访问的权限 GRANT ALL PRIVILEGES ON wpd.* TO \u0026#39;test\u0026#39;@\u0026#39;IP地址\u0026#39;; 上方 ALL PRIVILEGES\n– 表示授予该用户对 wpd 数据库的所有操作权限（如 SELECT、INSERT、UPDATE、DELETE 等）。\n如果希望限制为特定权限，可以改为如(本地访问用户的例子)： GRANT SELECT, INSERT, UPDATE, DELETE ON forumdb.* TO \u0026#39;test\u0026#39;@\u0026#39;localhost\u0026#39;; 5.最后 ，我们刷新权限，以使得我们的设置生效。\nFLUSH PRIVILEGES; 完成： 后记： 如果，你希望 查看用户权限 的话：\nSHOW GRANTS FOR \u0026#39;test\u0026#39;@\u0026#39;localhost\u0026#39;; 如果，你希望 删除这个用户 的话：\nDROP USER \u0026#39;test\u0026#39;@\u0026#39;localhost\u0026#39;; ","permalink":"https://blog.dontalk.org/posts/%E5%A6%82%E4%BD%95%E4%B8%BA%E6%9F%90%E4%B8%AA%E7%BD%91%E7%AB%99%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA%E6%95%B0%E6%8D%AE%E5%BA%93%E5%92%8C%E6%95%B0%E6%8D%AE%E5%BA%93%E8%B4%A6%E5%8F%B7%E4%BE%9B%E5%85%B6%E4%BD%BF%E7%94%A8/","summary":"创建一个数据库并不难，无非是 Root 进入到数据库后，输入 CREATE DATABASE 数据库名; 的事。但是很多人在输入数据库账号密码时，都默契地给出 Root 的账户。这并不优雅(符合标准流程)，或者说，符合我们的安全需要。所以，如何做才是优雅的呢，符合标准流程，安全需要的呢？\n正文： 1.首先 ，我们先使用 Root(数据库的Root) 进入到 …","title":"如何为某个网站创建一个数据库和数据库账号供其使用。"},{"content":"Mac原生的终端只能说..能用，用起来并没有多舒服。之后安装了iTerm2，但是事实上和原生终端并无二异(乍一看)。所以我们还需要对iTerm2内部做一些美化。\n(事实上iTerm2提供很多的功能，此处不做探讨)\n我们通过下面命令手动安装 zimfw (下面适用于Linux)\ncurl -fsSL https://raw.githubusercontent.com/zimfw/install/master/install.zsh | zsh 不要用Homebrew安装，安装出来莫名其妙的，直接用官方提供的方法。\n之后我们编辑 .zimrc 这个文件\nvim ~/.zimrc 在文件最后面追加下面内容，以添加 powerlevel10k 模块来美化Shell\nzmodule romkatv/powerlevel10k 之后通过下面命令来安装刚刚引入的模块\nzimfw install 注意： 如果显示找不到zimfw这个命令，请重启一个新的终端。重启终端一般会加载入zimfw，于是，甚至不再需要你执行 zimfw install。\n当出现下面界面，即代表重启终端后自动执行了 zimfw install 命令，安装好了 powerlevel10k 并启动了客制化(个性化)向导。\n如果英文水平有限/不想翻译。可以参考这篇知乎文章，我个人觉得写得不错 oh-my-zsh的powerlevel10k主题配置\n选择 y 来安装需要的字体。完毕后会要求你彻底重启终端(就是不留后台-对于Mac我想你们都懂)\n之后按照他的向导来做就好，大概这样：\n后面大概会有这种选择，让你选择一个你喜欢的风格，我选择了 1。\n当然，如果你不满意，想要重来，只需要按下 r\n个人建议：编码选择Unicode，颜色256种，其他看你喜欢\n瞬时提示也可以看需要开启，是否应用于 Apply changes to ~/.zshrc? 请确定\n我个人建议是开启(瞬时提示)的。不然插件多的话，你得等到加载完才能输入命令。\n瞬时提示(\u0026gt;)的意思是，打开终端先给你一个最原始 \u0026gt; 来使用，之后再加载个性和插。不然你得等待插件加载完毕\n完成后：\n之后请试试新的终端吧。\n","permalink":"https://blog.dontalk.org/posts/%E4%B8%BAmacos%E8%BD%BB%E9%87%8F%E5%8C%96%E5%9C%B0%E5%AE%9A%E5%88%B6%E4%B8%80%E4%B8%AA%E5%A5%BD%E7%9C%8B%E7%9A%84shell-%E6%9B%B4%E6%8E%A5%E8%BF%91%E5%8E%9F%E6%B1%81%E5%8E%9F%E5%91%B3%E7%9A%84zimfw%E9%80%82%E7%94%A8%E4%BA%8Elinux/","summary":"Mac原生的终端只能说..能用，用起来并没有多舒服。之后安装了iTerm2，但是事实上和原生终端并无二异(乍一看)。所以我们还需要对iTerm2内部做一些美化。\n(事实上iTerm2提供很多的功能，此处不做探讨)\n我们通过下面命令手动安装 zimfw (下面适用于Linux)\ncurl -fsSL …","title":"为MacOS轻量化地定制一个好看的Shell-更接近原汁原味的Zimfw(适用于Linux)"},{"content":"Btop++可以说是我用过最好用的终端“资源管理器”了。它很直观地写明了主机各样配置的占用情况，包括网络，并且支持鼠标直接点击使用，即便在无桌面的纯shell情况下。\n其次还提供了很多个性化的修改，ESC即可唤出菜单，通过终端可视化交互的方式修改，或者设置更多内容。\n但是有时，apt/yum之类的源，并没有提供btop++(btop)。(官方提供)。甚至有可能计算机需要离线使用，所以不妨试试手动安装一个btop++\n下面是btop++在终端内的截图。或许你会发现我其实电脑装了桌面环境。事实上在没有桌面环境的服务器也可以使用并且点击，和下图一模一样。之所以有桌面是因为这是我的一台虚拟实验机，最主要还是做实验。\n如果可以的话，你可以试试直接通过apt来安装btop++\napt update \u0026amp;\u0026amp; apt install btop 如果没有，或者版本旧，再或者需要离线安装，你可以在此处下载一个安装包。\nhttps://github.com/aristocratos/btop/releases/\n我们下面的操作以 AMDx64为例，安装目前(250326)最新的btop。\n不过，在此之前，我们需要安装。。额，我承认这一点有点抽象，因为btop使用tbz压缩。\napt install bzip2 因为不一定所有的系统都安装了tbz的压缩包支持。所以需要安装bzip2。\n之后我们在github下载最新的程序，当然，你需要找到合适你 架构 的程序，不过一般都是 x86_64 或者是 arm 的了。\n所以，如果你要在 离线主机 上面安装，除了要准备好 对应架构 的安装包，还要准备好 bzip2 的安装包\nwget https://github.com/aristocratos/btop/releases/download/v1.4.0/btop-x86_64-linux-musl.tbz 之后我们解压刚刚下载好的压缩包\nbunzip2 btop-x86_64-linux-musl.tbz tar xf btop-x86_64-linux-musl.tar 之后我们进去文件夹，编译安装\ncd btop make install PREFIX=/opt/btop ln -s /opt/btop/bin/btop /usr/bin/btop 如果你倒霉到连make命令都没有就:\napt install make # 之后再编译安装 make install PREFIX=/opt/btop ln -s /opt/btop/bin/btop /usr/bin/btop 注意： Debian之类的的纯白Bash有几率触发问题，就是字体变白，以为安装卡住了，其实可能早就完成了(一般在1分钟内完成)。在终端输入 clear 回车试试(清屏证明是当前终端字体变白了)，新建一个bash页面，输入 btop 试试(正常使用证明没问题，是上一个终端字体变白了)。\n之后你可以删除刚刚下载的和解压出来的文件。\n试试唤出btop吧！\nbtop 用鼠标点击一些选项或者ESC试试。\n","permalink":"https://blog.dontalk.org/posts/%E6%89%8B%E5%8A%A8%E5%AE%89%E8%A3%85%E4%B8%80%E4%B8%AA%E5%A5%BD%E7%9C%8B%E7%9A%84%E7%BB%88%E7%AB%AF%E8%B5%84%E6%BA%90%E7%AE%A1%E7%90%86%E5%99%A8btop-%E5%8D%B3%E4%BE%BF%E5%9C%A8%E6%97%A0%E6%A1%8C%E9%9D%A2%E7%9A%84%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%B9%9F%E5%8F%AF%E4%BB%A5%E8%BF%9B%E8%A1%8C%E9%BC%A0%E6%A0%87%E4%BA%A4%E4%BA%92/","summary":"Btop++可以说是我用过最好用的终端“资源管理器”了。它很直观地写明了主机各样配置的占用情况，包括网络，并且支持鼠标直接点击使用，即便在无桌面的纯shell情况下。\n其次还提供了很多个性化的修改，ESC即可唤出菜单，通过终端可视化交互的方式修改，或者设置更多内容。\n但是有时，apt/yum之类的源，并没有提供btop++(btop)。(官方提供)。甚至有可 …","title":"手动安装一个好看的终端资源管理器Btop++(即便在无桌面的服务器也可以进行鼠标交互)"},{"content":"我个人算是一个Linux忠实用户了，尝试过各种各样的发行版。比较惯用Debian、Arch，但是每次都需要重新安装Pinyin输入法才能输入中文。说到这个，安装中文拼音输入法也算是我自学Linux的一个大坑了，网络上的教程质量参差不齐，而且那会才读初中，没有多少时间浪费在计算机，虽然输入法安装风波已经过去了那么多年，但还是不可避免给我留下了一些阴影。\n于是，我打算写一篇博文，方便大家，也方便我自己梭哈(XD)。认真看完的话，估计Linux安装输入法就不再会困扰你了。\n安装Rime-中州韻输入法 1.首先，我们一如既往地更新源。 如果源很慢，可以改成当地源。源这个没什么好说的，相比于输入法，我个人觉得是小巫见大巫。所以不作讨论，起码现在不作讨论XD。\napt update 插一嘴，安装前可以先去开始菜单搜索一下Fcitx，像Gnome(其实是发行版的问题，后安装一般不带)，是自带了Fcitx的，所以能直接搜索出这个软件，建议先卸载一下。因为可能是fcitx4\napt remove fcitx # 卸载命令，也可以试试apt remove fcitx4/5 apt autoremove # 清理用不到的库 2.安装Fcitx5框架\napt install fcitx5 fcitx5-chinese-addons fcitx5-rime # 如若不指定5，一般安装的是4 # 一般APT的fcitx5包含了整个fcitx5，包括im，configtool等 请记得，一定要带个5。因为fcitx-rime和fcitx5-rime是两个东西。\n其他Linux (如Arch Linux)安装：(缺少什么，启动fcitx5时会提示，安装补上就行)\nsudo pacman -S fcitx5-im fcitx5-chinese-addons fcitx5-rime # fcitx5-im 提供了核心的fcitx5框架 # fcitx5-chinese-addons 提供额外的中文支持 # fcitx5-rime 则是我们需要的rime-pinyin 安装完毕后。在开始菜单就能搜索到几个Fcitx的软件了。(系统没有预装或者卸载了是搜索不到的)能搜索到就证明安装好了。搜索不到的话看看是不是安装失败了。\n(也可以通过命令启动：fcitx5 \u0026amp;)\n点击第一个，之后右下角或者右上角(具体看桌面)，就会出现一个键盘，右键它。选择Configure。\n注意(一些问题)：\n1.如果 你是Gnome桌面之类的，你可能在状态栏看不到它(不一定，具体看Gnome配置)，并且惊奇地发现，直接就可以切换Pinyin输入法使用。当然，如果不行，请查看有没有【fcitx5配置】这个软件。(如果状态栏没有输入法设置可以快速进入的话)\n2.如果 还是没有，你可以通过 fcitx5-configtool 命令进入控制面板。\n3.如果 控制面板提示 无法通过DBus连接到Fcitx\u0026hellip;. 并且点击无响应。这是个bug。因为wayland下会使用 /etc/zprofile 而非 /etc/profile 所以只需要 在 zprofile 中 souce /etc/profile 就行。\n正常来说(标准处理流程)： 进入fcitx5配置页面\n搜索rime，添加到左边。之后OK即可。如果搜索不到，就先注销系统再登陆 或者 重启系统。如果还是搜不到，只看到了中州韻。别忘了Rime就叫中州韻。\n之后，再右键，就是这样了。第二个就是Rime-pinyin了，但是点击或者切换一般是没用的。因为安装好后，必须注销系统，重新登陆。或者重启系统。\n（依旧不行，请找到文末 疑难杂症2）\n如果出现弹窗报错之类的，并且没法选中中文拼音输入法使用。那就重新执行下图操作。(把左侧Rime删除，再搜索，把右边Rime，添加到左边)即可恢复正常。\n注意： 经过测试，有一些系统Fcitx5可能不会开机自动启动，切换输入法就没有反应，需要手动启动一下Fcitx5。至于如何使其开机自动启动，请自行搜索。\n后记 1.更好的中文输入支持 (最好直接安装上)\nfcitx5-chinese-addons包提供中文特定的拼音和表格输入法支持，用户可以考虑安装它以获得与 fcitx5 最佳的中文集成体验。\napt install fcitx5-chinese-addons 事实上，一般你只需要安装 fcitx5 fcitx5-chinese-addons 两个库，就可以直接搜索Pinyin并添加，就可以输入中文拼音了。\n2.Rime(中州韻)输入法的其他Pinyin库|其他Pinyin输入法\n这个可以从Github或者其他地方同步，并安装入Rime，但这里不作推荐和讨论，需要更换的可以自行搜索。\n如果你希望安装其他Pinyin输入法，如GooglePinyin，本质上安装与Rime无异。套公式就行了。\n3.Rime(中州韻)输入法设置双拼/其他输入方式\n需要修改Rime的配置文件，具体这里也不作探讨，需要请自行搜索。\n(其实通过状态栏输入法配置就能快速切换。但是这因发行版(桌面)而异，有些没有的，甚至状态栏都没办法找到输入法配置。)\n疑难杂症 1.Linux中文出现方块乱码(豆腐块乱码) 如果系统中文(也可以是其他语言，非英语)变成了豆腐块一样的方框。你可以去查阅 =\u0026gt; Linux安装中文语言环境\n2.配置一些特定情况的Pinyin输入法支持(无法切换输入法试试这个) 修改用户目录下的 ~/.profile/ ~/.bashrc 文件。(不一定是这个文件，具体取决于桌面环境类型，但一般都是这个，如Arch有可能是 ~/.xprofile, ~/.zshrc)\n在里面追加一些内容，如：\n# export fcitx5 variable export LC_CTYPE=\u0026#34;zh_CN.UTF-8\u0026#34; # 这段跟系统设置编码有关，不设置为\u0026#34;zh_CN.UTF-8\u0026#34;会报找不到，直接不要这行影响也不大 export GTK_IM_MODULE=fcitx5 export QT_IM_MODULE=fcitx5 export XMODIFIERS=\u0026#34;@im=fcitx5\u0026#34; 以便在GTK/QT库的程序下获得更好的支持。之后在浏览器(Chrome)等软件下，就会惊奇发现，可以切换输入法了。\n保存后运行 source ~/.修改的文件名 即可生效\n","permalink":"https://blog.dontalk.org/posts/%E4%B8%BAdebian12%E5%AE%89%E8%A3%85%E4%B8%80%E4%B8%AAfcitx5-rime%E4%B8%AD%E5%B7%9E%E9%9F%BB-pinyin%E6%8B%BC%E9%9F%B3%E8%BE%93%E5%85%A5%E6%B3%95/","summary":"我个人算是一个Linux忠实用户了，尝试过各种各样的发行版。比较惯用Debian、Arch，但是每次都需要重新安装Pinyin输入法才能输入中文。说到这个，安装中文拼音输入法也算是我自学Linux的一个大坑了，网络上的教程质量参差不齐，而且那会才读初中，没有多少时间浪费在计算机，虽然输入法安装风波已经过去了那么多年，但还是不可避免给我留下了一些阴影。\n于是， …","title":"为Debian12安装一个Fcitx5-Rime中州韻-Pinyin(拼音)输入法"},{"content":"问题：\n这里的问题：\n打开了CDN，SSL/TLS里面设置的问题。怎么样设置一个适合自己的配置，以及代表什么，看下图：\n一般都是完全的，但是这样的话，你得保证你的Nginx配置文件配置有证书。\n不然报错「重定向过多，请清理Cookie再试」还好，如果报了521，你根本不知道啥问题。\n我就找了半天，我nginx配置问题没有一点问题(由于测试，还没申请证书)，其他解析正常访问，解析了一个新的域名(即便开了CDN，因为两个域名的SSL/TLS设置不同)也可以正常访问，唯独这个开了CDN就是不行。(没有关闭测试过，不想暴露IP，所以没发现问题)。\n设置查看了端口占用，日志，啥问题没有，521明明是我服务器问题啊，但是我服务器确实又证明了没问题。直到我打开SSL/TLS页面，更改了这个配置后。。。原来是(没有)证书问题，byd给我报521。。找半天。\n之后我重新打开SSL/TLS设置为「完全」，为网站申请一下证书后，一切都正常了。\n","permalink":"https://blog.dontalk.org/posts/%E4%BD%BF%E7%94%A8cloudflarecdn%E7%BD%91%E7%AB%99%E6%8A%A5%E9%94%99%E9%87%8D%E5%AE%9A%E5%90%91%E6%AC%A1%E6%95%B0%E8%BF%87%E5%A4%9A%E6%88%96%E8%80%85521%E9%94%99%E8%AF%AF/","summary":"问题：\n这里的问题：\n打开了CDN，SSL/TLS里面设置的问题。怎么样设置一个适合自己的配置，以及代表什么，看下图：\n一般都是完全的，但是这样的话，你得保证你的Nginx配置文件配置有证书。\n不然报错「重定向过多，请清理Cookie再试」还好，如果报了521，你根本不知道啥问题。\n我就找了半天，我nginx配置问题没有一点问题(由于测试，还没申请证书)，其 …","title":"使用CloudFlareCDN，网站报错重定向次数过多或者521错误。"},{"content":" 欢迎来到Dontalk的商店！ 但是由于我暂时没有时间去完成这个网站的支付宝支付回调程序，所以你希望购买东西的话，请来到这个网站。\nhttps://shop.dontalk.org 请注意，只有【.dontalk.org】时才代表是我们的网站。你只有在属于我们的网站购买时，才受到我的服务条款保护。 与史诗对话。\n当猎枪瞄准无辜的“知更鸟”，扣动扳机的从不是具体持枪的某个人，而是沉默群体的怯懦与盲从。 ","permalink":"https://blog.dontalk.org/shop/","summary":" 欢迎来到Dontalk的商店！ 但是由于我暂时没有时间去完成这个网站的支付宝支付回调程序，所以你希望购买东西的话，请来到这个网站。\nhttps://shop.dontalk.org 请注意，只有【.dontalk.org】时才代表是我们的网站。你只有在属于我们的网站购买时，才受到我的服务条款保护。 与史诗对话。\n当猎枪瞄准无辜的“知更鸟”，扣动扳机的从不是 …","title":"商店"},{"content":"因为工作与学习的缘故，总是需要浏览全球文献，但是由于我有时的地理位置并不支持我自由地穿梭在全球互联网上。而且我的服务器总是容易被封锁端口，于是我就参考官方和第三方文档搭建了一个服务器，并且稳定半年使用都没有问题。\n于是，我决定打开一个教程，用于自己的温习和记录。\n前置条件 首先我们需要【一台】线路不错的【服务器】，起码在受限制的地区连接不会很慢。(延迟并不能代表一切)\n其次，我们需要【一个域名】\n我使用的一台香港的服务器，因为香港作为一个发达的网络数据处理中心，在很多地方连接都不会很慢(起码对我来说)。\n解析域名 我们需要解析一个【A记录】给服务器，请勿开启CDN。\n安装Xray \u0026amp; 所需软件 因为V2ray内部团队的问题，Xray从V2ray分家出来，Xray更像是V2ray的超集，完全支援V2ray。我们不过多探讨这些内容，总之就是，Xray的历史原因，使得其并不包含在deb官方仓库中(我使用的是Debian)，所以我们使用Xray官方提供的安装脚本来安装Xray：\n从官方脚本 安装Xray\nbash -c \u0026#34;$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)\u0026#34; @ install 安装Nginx和证书申请工具\napt install nginx python3-certbot python3-certbot-nginx 生成Xray所需的UUID\nxray uuid # 之后大概会得到类似下面的内容 # 3c0841a0-0017-4282-bd7f-afa72c006a3f 生成Reality所需的密钥\nxray x25519 # 之后大概会得到类似下面的内容 # Private key: SJcCdG*******o2TPC_CTzz*****EcU0 # Public key: RiM_pehn*******aTQ-xMBR*****B9Qk 开始配置 Xray 的 Config 文件\nmkdir -p /usr/xray/xray_log/ vim /usr/local/etc/xray/config.json 可以参考并替换成我的配置：(【需要修改】的已经用【六】个【/】来标出)\n{ \u0026#34;log\u0026#34;: { \u0026#34;loglevel\u0026#34;: \u0026#34;warning\u0026#34;, \u0026#34;access\u0026#34;: \u0026#34;/usr/xray/xray_log/access.log\u0026#34;, \u0026#34;error\u0026#34;: \u0026#34;/usr/xray/xray_log/error.log\u0026#34; }, \u0026#34;dns\u0026#34;: { \u0026#34;servers\u0026#34;: [ \u0026#34;https+local://1.1.1.1/dns-query\u0026#34;, // 首选 1.1.1.1 的 DoH 查询，牺牲速度但可防止 ISP 偷窥 \u0026#34;localhost\u0026#34; ] }, \u0026#34;routing\u0026#34;: { \u0026#34;domainStrategy\u0026#34;: \u0026#34;IPIfNonMatch\u0026#34;, \u0026#34;rules\u0026#34;: [ { // 防止服务器本地流转问题：如内网被攻击或滥用、错误的本地回环等 \u0026#34;type\u0026#34;: \u0026#34;field\u0026#34;, \u0026#34;ip\u0026#34;: [ \u0026#34;geoip:private\u0026#34; ], \u0026#34;outboundTag\u0026#34;: \u0026#34;block\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;field\u0026#34;, \u0026#34;ip\u0026#34;: [\u0026#34;geoip:cn\u0026#34;], \u0026#34;outboundTag\u0026#34;: \u0026#34;block\u0026#34; // 这里对中国大陆的访问做了抛弃处理，所以开了全局模式，让所有流量走到服务器，发现访问的是中国大陆网站时，Xray会做抛弃处理，不返回任何数据。 // 流量直连（direct） // 流量转发 VPS（proxy） // 流量屏蔽（block） }, // 屏蔽广告 { \u0026#34;type\u0026#34;: \u0026#34;field\u0026#34;, \u0026#34;domain\u0026#34;: [ \u0026#34;geosite:category-ads-all\u0026#34; ], \u0026#34;outboundTag\u0026#34;: \u0026#34;block\u0026#34; } ] }, // 入站设置(如若多个inbounds，则可以单服务器多协议，或者多节点) \u0026#34;inbounds\u0026#34;: [ { \u0026#34;port\u0026#34;: 443, \u0026#34;protocol\u0026#34;: \u0026#34;vless\u0026#34;, \u0026#34;settings\u0026#34;: { \u0026#34;clients\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;3c0841a0-0017-4282-bd7f-afa72c006a3f\u0026#34;, ////// 填入生成的UUID \u0026#34;flow\u0026#34;: \u0026#34;xtls-rprx-vision\u0026#34;, \u0026#34;level\u0026#34;: 0, \u0026#34;email\u0026#34;: \u0026#34;xxxx@gmail.com\u0026#34; ////// 电邮地址(随便) } ], \u0026#34;decryption\u0026#34;: \u0026#34;none\u0026#34;, \u0026#34;fallbacks\u0026#34;: [ { \u0026#34;dest\u0026#34;: 80 // 默认回落到防探测的代理 } ] }, \u0026#34;streamSettings\u0026#34;: { \u0026#34;network\u0026#34;: \u0026#34;tcp\u0026#34;, \u0026#34;security\u0026#34;: \u0026#34;reality\u0026#34;, \u0026#34;realitySettings\u0026#34;: { \u0026#34;show\u0026#34;: false, \u0026#34;dest\u0026#34;: \u0026#34;xxxx.com:8909\u0026#34;, ////// (解析的域名:Nginx配置的端口)。不知道端口是什么请往后看 \u0026#34;xver\u0026#34;: 0, \u0026#34;serverNames\u0026#34;: [ \u0026#34;xxxx.com\u0026#34; ////// 上面写的域名 ], \u0026#34;privateKey\u0026#34;: \u0026#34;SJcCdG*******o2TPC_CTzz*****EcU0\u0026#34;, ////// 上面生成的x25519私钥 \u0026#34;maxTimeDiff\u0026#34;: 0, \u0026#34;shortIds\u0026#34;: [ \u0026#34;abc123\u0026#34; ////// 0 到 f，长度为 2 的倍数，长度上限为 16 ] } } } ], // 出站设置 \u0026#34;outbounds\u0026#34;: [ { // 默认规则，freedom 就是对外直连（VPS 已经是外网，所以直连） \u0026#34;tag\u0026#34;: \u0026#34;direct\u0026#34;, \u0026#34;protocol\u0026#34;: \u0026#34;freedom\u0026#34; }, { // 屏蔽规则，blackhole 协议就是把流量导入到黑洞里（屏蔽） \u0026#34;tag\u0026#34;: \u0026#34;block\u0026#34;, \u0026#34;protocol\u0026#34;: \u0026#34;blackhole\u0026#34; } ] } 配置完毕上面的json文件后。\n写一个 HTML 静态网站。 把下面内容写入到 /var/www/hk/index.html\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026ldquo;zh-CN\u0026rdquo;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026ldquo;UTF-8\u0026rdquo;\u0026gt; \u0026lt;meta name=\u0026ldquo;viewport\u0026rdquo; content=\u0026ldquo;width=device-width, initial-scale=1.0\u0026rdquo;\u0026gt; \u0026lt;title\u0026gt;九龙城寨历史介绍\u0026lt;/title\u0026gt; \u0026lt;link href=\u0026ldquo;https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css\u0026quot; rel=\u0026ldquo;stylesheet\u0026rdquo;\u0026gt; \u0026lt;style\u0026gt; .intro-section { background-color: #f8f9fa; padding: 3rem 1rem; } .history-section { padding: 3rem 1rem; } .image-section img { max-width: 100%; border-radius: 10px; } .footer { background-color: #343a40; color: white; padding: 1rem 0; text-align: center; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;header class=\u0026ldquo;bg-dark text-white py-3\u0026rdquo;\u0026gt; \u0026lt;div class=\u0026ldquo;container\u0026rdquo;\u0026gt; \u0026lt;h1 class=\u0026ldquo;text-center\u0026rdquo;\u0026gt;九龙城寨历史介绍\u0026lt;/h1\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/header\u0026gt;\n\u0026amp;lt;section class=\u0026quot;intro-section text-center\u0026quot;\u0026amp;gt; \u0026amp;lt;div class=\u0026quot;container\u0026quot;\u0026amp;gt; \u0026amp;lt;h2 class=\u0026quot;mb-4\u0026quot;\u0026amp;gt;关于九龙城寨\u0026amp;lt;/h2\u0026amp;gt; \u0026amp;lt;p class=\u0026quot;lead\u0026quot;\u0026amp;gt; 九龙城寨是位于中国香港九龙的一座独特的城市结构，曾被称为“城中之城”。 它以高密度的建筑、复杂的社会结构和丰富的历史闻名于世。 \u0026amp;lt;/p\u0026amp;gt; \u0026amp;lt;/div\u0026amp;gt; \u0026amp;lt;/section\u0026amp;gt; \u0026amp;lt;section class=\u0026quot;history-section\u0026quot;\u0026amp;gt; \u0026amp;lt;div class=\u0026quot;container\u0026quot;\u0026amp;gt; \u0026amp;lt;h2 class=\u0026quot;text-center mb-4\u0026quot;\u0026amp;gt;历史背景\u0026amp;lt;/h2\u0026amp;gt; \u0026amp;lt;div class=\u0026quot;row align-items-center\u0026quot;\u0026amp;gt; \u0026amp;lt;div class=\u0026quot;col-lg-6\u0026quot;\u0026amp;gt; \u0026amp;lt;p\u0026amp;gt; 九龙城寨的历史可以追溯到宋朝，当时这里是一个军事要塞。清朝时期， 它被用作行政和防御中心。随着时间的推移，九龙城寨逐渐演变为一个 拥有高度自治的社区，居民依靠自己的规则生活。 \u0026amp;lt;/p\u0026amp;gt; \u0026amp;lt;p\u0026amp;gt; 在20世纪中期，九龙城寨成为一个无政府状态的区域，人口密集，建筑杂乱无章， 但也形成了一个自给自足的社会。1993年，九龙城寨被清拆，取而代之的是一座公园。 \u0026amp;lt;/p\u0026amp;gt; \u0026amp;lt;/div\u0026amp;gt; \u0026amp;lt;div class=\u0026quot;col-lg-6 image-section\u0026quot;\u0026amp;gt; \u0026amp;lt;img src=\u0026quot;https://upload.wikimedia.org/wikipedia/commons/3/3a/Kowloon_Walled_City.jpg\u0026quot; alt=\u0026quot;九龙城寨历史图片\u0026quot;\u0026amp;gt; \u0026amp;lt;/div\u0026amp;gt; \u0026amp;lt;/div\u0026amp;gt; \u0026amp;lt;/div\u0026amp;gt; \u0026amp;lt;/section\u0026amp;gt; \u0026amp;lt;section class=\u0026quot;intro-section text-center\u0026quot;\u0026amp;gt; \u0026amp;lt;div class=\u0026quot;container\u0026quot;\u0026amp;gt; \u0026amp;lt;h2 class=\u0026quot;mb-4\u0026quot;\u0026amp;gt;九龙城寨的特点\u0026amp;lt;/h2\u0026amp;gt; \u0026amp;lt;ul class=\u0026quot;list-unstyled\u0026quot;\u0026amp;gt; \u0026amp;lt;li class=\u0026quot;mb-2\u0026quot;\u0026amp;gt;高密度：建筑物层层叠叠，形成一个复杂的建筑网络。\u0026amp;lt;/li\u0026amp;gt; \u0026amp;lt;li class=\u0026quot;mb-2\u0026quot;\u0026amp;gt;自治：居民依靠自己的规则和秩序生活。\u0026amp;lt;/li\u0026amp;gt; \u0026amp;lt;li class=\u0026quot;mb-2\u0026quot;\u0026amp;gt;多样性：拥有各种商铺、工厂和居民区。\u0026amp;lt;/li\u0026amp;gt; \u0026amp;lt;/ul\u0026amp;gt; \u0026amp;lt;/div\u0026amp;gt; \u0026amp;lt;/section\u0026amp;gt; \u0026amp;lt;footer class=\u0026quot;footer\u0026quot;\u0026amp;gt; \u0026amp;lt;div class=\u0026quot;container\u0026quot;\u0026amp;gt; \u0026amp;lt;p\u0026amp;gt;© 2025 九龙城寨历史介绍 | 由 Bootstrap 5 构建\u0026amp;lt;/p\u0026amp;gt; \u0026amp;lt;/div\u0026amp;gt; \u0026amp;lt;/footer\u0026amp;gt; \u0026amp;lt;script src=\u0026quot;https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js\u0026quot;\u0026amp;gt;\u0026amp;lt;/script\u0026amp;gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 效果大概如下：\n写一段Nginx配置，并申请证书 把下面配置写入 /etc/nginx/conf.d/hk.conf\nserver { listen 80; server_name xxxx.com; # 你解析的域名 root /var/www/hk; # 刚刚 index.html 存放的目录 index index.html; } 保存完毕上面的内容后\n用下面命令查看Nginx配置语法是否有错\nnginx -t # 输出下面的内容则表明没问题 nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful 使用下面的内容重载Nginx和申请证书\n# 重载Nginx nginx -s reload # 申请证书 certbot --nginx 之后我们再次打开nginx的配置文件，大概就是这样了，我们进行一些修改\nserver { listen 80; server_name xxxx.com; # 你解析的域名 root /var/www/hk; # 刚刚 index.html 存放的目录 index index.html; listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/xxxx.com/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/xxxx.com/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } 修改后：\nserver { listen 8909 ssl http2; # 还记得吗，这里对应了Xray配置文件的端口 server_name xxxx.com; # 你解析的域名 root /var/www/hk; # 刚刚 index.html 存放的目录 index index.html; ssl_protocols TLSv1.2 TLSv1.3; ### 很重要！因为Reality只支持TLS1.3，如果不填写这项配置，节点将无效。因为Nginx默认使用TLSv1.1。强制使用TLSv1.2 TLSv1.3。 # listen 443 ssl; # managed by Certbot 注释或者删掉。下面证书申请到的地址和你的域名相关 ssl_certificate /etc/letsencrypt/live/xxxx.com/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/xxxx.com/privkey.pem; # managed by Certbot # include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } 修改完成，保存，之后继续看看nginx配置有没有问题，没问题就重载一下nginx\n如果你之后又修改了Nginx配置文件，请记得重载Nginx。\n打开网站，右键检查，找到安全的标签页，就能看到网站走的是TLSv几了\n新版Chrome合并后，这个页面叫:Privacy and Security。打开后在Security页查看。\n(第一次)启动 Xray systemctl enable --now xray # Xray不会报错的(即便无法运行)所以要用下面命令看看有没有启动成功。 systemctl status xray (手动安装的)Xray启动失败除了很明显写了你的配置文件有问题之外，就剩下权限问题了。一般来说，Xray的权限是Everyone，是很低很低的，所以你的xray必须放在/usr这个目录下，权限才会足够。(/usr/local/bin/xray)。或者你可以修改Xray的权限和归属组，但是我认为太麻烦了。\n完成后，重启Xray: systemctl restart xray\n这个时候应该就没问题了: systemctl status xray\n不过，目前xray除了手动安装外，安装脚本会把你的xray放在 /usr/local/bin/xray 下，所以不必担心。\nXray报错 – 疑难杂症 目录权限不够 我们的xray日志目录会不够权限，从而报错：\nMar 20 07:08:47 C20240928005482 xray[387022]: Failed to start: main: failed to create server \u0026gt; app/log: failed to initialize access logger \u0026gt; open /usr/xray/xray_log/access.log: permission denied 我们这样给一下权限就行：\n# 未给权限时的目录 #drwxr-xr-x 2 root root 4096 Mar 20 07:11 xray_log chown -R nobody:nogroup /usr/xray/xray_log/ # 给了权限后的目录 #drwxr-xr-x 2 nobody nogroup 4096 Mar 20 07:11 xray_log 之后重启xray systemctl restart xray ，目录会生成log文件，但是依旧会报错，因为log没有写入权限。\n我们给一下 log文件 一下755权限：\n# 未给权限时的文件 #-rw------- 1 nobody nogroup 0 Mar 20 07:12 access.log #-rw------- 1 nobody nogroup 0 Mar 20 07:12 error.log chmod -R 755 /usr/xray/xray_log/ # 给了权限后的文件 #-rwxr-xr-x 1 nobody nogroup 0 Mar 20 07:12 access.log #-rwxr-xr-x 1 nobody nogroup 0 Mar 20 07:12 error.log Nginx 443端口占用问题 xray.service: Start request repeated too quickly.\n报错xray.service: Start request repeated too quickly.：\nMar 20 07:13:00 C20240928005482 systemd[1]: xray.service: Main process exited, code=exited, status=255/EXCEPTION Mar 20 07:13:00 C20240928005482 systemd[1]: xray.service: Failed with result \u0026#39;exit-code\u0026#39;. Mar 20 07:13:01 C20240928005482 systemd[1]: xray.service: Scheduled restart job, restart counter is at 5. Mar 20 07:13:01 C20240928005482 systemd[1]: Stopped Xray Service. Mar 20 07:13:01 C20240928005482 systemd[1]: xray.service: Start request repeated too quickly. Mar 20 07:13:01 C20240928005482 systemd[1]: xray.service: Failed with result \u0026#39;exit-code\u0026#39;. Mar 20 07:13:01 C20240928005482 systemd[1]: Failed to start Xray Service. 上面没有特别有用的信息，所以我们用 journalctl -u xray 命令看一下怎么样了，可以看到一条：\nFailed to start: app/proxyman/inbound: failed to listen TCP on 443 \u0026gt; transport/internet: failed to listen on address: 0.0.0.0:443 \u0026gt; transport/internet/tcp: failed to listen TCP on 0.0.0.0:443 \u0026gt; listen tcp 0.0.0.0:443: bind: address already in use 明显是443端口占用问题。明显是Nginx刚刚配置文件顶号了，Nginx配置文件顶号了，自行修改Xray配置文件，或者Nginx配置。因为服务器配置好，不是只是节点服务器，服务器这里有一个Web实例正在运行，所以不好改Nginx配置文件，我只好更改Xray配置文件。\n在配置文件39行处，反正就是全文，有且只有一个443，就在39行(我的是)。 所以我需要更改这个443，防止其和Nginx冲突，但是只是因为我艺高人胆大(目移)，如果害怕服务器被封锁，我还是建议更改Nginx配置文件吧(嗯)。\n改完后重启xray，之后把下面客户端配置内的端口换成你改完的端口就行。比如说我把Xray配置文件的443改成了9901，那我就把客户端输入的443换成了9901。没啥问题，正常可用。\n记得重新启动xray\nsystemctl restart xray 节点有延迟，但不正常工作 – (包括发现网页打不开，没有发送任何数据) 如果网页正常打开，你看看是不是客户端 ShortId 没填对，填错的话就会出现无法连接节点的问题，但是节点会有延迟。\n如果是网页也打不开的话：检查 nginx 网页配置文件里面的端口配置，比如说上文提到的 8909，如果你的 nginx 配置端口和 xray 配置文件的回落端口不一致(上文是8909)，就会导致节点有延迟，但是打不开网页的问题。当然，如果你不单单配置错端口，连域名也搞错了，那就绝对不可能打开网页了。\n因为原理是：Xray 监听 443 端口，将合法的伪装流量（节点流量）直接转发，而将所有非合法的伪装流量（普通浏览器访问）通过 fallbacks 规则转发给 Nginx 来提供正常的网页服务。如果 Nginx 配置(无法和xray配置文件端口对上)或运行有问题，网页就会无法显示(没有发送任何数据)。\n正常运行(成功启动 Xray) 下面是正常运行的状态：\n● xray.service - Xray Service Loaded: loaded (/etc/systemd/system/xray.service; enabled; vendor preset: enabled) Drop-In: /etc/systemd/system/xray.service.d └─10-donot_touch_single_conf.conf Active: active (running) since Thu 2025-03-20 05:59:09 UTC; 52min ago Docs: https://github.com/xtls Main PID: 385334 (xray) Tasks: 6 (limit: 2339) Memory: 3.8M CGroup: /system.slice/xray.service └─385334 /usr/local/bin/xray run -config /usr/local/etc/xray/config.json Mar 20 05:59:09 C20240928005482 systemd[1]: Started Xray Service. Mar 20 05:59:09 C20240928005482 xray[385334]: Xray 25.3.6 (Xray, Penetrates Everything.) 2cba2c4 (go1.24.1 linux/amd64) Mar 20 05:59:09 C20240928005482 xray[385334]: A unified platform for anti-censorship. Mar 20 05:59:09 C20240928005482 xray[385334]: 2025/03/20 05:59:09.657653 [Info] infra/conf/serial: Reading config: \u0026amp;{Name:/usr/local/etc/xray/config.json Format:json} Mar 20 05:59:09 C20240928005482 xray[385334]: 2025/03/20 05:59:09.703632 [Warning] core: Xray 25.3.6 started 客户端连接配置 如果上面更改了443端口(Nginx顶号了)，这里的443按照你Xray配置更改的端口，写上去就好。\n(Windows、安卓)V2rayN配置:\n(Mac，iOS)Shadowrocket配置:\n节点测试 我测试过，即便是晚高峰期间，4k视频的缓冲依旧有10s左右，每一次点击，缓冲只需要1.2s左右即可完成。不过，具体还是要看你的服务器的。在服务器稳定运行半年后，才有了这个教程。直至目前，依旧没有发现服务器被任何封锁。\n参考了\n(Project X 小小白话文) – https://xtls.github.io/document/level-0/\n(Project X 回落功能) – https://xtls.github.io/document/level-1/fallbacks-lv1.html\n(阿良的幻想乡) – https://aliang-zh.top/2023/11/vless-vision-utls-reality/\n","permalink":"https://blog.dontalk.org/posts/xray-%E6%90%AD%E5%BB%BA%E4%B8%80%E4%B8%AAvless-vision-realitynginx%E4%BC%AA%E8%A3%85%E7%9A%84%E8%8A%82%E7%82%B9/","summary":"因为工作与学习的缘故，总是需要浏览全球文献，但是由于我有时的地理位置并不支持我自由地穿梭在全球互联网上。而且我的服务器总是容易被封锁端口，于是我就参考官方和第三方文档搭建了一个服务器，并且稳定半年使用都没有问题。\n于是，我决定打开一个教程，用于自己的温习和记录。\n前置条件 首先我们需要【一台】线路不错的【服务器】，起码在受限制的地区连接不会很慢。(延迟并不能 …","title":"Xray 搭建一个VLESS-Vision-Reality(Nginx伪装)的节点"},{"content":"前言 我打算在我的服务器上面托管一些代码，并且需要可视化桌面。最主要的是，不会像VNC一样，总是会终止我的代码，或者出现一下其他的问题。所以我选择了RDP协议的远程桌面协议。\n和VNC不同，rdp的账号密码和系统用户相同，如root。\n本文在事实和创作方面都参考了：pickstar.today的文章\n首先，我创建了一个新的虚拟机，并且安装好系统。之后我更新源:\napt update -y \u0026amp;\u0026amp; apt upgrade -y 安装桌面系统 选择一个桌面进行安装，我选择的是xfce:\napt install task-xfce-desktop # XFCE apt install task-gnome-desktop # GNOME apt install kde-plasma-desktop # KDE 如果是 Debian 的话，同样安装一个就可以了，不过 debian11 无法使用 xrdp 远程 Gnome 桌面，尽管下面列出了且可以安装，但用不了，所以请不要选择\n之后设置启动时默认启动图形化桌面环境:\nsystemctl set-default graphical.target 安装 rdp 服务(远程控制) apt install xrdp 设置 xrdp 开机启动\u0026amp;立即启动 xrdp\nsystemctl enable --now xrdp 默认情况下，Xrdp 使用 /etc/ssl/private/ssl-cert-snakeoil.key, 它仅仅对 “ssl-cert” 用户组成语可读。运行下面的命令，将 xrdp 用户添加到这个用户组：\nadduser xrdp ssl-cert 我出现了找不到 adduser 命令的问题，但是我并没有理睬，确定服务器没有防火墙后，我继续连接，没有发现问题。可以直接连接使用。如果有问题，请搜索，这里不做赘述。\n至此，xrdp搭建已经完成！如果有公网IP，那么此时，服务就是暴露在3389端口内的！(并非指如果没有公网IP，就不是3389。而是这个端口在公网很危险)所以我建议你修改端口，或者设置防火墙规则。不过说到防火墙，你如果有并且没有放行3389，那么你的访问也会是不通过的。(会被拒绝)。如果你使用穿透或者其他，请留意3389是否被服务提供商封锁禁用。\n# ufw sudo ufw allow 3389 # nft sudo nft add rule inet filter input tcp dport 3389 ct state new,established counter accept # iptables iptables -I INPUT -p tcp --dport 3389 -j ACCEPT # firewalld firewall-cmd --zone=public --add-port=3389/tcp --permanent 连接服务器 信任证书(出现这个证明没问题，长时间连接请查看是不是服务器防火墙问题)。\n使用服务器系统账户即可登陆，如root。\nMacOS下需要安装WindowsApp才可以连接RDP，MacOS原生支持VNC。\n我测试了XFCE、GNOME、KDE桌面都没发现问题，下面追加的截图来自KDE桌面。\n追加的图片:\n开启声音转发（可选） 此处全段来自pickstar.today的文章\n虽然现在桌面环境和远程服务已经配置好了，但是如果直接使用的话，远程桌面的声音是无法播放的，如果想要正常播放声音，就需要安装声音转发模块。xrdp 服务中并未包含此模块，需要手动编译安装。\n首先安装依赖\nsudo apt install git build-essential autoconf libtool automake pkg-config libssl-dev libpam0g-dev libx11-dev libxfixes-dev libxrandr-dev libpulse-dev -y 拉取源代码\ncd ~ git clone https://github.com/neutrinolabs/pulseaudio-module-xrdp.git cd pulseaudio-module-xrdp 准备安装环境\nscripts/install_pulseaudio_sources_apt_wrapper.sh 这个脚本需要执行的时间比较长，请耐心等待，执行完后能看到\n- Creating focal build root. Log file in /var/tmp/pa-build-testuser-debootstrap.log - Creating schroot config file /etc/schroot/chroot.d/pa-build-testuser.conf [sudo] password for testuser: - Copying /etc/apt/sources.list to the root - Creating the build directory /build - Copying the wrapped script to the build directory - Building PA sources. Log file in /var/tmp/pa-build-testuser-schroot.log - Copying sources out of the build root - Removing schroot config file and build root - All done. Configure PA xrdp module with PULSE_DIR=/home/testuser/pulseaudio.src 注意最后一行的输出，复制下 PULSE_DIR=/home/testuser/pulseaudio.src，粘贴在 configure 后，例如\n./bootstrap \u0026amp;\u0026amp; ./configure PULSE_DIR=/home/testuser/pulseaudio.src 编译并安装\nmake sudo make install 在远程桌面看，你会发现音频输出设备变成了 xrdp sink。\n如此以来，就可以将远程桌面的声音在本地播放了。\n安装中文支持（可选） 此处全段来自pickstar.today的文章\n安装中文字体\nsudo apt install fonts-arphic-ukai fonts-arphic-uming fonts-ipafont-mincho fonts-ipafont-gothic fonts-unfonts-core -y 然后只需执行\nsudo dpkg-reconfigure locales 会看到\n向下找到 zh_CN.UTF-8，按空格选中后回车\n系统默认语言也选中 zh_CN.UTF-8 即可\n如果有的桌面环境中没有安装浏览器的话，可以这样一键安装 firefox\nUbuntu：\nsudo apt install firefox firefox-locale-zh-hans -y Debian：\nsudo apt install firefox-esr -y 疑难杂症 1\u0026gt; Linux的RDP账号密码正确，登陆成功后闪退了\n我在安装完毕桌面后，我发现使用WindowsApp(Mac)RDP协议连接的时候，会在登入后就闪退。\n比如说我们想要登陆root这个账户，我们就在root的账户下执行下面的命令，用来选择默认的桌面即可\n# gnome桌面 echo gnome-session \u0026gt; ~/.xsession # xfce桌面 echo xfce4-session \u0026gt;~/.xsession # kde桌面 echo \u0026#34;/usr/bin/startplasma-x11\u0026#34; \u0026gt; ~/.xsession 2\u0026gt; something has gone wrong. A problem has occurred and the system can’t recover.\n如果是 Debian11 且选择 Gnome 桌面看到这个错误，请放弃并选择其他桌面\n一般未选择桌面版本或者选择错误了，就会报这个错误。\nls -l /etc/alternatives/x-session-manager update-alternatives --config x-session-manager 输入命令后，通过数字选择你安装好的桌面即可\n","permalink":"https://blog.dontalk.org/posts/%E5%A6%82%E4%BD%95%E7%BB%99%E6%97%A0%E5%8F%AF%E8%A7%86%E5%8C%96%E7%9A%84linux%E7%B3%BB%E7%BB%9F%E5%AE%89%E8%A3%85%E6%A1%8C%E9%9D%A2%E7%8E%AF%E5%A2%83rdp/","summary":"前言 我打算在我的服务器上面托管一些代码，并且需要可视化桌面。最主要的是，不会像VNC一样，总是会终止我的代码，或者出现一下其他的问题。所以我选择了RDP协议的远程桌面协议。\n和VNC不同，rdp的账号密码和系统用户相同，如root。\n本文在事实和创作方面都参考了：pickstar.today的文章\n首先，我创建了一个新的虚拟机，并且安装好系统。之后我更新源 …","title":"如何给无可视化的Linux系统安装桌面环境？(RDP)"},{"content":"前言 早些时候，我在家里部署了PVE，并通过FRP穿透到了公网去，但是当我需要访问家庭路由器时，却得需要一个桌面环境，我才能很好地在浏览器控制我家中设备，而非穿透整个局域网。(Debian12)\n目前，最新的tigerVNC变得奇奇怪怪的，下面文章已经过审查与修订。使得VNC可以使用。或许是我的技术问题，我实在没有在它们官方文档找到有用信息。\n如果你讨厌新版本的各种问题，你可以试试旧版，网络上很多教程，或者在文章结尾处的引用。但是出于安全考虑，这里仅对新版本作介绍。一切源于我发现我自己写的博文不再可用。不过经过这次折腾，我已经对tigerVNC失去耐心，在今天修改过后，该博文在2025年3月23日永久归档！\n或者..如非VNC，我建议你可以试试rdp，一般只需要5条命令，3分钟左右就可以完成。\n如何给无可视化的Linux系统安装桌面环境？(RDP)\n安装TigerVNC的服务端 我们更新一下源，之后再安装一下TigerVNC的服务端。请在root账户下 su root\napt update apt install tigervnc-standalone-server tigervnc-common 一般来说，服务器并不自带桌面，如果是服务器或者没有桌面环境，可以通过下面的命令来安装桌面环境。安装会需要点时间。\n# GNOME # apt install task-gnome-desktop # 上面的GNOME桌面测试不通过，没有太多时间测试，在修订中被删除。请使用下面的xfce4，而不是GNOME。 # XFCE apt install xfce4 xfce4-goodies 配置VNC服务器 1.创建或编辑 VNC 的配置文件：\nmkdir /root/.vnc vim ~/.vnc/xstartup ## 记得在root下。就是/root/.vnc XFCE桌面配置：\n#!/bin/bash xrdb $HOME/.Xresources startxfce4 该区块只是错误示范：\n#########################################################\n下面GNOME随着修订被遗弃，还有一个问题，来自于【\u0026amp;】，因为新版tigerVNC希望先在前台运行一下，这里的【\u0026amp;】会报错。这个修改很抽象。\n#!/bin/bash # 错误示范，请勿照抄 xrdb $HOME/.Xresources # 错误示范，请勿照抄 gnome-session \u0026amp; # 错误示范，请勿照抄 #########################################################\n2.给予 xstartup 文件可执行权限：\nchmod +x ~/.vnc/xstartup 3.第一次启动VNC服务|设定密码：\nvncserver 在第一次启动时，会向你取要密码，如下\nvncserver #(tigervncserver) You will require a password to access your desktops. Password: # 输入第一次密码，6-8位 Verify: # 验证第一次输入的密码，6-8位 Would you like to enter a view-only password (y/n)? n # 是否创建一个只查看的用户(只能看)，n就行。 如若上面不是你想要的设定，或者你想重新设置一个只能查看的用户。只需要执行:\nvncpasswd 启动VNC服务 一般启动会报错，总之就是各种问题。。\n你可以通过 ps -ef | grep tigervnc 查看启动的vnc进程，并kill它。\n(请记住，无论如何，只要使用了grep，就总是会查找到一个进程，这个进程会一直变化，因为这个进程是grep，和你查找的内容无关)。\n在你安装完桌面环境后，以及配置好密码，也就是到了这一步了，请先重启服务器\n在本地服务器只有tty的情况下，你会发现重启进入了一个桌面环境。\n完成重启后，请不要直接使用 vncserver 或者 tigervncserver 命令启动vnc服务器，这样启动即便成功了，你也连接不上。。。\n使用下面命令启动:\nvncserver :1 -localhost no # 这里的:1指的是默认端口5901，:1代表1。:2则是5902。此处可以省略。 # 但是-localhost no不能省略，省略后无法连接 如果你只想vnc一个终端:\nvncserver -xstartup /usr/bin/xterm :1 -localhost no 连接： 学会用 ps -ef | grep tigervnc。这个命令还能看到vncserver启动参数。还有，你记得安装好桌面以及设置好密码后就重启服务器吗？\n1.确保vncserver在运行。\n2.确保没有防火墙阻拦(或者说放行了端口)\n3.使用了 -localhost no 参数\n有什么问题可以通过 ps -ef | grep tigervnc 查看启动的vnc进程，看它的参数，或kill它。\n如今，你应该就可以通过VNC连接软件进行连接了。密码源自于你设置的密码，而不是服务器root密码。\n系统服务/守护进程 不再介绍系统服务设置(修订中删除)\n疑难杂症 1.默认端口是5901，:1代表1。:2则是5902。此处可以省略。\n2.-localhost no不能省略，省略后无法连接，这个指的可不是本地访问，而是决定其他计算机是否可以访问的。\n3.当你 xstartup 文件不存在时，就得自己指定一个。如：vncserver -xstartup /usr/bin/xterm :1 -localhost no 就会vnc一个终端出去。\n4.如果出现各种问题，如黑屏之类的，请查看是否在虚拟环境或者其他模式下启动。确保在真实的系统环境下启动。\n5.有防火墙吗。你确定防火墙放行了吗？\n本文有参考：\nunix.stackexchange – 785500 疑难杂症【\u0026amp;】\nUbuntu 20.04 安装多用户VNC(基于gnome) 其介绍了旧版的tigerVNC如何设置使用，以及一些新版的疑难杂症。\n","permalink":"https://blog.dontalk.org/posts/%E6%9C%80%E6%96%B0%E4%BF%AE%E8%AE%A2%E5%A6%82%E4%BD%95%E7%BB%99linux%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%AE%89%E8%A3%85vnc%E5%B9%B6%E8%BF%9C%E7%A8%8B%E8%AE%BF%E9%97%AEx/tigervnc-%E4%B8%8E%E7%96%91%E9%9A%BE%E6%9D%82%E7%97%87/","summary":"前言 早些时候，我在家里部署了PVE，并通过FRP穿透到了公网去，但是当我需要访问家庭路由器时，却得需要一个桌面环境，我才能很好地在浏览器控制我家中设备，而非穿透整个局域网。(Debian12)\n目前，最新的tigerVNC变得奇奇怪怪的，下面文章已经过审查与修订。使得VNC可以使用。或许是我的技术问题，我实在没有在它们官方文档找到有用信息。\n如果你讨厌新版 …","title":"【最新修订】如何给Linux服务器安装VNC并远程访问？(X/tigerVNC) 与疑难杂症"},{"content":"说明 学习MySQL需要，所以做了一些笔记。\n从很久之前就使用MySQL了，如今做了一些笔记，加强一下曾经碎片化的技术记忆。\n下载/查看(PDF文件) MySQL笔记.pdf\n","permalink":"https://blog.dontalk.org/posts/mysql%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0pdf/","summary":"说明 学习MySQL需要，所以做了一些笔记。\n从很久之前就使用MySQL了，如今做了一些笔记，加强一下曾经碎片化的技术记忆。\n下载/查看(PDF文件) MySQL笔记.pdf\n","title":"MySQL学习笔记(PDF)"},{"content":"说明 比赛需要，我做了一份CTF的复习资料汇总。\n笔记就不公布了，写得有点乱。\n目前已经获得23年省赛，蓝桥杯三等奖。。可惜，和二等奖两次擦肩而过。\n下载/查看(PDF文件) CTF – 比赛复习概要汇总.pdf\n","permalink":"https://blog.dontalk.org/posts/ctf%E6%AF%94%E8%B5%9B%E5%A4%8D%E4%B9%A0%E6%A6%82%E8%A6%81%E6%B1%87%E6%80%BBpdf/","summary":"说明 比赛需要，我做了一份CTF的复习资料汇总。\n笔记就不公布了，写得有点乱。\n目前已经获得23年省赛，蓝桥杯三等奖。。可惜，和二等奖两次擦肩而过。\n下载/查看(PDF文件) CTF – 比赛复习概要汇总.pdf\n","title":"CTF比赛复习概要汇总(PDF)"},{"content":"最后更新 更新至第三版 – 2024年初。\n做了一些细微的调整与修改。\n说明 依旧如此，帮助同学朋友快速上手，入门APT(Debian)系列Linux。\n下载/查看(PDF文件) GitHub风格 – Linux入门_Github.pdf\nNight风格 – Linux入门_Night.pdf\n纪念 *纪念你，我死去的朋友…们\n","permalink":"https://blog.dontalk.org/posts/linux%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8Bpdf/","summary":"最后更新 更新至第三版 – 2024年初。\n做了一些细微的调整与修改。\n说明 依旧如此，帮助同学朋友快速上手，入门APT(Debian)系列Linux。\n下载/查看(PDF文件) GitHub风格 – Linux入门_Github.pdf\nNight风格 – Linux入门_Night.pdf\n纪念 *纪念你，我死去的朋友…们\n","title":"Linux入门教程(PDF)"},{"content":"最后更新 更新至第三版 – 2024年初。\n做了一些细微的调整与修改。\n说明： 这些年来发生了一些事，几乎都在伤春悲秋，就再也懒得撰写技术博客。\n包括不限于别离，死亡，病疾。我想你未必想知道太多，而且我也不想说太多。希望你能安好。\n这段时间做了挺多事，做了一个游戏，两个博客，还有论坛等等。一直受雇在写一些无关紧要的代码。\n好吧，我想寒暄应该就此为止。为了照顾一个因病长时间没来上学的同学（学校要求）。\n所以就有了这一版教程，希望可以帮助到更多人，学习和入门C语言。文章有不足的地方，请多多包涵。\n下载/查看(PDF文件) GitHub风格 – C语言入门_Github.pdf\nNight风格 – C语言入门_Night.pdf\n纪念 *纪念你，我死去的朋友…们\n","permalink":"https://blog.dontalk.org/posts/c%E8%AF%AD%E8%A8%80%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8Bpdf/","summary":"最后更新 更新至第三版 – 2024年初。\n做了一些细微的调整与修改。\n说明： 这些年来发生了一些事，几乎都在伤春悲秋，就再也懒得撰写技术博客。\n包括不限于别离，死亡，病疾。我想你未必想知道太多，而且我也不想说太多。希望你能安好。\n这段时间做了挺多事，做了一个游戏，两个博客，还有论坛等等。一直受雇在写一些无关紧要的代码。\n好吧，我想寒暄应该就此为止。为了照顾 …","title":"C语言入门教程(PDF)"},{"content":"之前的方法有了小小变化，因为现在(2024)的 Maddy 已经没了 maddyctl 这个文件。增加用户等命令已经集成到了 Maddy 一个文件中了 更多问题请看文章末尾疑难杂症\n什么是Maddy？为何是Maddy 文章受到lala启发：lala.im\nMaddy是一款用Go语言开发的邮件服务器，它实现了运行电子邮件服务器所需的所有功能。\nMaddy用一个具有统一配置和最低维护成本的守护进程取代了Postfix、Dovecot、OpenDKIM、OpenSPF、OpenDMARC 等程序。\n因为Maddy简单的配置，低配置占用，所有东西都集成一体(ALL IN ONE)。所以我们选择使用Maddy来充当我们的邮件服务器。\n前置条件： 服务器一台（最好支持rDNS，不然减分严重。一般发工单给服务器提供商设置）\n请留意端口是否被封锁，要说993/465/143/587都没有被占用封锁，就要留意25端口了。\n如果 25 端口无法使用，确实会影响邮件的接收，因为 SMTP 协议的标准要求邮件服务器之间的通信必须通过 25 端口。换端口只能用于 邮件发送 或 用户端（客户端）收取邮件，但并不能解决从其他邮件服务器接收邮件的问题。\n查看占用命令(二选一)：\nlsof -i:端口号 ############# netstat -tulnp | grep 端口号 干净域名一个，似乎.com/.org更好。\n正文 开始（DNS解析） 我使用CloudFlare来解析DNS，我们添加几个解析指向我们的Maddy服务器。\n添加一条 A记录 指向服务器IP\n类型A 名称mx1 IPv4 关闭代理 TTL自动 添加一条 MX记录 指向上面添加的A记录\n类型MX 名称@ mx1.dontalk.org TTL自动 优先级0 添加一条 TXT记录 记录_dmarc\n类型TXT 名称_dmarc 记录值v=DMARC1; p=none; rua=mailto:admin@dontalk.org 记录值的最后面，admin@dontalk.org，决定了我的用户用户名为admin。\n再添加一个 TXT记录 记录spf1\n类型TXT 名称@ 记录值v=spf1 mx ip4:xxx.xxx.xxx.xxx(服务器的IP(v4)地址)/32 -all 记录值里面的ip4需要更改外上面第一条mx1-A记录的服务器IP地址\n之后（安装Linux软件） 开始前检查一下机器的25/465/587/993/143端口有没有被占用以及这些端口有没有被服务器商家屏蔽。\n如果被屏蔽，不起作用(IP绿，993/143绿，但是465/587怎么都是红，这样类似的问题)可以通过修改 /etc/maddy/maddy.conf 文件内的配置默认端口解决。\n之后安装需要用到的包与下载解压Maddy本体。\nMaddy的项目地址是：Github – https://github.com/foxcpp/maddy\napt -y update apt -y install acl zstd wget certbot wget https://github.com/foxcpp/maddy/releases/download/v0.7.1/maddy-0.7.1-x86_64-linux-musl.tar.zst unzstd maddy-0.7.1-x86_64-linux-musl.tar.zst tar -xvf maddy-0.7.1-x86_64-linux-musl.tar cd maddy-0.7.1-x86_64-linux-musl/ 之后创建需要用到的目录，复制相应的文件到对应的目录：\nmkdir /etc/maddy cp maddy.conf /etc/maddy cp maddy /usr/local/bin/ cp systemd/*.service /etc/systemd/system maddy不能直接运行在root用户下，务必创建一个单独的，/sbin/nologin 的，不可登录系统的用户：\nuseradd -mrU -s /sbin/nologin -d /var/lib/maddy -c \u0026#34;maddy mail server\u0026#34; maddy 之后 申请证书 没有Nginx/Apache等任何应用占用80端口情况下 现在使用certbot申请一个SSL证书。\n如果服务器上没有 Nginx/Apache（没有任何程序占用80端口）可以直接用certbot内置的web服务器来申请ssl证书：\ncertbot certonly --standalone --agree-tos --no-eff-email --email xxxxx@example.com -d mx1.dontalk.org 如果服务器上有 Nginx 正在运行，那么可以再装一个 certbot的Nginx插件： apt -y install python-certbot-nginx ## 如若失败，请使用下面命令 apt -y install python3-certbot-nginx 然后新建一个Nginx站点配置文件：\nvim /etc/nginx/conf.d/maddy.conf 写入一个最小的Nginx站点配置：\nserver { listen 80; server_name mx1.dontalk.org; } 然后使用 certbot 的 Nginx插件 来申请SSL证书：\ncertbot --nginx --agree-tos --no-eff-email --email xxxxx@example.com ACME.sh 脚本 这里不作解释，详见官网：Maddy\nmaddy是没有web客户端的，考虑到有人可能会自己搭建web客户端，比如rainloop这类，所以这里提供了nginx申请证书的方法。\n实际上可以直接用本地的客户端，比如thunderbird、foxmail这些。\n现在需要配置acl，让maddy这个用户（上面创建的不可登录用户）有权限读取证书：\nsetfacl -R -m u:maddy:rx /etc/letsencrypt/{live,archive} ## 如若报错无法找到命令：setfacl。那么请先安装： apt install acl 接下来编辑maddy的配置文件：\nvim /etc/maddy/maddy.conf 修改下面列出的配置：\n$(hostname) = mx1.dontalk.org $(primary_domain) = dontalk.org $(local_domains) = $(primary_domain) tls file /etc/letsencrypt/live/mx1.dontalk.org/fullchain.pem /etc/letsencrypt/live/mx1.dontalk.org/privkey.pem ## 注意上面这行配置和conf文件中的配置不同，conf配置文件的证书目录在： ## tls file /etc/maddy/certs/$(hostname)/fullchain.pem /etc/maddy/certs/$(hostname)/privkey.pem ## 所以需要更改这一行，否则启动时会提示start request repeated too quickly错误 Maddy的启动与收尾 启动maddy以及设置开机自启：\nsystemctl enable --now maddy.service ## 如若提示 start request repeated too quickly 错误 ## 请保证conf的证书目录是否填写正确，已经成功申请到证书到证书目录。是否配置了ACL使得Maddy可以访问证书。 查看运行状态，确保正常：\nsystemctl status maddy.service Maddy在第一次启动的时候会生成一个DKIM密钥，所以现在来补充配置一下DKIM的dns解析。\n查看下面的文件（域名换成你自己的）获取DKIM密钥：\ncat /var/lib/maddy/dkim_keys/dontalk.org_default.dns 内容类似于：\nv=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0......（不换行，很长，别把主机名也复制了） 类型TXT 名称default._domainkey v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0...... TTL自动 如今，Maddy已经部署好了。 现在可以尝试 添加用户 了 创建一个用户账号，这里的用户名必须是电子邮件的地址\nmaddy creds create admin@dontalk.org ## 创建用户 ## 移除账户 maddy creds remove admin@dontalk.org 注意用户名是电子邮件地址。这是必需的，因为用户名用于授权 IMAP 和 SMTP 访问\n注册用户凭据后，还需要创建本地存储帐户：\nmaddy imap-acct create admin@dontalk.org ## 创建用户 ## 移除账户 maddy imap-acct remove admin@dontalk.org 最后，登陆与发信 最后使用本地邮件客户端 Thunderbird 来测试一下邮件服务器。\n一直连接中 如果你的服务正常在运行，但是连接不上服务器，就是不报密码错误，就是一直连不上，一直在验证。那大概率是防火墙或者服务器提供商把你25/993/465/143/587端口ban了。除了前者可以放行端口外，后者就只能改端口号了。(狠一点的话会按照协议ban，只能发工单)\n配置文件在：/etc/maddy/maddy.conf\n在83行前后：\n在175行前后：\n更改后，保存并重启maddy就可以生效了。\n正常连接 如果使用993/465端口\n如果使用143/587端口\n登陆成功后，让我们测试一下邮件得分。\n完美，十分。\npyzor扣分: PYZOR_CHECK -1.985 出现类似这样的扣分等，大概是你发送的邮件内容太少/单调导致的。\n无伤大雅。只要主要的解析记录做好，rDNS做好，基本没有什么问题。\n疑难杂症 只能发信，无法收信 请留意看文章开头的前置条件\n如果 25 端口无法使用，确实会影响邮件的接收，因为 SMTP 协议的标准要求邮件服务器之间的通信必须通过 25 端口。换端口只能用于 邮件发送 或 用户端（客户端）收取邮件，但并不能解决从其他邮件服务器接收邮件的问题。\n查看占用命令(二选一)：\nlsof -i:端口号 ############# netstat -tulnp | grep 端口号 注意！rDNS与Gmail退信。 如果没有设置rDNS可能会扣分高达1.7（一般服务器设置需要发起工单，要求设置）。而且！\n而且会被Gmail退信。如果你设置了rDNS也被Gmail退信了，很有可能是IPv6设置rDNS。我直接禁用了服务器的IPv6就解决问题了。\n过了几个月忽然发现已经无法登入使用了，根本毫无反应(无法找到有效的服务器配置) 我们通过 systemctl status maddy 命令大概会看到下面这样的内容\nApr 13 15:45:24 Mail-Server maddy[9168]: submission: handler error: remote error: tls: expired certificate Apr 13 15:45:27 Mail-Server maddy[9168]: submission: handler error: EOF Apr 13 15:45:39 Mail-Server maddy[9168]: submission: handler error: EOF handler error: remote error: tls: expired certificate 问题已经显而易见了，就是证书过期了。\n我们使用下面命令更新证书：\ncertbot renew 如果显示端口被占用的话，使用下面命令查看什么被占用，之后暂停程序，更新完证书再启动就好了。\n查看占用命令(二选一)：\nlsof -i:端口号 ############# netstat -tulnp | grep 端口号 之后 大概会接着报一个无法读取证书的错误 ，重启Maddy会发现是证书的权限问题，我们再次执行这个命令给予一下权限就可以成功正常启动了\nsetfacl -R -m u:maddy:rx /etc/letsencrypt/{live,archive} ## 如若报错无法找到命令：setfacl。那么请先安装： apt install acl 这时，类似Thunderbird这样的软件，就可以再次成功找到服务器配置了。\n添加 IPv6 支持(AAAA解析记录) 这里不作解释，因为我的服务器IPv6很多，懒得一个个添加/禁用。所以具体设置详见官网解析: Maddy\n禁用 IPv6 通过 sysctl 禁用 IPv6: vim /etc/sysctl.conf 添加以下行到文件底部，以禁用 IPv6：\nnet.ipv6.conf.all.disable_ipv6 = 1 net.ipv6.conf.default.disable_ipv6 = 1 net.ipv6.conf.lo.disable_ipv6 = 1 使更改生效：\nsysctl -p 通过 GRUB 禁用 IPv6（如果需要）: 编辑 GRUB 配置文件：\nvim /etc/default/grub 找到以 GRUB_CMDLINE_LINUX 开头的行，并添加 ipv6.disable=1。例如：\nGRUB_CMDLINE_LINUX=\u0026#34;quiet splash ipv6.disable=1\u0026#34; 更新 GRUB：\nupdate-grub 重启服务器：\nreboot 验证 IPv6 是否已禁用: 检查 IPv6 状态：\nip a ## 找到不到IP命令就用下面的命令 ifconfig 看看还有没有以 inet6 开头的地址。\n或者\nip a | grep inet6 ## 找到不到IP命令就用下面的命令 ifconfig | grep inet6 看看有没有IPv6地址返回。\n","permalink":"https://blog.dontalk.org/posts/%E6%90%AD%E5%BB%BA%E7%A7%81%E6%9C%89e-mail%E5%B9%B3%E5%8F%B0maddyall-in-one-2025%E5%B9%B44%E6%9C%88%E6%9B%B4%E6%96%B0/","summary":"之前的方法有了小小变化，因为现在(2024)的 Maddy 已经没了 maddyctl 这个文件。增加用户等命令已经集成到了 Maddy 一个文件中了 更多问题请看文章末尾疑难杂症\n什么是Maddy？为何是Maddy 文章受到lala启发：lala.im\nMaddy是一款用Go语言开发的邮件服务器，它实现了运行电子邮件服务器所需的所有功能。\nMaddy用一个 …","title":"搭建私有E-Mail平台：Maddy(ALL in ONE) – 2025年4月更新"},{"content":"啊。如今的绅♂️士啊。是何等孱弱的存在。你们有脸面对神圣的幻想乡吗？ 什么是车牌号？ 或许我们在日常群聊的时候，经常会有人发一些类似 “ABCD-1234” 的英文数字短文出来，有时候乍一看，还有点像车的车牌号。\n其实这些就是 “车牌号” 。在日本AV发行出来的时候，一般都会附带一个编号在电影标题开头，起着区分在哪个系列，是哪个社团制作等作用。\n而这个标识码，是唯一的。每一个由日本正品发出（起码是大平台）标题都会保留这串 “车牌号” 。于是，通过Google等搜索引擎，就可以搜索到该电影。\n可以理解成一串每个人都可以轻易书写，打出来的一串数字代码。—— 电影的标题浓缩。\n怎么通过车牌号找到电影/下载电影？ 上面已经演示了，直接通过Google就能搜出来。可是，没法下载啊！\n为什么大家拿到车牌号就能下载呢？（Google搜索：磁力搜索器）\n因为有些网站提供通过车牌号搜索磁力链接，那我们有磁力链接，当然就可以直接下载啦。\n此时，我们就已经得到了磁力链接。现在就能复制进磁力下载器，愉快地下载啦～\n","permalink":"https://blog.dontalk.org/posts/%E7%BB%85%EF%B8%8F%E5%A3%AB%E7%9A%84%E8%87%AA%E6%88%91%E4%BF%AE%E5%85%BB%E4%BB%80%E4%B9%88%E6%98%AF%E8%BD%A6%E7%89%8C%E5%8F%B7%E6%80%8E%E4%B9%88%E9%9D%A0%E8%BD%A6%E7%89%8C%E5%8F%B7%E6%89%BE%E5%88%B0%E6%88%96%E4%B8%8B%E8%BD%BD%E6%9F%90%E4%B8%80%E9%83%A8%E5%8A%A8%E4%BD%9C%E7%88%B1%E6%83%85%E5%A4%A7%E7%94%B5%E5%BD%B1/","summary":"啊。如今的绅♂️士啊。是何等孱弱的存在。你们有脸面对神圣的幻想乡吗？ 什么是车牌号？ 或许我们在日常群聊的时候，经常会有人发一些类似 “ABCD-1234” 的英文数字短文出来，有时候乍一看，还有点像车的车牌号。\n其实这些就是 “车牌号” 。在日本AV发行出来的时候，一般都会附带一个编号在电影标题开头，起着区分在哪个系列，是哪个社团制作等作用。\n而这个标识码 …","title":"【绅♂️士的自我修养】什么是车牌号？怎么靠车牌号找到或下载某一部动作爱情大电影？"},{"content":"啊。如今的绅♂️士啊。是何等孱弱的存在。你们有脸面对神圣的幻想乡吗？ 如若只想知道如何下载，请滑动到最后。已提供各操作系统的软件下载\u0026amp;使用教程。\n我们要下载磁力或种子文件，就得先搞明白什么是磁力，种子，BT，PT。 首先，我们需要知道，到底什么是磁力链接，什么是种子。如果连它是什么都不知道，谈何下载呢？…你连看片都不配！(语怒)\n我们的世界似乎已经被 某度云 等云盘占据，下载什么东西都是 pan.xxidu.com。或许我们已经忘记了那个神圣的，不限速的(一般而言) —— 本地下载\n而这个本地下载，就是HTTP下载。(想到当年被高速下载的捆绑软件包迫害场景的恐惧了)\n我们有时候还能看到用一些软件打开一个链接，之后会列出一排，等待你下载文件的软件。这个，是FTP下载。\n这些和我们今天的主题有何相关呢？\n包相关的，如今的人已经蠢到连网址是什么意思都不知道了。\nBT （下载的一种方式 – 参与进来的人越多，下载的速度也越快） BT下载时，软件会分析.torrent种子文件得到Tracker地址，然后连接Tracker服务器，服务器返回其他下载者的IP，下载者再与这些IP联系进行下载，BT下载的Tracker是公开的，而PT下载的Tracker则是私有的，透过禁用DHT有要求地控制用户数量，PT下载还透过论坛等方式的约束机制将BT下载的理念现实化，让用户做到下载的过程中努力上传。\nBT下载和传统的依靠网站服务器作为下载源的HTTP/FTP下载不同，采用的是P2P点对点下载方式。BT下载的同时也进行上传，这样参与进来的人越多，下载的速度也越快。如果下载的人数足够多，往往可以达到网络带宽的峰值。\nPT （和BT一样，又不一样的 — 态度） PT[private tracker]可进行私密范围下载，因此提供PT的论坛（网络论坛）大多非公开的，采用邀请制或是不定时开放注册。用户注册后会得到一个passkey(你在网站注册，下发给你的身份证)，因此可借由passkey识别每个用户，用户从某PT站下载种子后，这个种子会带上有用户的passkey。\n所以PT讲究的是付出。很多人会为PT社群付出。这里就不得不骂一下某吸血雷了。把本应该属于大家的权益，给了他们付钱的会员。还偷偷占用我的带宽上传，只为了服务他们的会员。换句话说，我们都是这个制度里面的狗。\n所以其实，BT和PT可以简单总结为\n知乎用户｜网络系统建设与运维职业技能等级证书持证人：\n下载5M/s，上传3M/s\nBT：为啥上传这么快？赶紧限速！(普通玩家)\nPT：为啥上传这么慢？垃圾宽带！(高级玩家)\n什么是磁力，什么是种子？ 种子 种子是与Tracker相关的，至于什么是Tracker，可以看下图。相当于服务器，如果Tracker都挂了的话，这个下载就会没有速度。也就是大家说的 —— 种子已死。\n至于种子文件的话。一般得需要人去做种，一些镜像下载网站，Linux之类的，也会提供一个种子文件，方便你导入软件下载。下面用Kali官网举例。\n点击后将会下载一个后缀为 .torrent 的文件，以使你导入软件去下载。\n磁力 下面这条链接就是一条很常规的磁力链接格式：\nmagnet:?xt=urn:btih:abc123Abc99123Abc(后面这段是随机的，我想表达的是，磁力链接后面这段组成是有大小写字母和阿拉伯数字组成的)\n而上面的magnet为协议名。后面一段随机的是一段hash值\n而磁力，是互相传播的，也就是当你下载时，也会充当服务器上传。你就是那个存活着的Tracker服务器。\n（除非你停止上传。或者所有100%的人停止上传，而你下载了20%，目前这条链最大，仍旧在共享的人下载了78%，那么他的下载速度会为零，同时上传，直到整条链所有人都下载到78%，大家彻底没了速度。除非100%重新开始共享，或者来了0%的新人加入链(下载)中。）\n下载器 其实有不少出名，挺好的用的下载器。如：μTorrent，qBittorrent，BitTorrent，Motrix等等。\n但是我们今天的主角会是开源免费，支持各大客户端，包括针对没有可视化桌面的Linux的nox版本的（无GUI，Web版本。有GUI也可以用，浏览器访问就行。） —— qBittorrent\nWindows安装 选择对应的版本下载安装就行了：qBittorrent官网下载页面\nMacOS安装 使用官网：qBittorrent官网下载页面\n或者包管理器：brew install qBittorrent\nLinux安装 使用官网：qBittorrent官网下载页面\n或者包管理器：apt install qBittorrent (Debian/Ubuntu)\n或者包管理器：yum install qBittorrent (CentOS/Fedora)\n或者包管理器：pacman install qBittorrent (Arch)\nLinux服务器(无可视化)网页版安装 使用包管理器：apt install qBittorrent-nox (Debian/Ubuntu)\n使用包管理器：yum install qBittorrent-nox (CentOS/Fedora)\n使用包管理器：pacman install qBittorrent-nox (Arch)\n安装完毕后，我们通过在终端输入：qBittorrent-nox 即可运行。如果需要后台运行（退出ssh也不会关闭）则：nohup qBittorrent-nox \u0026amp;\n如果成功启动了 qBittorrent-nox ，通过浏览器 IP+8080 即可访问。\n默认账号密码为：\n账号:admin 密码:adminadmin 如何开始下载磁力/种子文件 软件版 Nox网页版 ","permalink":"https://blog.dontalk.org/posts/%E7%BB%85%EF%B8%8F%E5%A3%AB%E7%9A%84%E8%87%AA%E6%88%91%E4%BF%AE%E5%85%BB%E5%A6%82%E4%BD%95%E4%B8%8B%E8%BD%BD%E7%A3%81%E5%8A%9B%E6%88%96%E7%A7%8D%E5%AD%90%E6%96%87%E4%BB%B6bt%E5%92%8Cpt%E5%8F%88%E6%98%AF%E4%BB%80%E4%B9%88/","summary":"啊。如今的绅♂️士啊。是何等孱弱的存在。你们有脸面对神圣的幻想乡吗？ 如若只想知道如何下载，请滑动到最后。已提供各操作系统的软件下载\u0026amp;使用教程。\n我们要下载磁力或种子文件，就得先搞明白什么是磁力，种子，BT，PT。 首先，我们需要知道，到底什么是磁力链接，什么是种子。如果连它是什么都不知道，谈何下载呢？…你连看片都不配！(语怒)\n我们的世界似乎已经被 …","title":"【绅♂️士的自我修养】如何下载磁力或种子文件？BT和PT又是什么？"},{"content":"VGA，DVI，HDMI，DP的区别 近年来，显示接口已经快迭代到只剩下HDMI和DP了（家用主机/显卡）。再少见VGA这种蓝蓝的，DVI这种白白的接口了。\n但是VGA其实在一些主板上面仍有保留。特别是工业领域上面。即便有时电脑没有DVI，同时拥有HDMI和DP，也会预留一个VGA接口。\n那究竟他们都长什么样，我们又应该如何选用呢？看下表与下图。\n接口名 最高分辨率\u0026amp;刷新率 特点 设计时间 VGA 2048×1536，60Hz 模拟信号，无法传输音频，质量低 1987年 DVI 2560×1600，60hz 数字信号，无法传输音频，质量一般 1999年4月 HDMI 1.4:3840×2160 30Hz，2.0:5120×2880 30Hz 数字信号，可以传输音频，质量好 2002年12月 DP 1.2:4096X2160 60Hz，1.3:7680X4320 30Hz 数字信号，可以传输音频，质量很好 2006年5月 接口图 VGA DVI DP \u0026amp; HDMI ","permalink":"https://blog.dontalk.org/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%B8%B8%E8%AF%86vgadvihdmidp%E9%83%BD%E6%98%AF%E4%BA%9B%E4%BB%80%E4%B9%88/","summary":"VGA，DVI，HDMI，DP的区别 近年来，显示接口已经快迭代到只剩下HDMI和DP了（家用主机/显卡）。再少见VGA这种蓝蓝的，DVI这种白白的接口了。\n但是VGA其实在一些主板上面仍有保留。特别是工业领域上面。即便有时电脑没有DVI，同时拥有HDMI和DP，也会预留一个VGA接口。\n那究竟他们都长什么样，我们又应该如何选用呢？看下表与下图。\n接口名 最 …","title":"【计算机常识】VGA，DVI，HDMI，DP都是些什么？"},{"content":"问题解决 至于是怎么样引起的呢。这就得把锅给到Windows了。如果没猜错。\n你的Windows应该一直开着快速启动之类的设置。或者电脑只是休眠情况下（“热插拔”）之类的情形取出了硬盘。\n反正就是。。Windows的关机其实并非真正的关机，只是深度地休眠，等待你的快速启动选项将其快速唤醒。\n这种情况取出的硬盘，一般会被Windows占用和上锁。导致到了其他操作系统就变成了只读硬盘。\n想要修复也非常简单。只需要：\n#打开终端并输入： lsblk #找到分区（如果您有一个 HDD，那么它将是/dev/sdax，另一个 HDD 将是sdbx）。 #当要修复的分区是 /dev/sda5 时，请在挂载时将其卸载： sudo umount /dev/sda5 # 「sda5是一个变量。尤其是a和5」 #运行以下命令，其中a是硬盘的位置，5是分区的位置： sudo ntfsfix /dev/sda5 引用 如果在 Windows 10 双启动的情况下发生这种情况，您需要取消登录turn on fast startup然后Control Panel –\u0026gt; Power Options –\u0026gt; Choose what the power buttons do关机，而不是重新启动。正确完成后，您将能够以读/写权限正常访问 NTFS 分区。\n发生这种情况的原因是 Windows 10 更新将其重置为默认的快速启动，这是一种休眠形式。\nWindows下设置：\nBIOS下设置：\n","permalink":"https://blog.dontalk.org/posts/%E6%88%91%E7%9A%84ntfs%E6%A0%BC%E5%BC%8F%E7%A1%AC%E7%9B%98%E5%9C%A8linux%E4%B8%8B%E9%9D%A2%E5%8F%AA%E8%AF%BB%E6%80%8E%E4%B9%88%E5%8A%9E/","summary":"问题解决 至于是怎么样引起的呢。这就得把锅给到Windows了。如果没猜错。\n你的Windows应该一直开着快速启动之类的设置。或者电脑只是休眠情况下（“热插拔”）之类的情形取出了硬盘。\n反正就是。。Windows的关机其实并非真正的关机，只是深度地休眠，等待你的快速启动选项将其快速唤醒。\n这种情况取出的硬盘，一般会被Windows占用和上锁。导致到了其他操 …","title":"我的NTFS格式硬盘在Linux下面只读怎么办！"},{"content":"什么是UEFI 目前来说，基本所有电脑都支持UEFI启动了。但是估计很多人不清楚UEFI和传统引导有何区别。下面引用维基百科做简单解释：\n统一可扩展固件接口（英语：Unified Extensible Firmware Interface，缩写UEFI）是一种个人电脑系统规格，用来定义操作系统与系统固件之间的软件界面，作为BIOS的替代方案。可扩展固件接口负责加电自检（POST）、联系操作系统以及提供连接操作系统与硬件的接口。\n而UEFI的前身是Intel在1998年开始开发的Intel Boot Initiative，后来被重命名为可扩展固件接口（Extensible Firmware Interface，缩写EFI）。Intel在2005年将其交由统一可扩展固件接口论坛（Unified EFI Forum）来推广与发展，为了凸显这一点，EFI也更名为UEFI（Unified EFI）。UEFI论坛的创始者是11家知名电脑公司，包括Intel、IBM等硬件厂商，软件厂商Microsoft，及BIOS厂商安迈科技、Insyde、Phoenix。\nEFI分区（ESP分区） 相信大家或多或少都会看到EFI这个字样。\nEFI系统分区（英语：EFI system partition，简写为ESP，使用DiskGenius工具分区的时候会询问你是否创建），是一个FAT或FAT32格式的磁盘分区，UEFI固件可从ESP加载EFI启动程序(系统引导程序)或者EFI应用程序。\n所以，如果你没有创建ESP分区，就没法修复引导(没有引导)，装完的Windows系统就会发现打不开～提示找不到可引导的系统。（如果你没插启动盘之类的USB设备，BIOS检查了一圈启动项没找到可以引导的磁盘/设备）（占很少的磁盘空间，大概几百兆(M)）\nUEFI优势 安全性更高：UEFI引导方式通过数字签名验证启动程序的真实性，可以有效防止病毒和恶意软件的攻击。\n启动速度更快：UEFI引导方式可以并行加载多个驱动程序和操作系统内核，启动速度更快。\n支持更多的设备：UEFI引导方式不仅支持硬盘启动，还支持从网络、光盘等其他存储介质启动。\n支持更大的硬盘：UEFI引导方式使用GPT分区表，支持大于2TB的硬盘。\n同时支持MBR/GPT格式的硬盘！\n什么是Legacy(传统引导) 顾名思义，是旧的，老的。为了古早的操作系统，软件，甚至是硬件的运行受到支持，所以目前电脑还是会支持UEFI/Legacy两种引导方式。以供用户选择。\n但是系统只能安装在MBR格式磁盘上。(不支持GPT格式的硬盘)\nLegacy优势 更优秀的兼容性，几乎什么系统都能跑。（安全的Arch和MacOS等系统除外）\n注意：系统只能安装在MBR格式磁盘上。(不支持GPT格式的硬盘)。但是UEFI同时支持MBR/GPT格式的硬盘。\n关闭快速启动一说 这个一般与微软Microsoft的Windows相关：\n为什么呢？因为一旦win处于快速启动产生的关机状态，或者处于休眠状态，会导致NTFS分区被锁死（锁成只读或者直接无法挂载），而在配置了挂载点的分区无法挂载的情况下，Linux就会卡在开机界面的地方。\n在主板处可以找到 FastBoot 并关闭。有时对Linux并不影响。但是确实很多人都说关掉为好。但我是不关闭的。（不关闭，装系统的时候，有可能读不到电脑硬盘）\n","permalink":"https://blog.dontalk.org/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%B8%B8%E8%AF%86bios%E9%87%8C%E9%9D%A2uefi%E5%92%8C%E4%BC%A0%E7%BB%9F%E5%BC%95%E5%AF%BClegacy%E6%98%AF%E4%BB%80%E4%B9%88/","summary":"什么是UEFI 目前来说，基本所有电脑都支持UEFI启动了。但是估计很多人不清楚UEFI和传统引导有何区别。下面引用维基百科做简单解释：\n统一可扩展固件接口（英语：Unified Extensible Firmware Interface，缩写UEFI）是一种个人电脑系统规格，用来定义操作系统与系统固件之间的软件界面，作为BIOS的替代方案。可扩展固件接口负 …","title":"【计算机常识】BIOS里面UEFI和传统引导(Legacy)是什么"},{"content":"Nginx故障分析与排查 我建立了一个网站，并且文件不以 .conf 的格式存放在 conf.d 文件夹。而是存放在了 sites-available 这个文件夹，之后软链接到了 sites-enabled。\n而之后的修改，只需要修改 sites-available 里面的配置文件就好了。软链接命令：\nln -s /etc/nginx/sites-available/xxx /etc/nginx/sites-enabled/xxx nginx -t 也显示语法正常。万事俱备，启动也正常。\n但是访问就出问题了，倒没有显示502，404之类的，也没有跳欢迎页，就是一直跳下载文件(磁盘图标)，而且我已经正常配置了 php-fpm.sock 了。真让人摸不着头脑。\n在经历一个多小时的排查后，发现只要把 sites-available 下面的配置文件，后缀 .conf 去掉就行了（没错，就是没有后缀就行了）。\n我被 nginx.conf 这个文件误导了。这个Nginx的默认配置文件下的第58行下面，有两行规则。分别是：\ninclude /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; 可以看到，*代表存入怎么样都行。但是*只能接受无后缀，如果你使用 .conf 的格式存入 sites-available 后，软链接到 sites-enabled，事实上是不起作用的。\n如果你希望用 .conf 的话，只需要在 conf.d 文件夹创建一个关于网站的 .conf 文件来写配置就行了。但是这是不严谨的，不方便我们启动/停止网站的访问。\n（如果希望停止网站的访问，只需要删除软链接到 sites-enabled 的文件即可。）\n指定配置文件运行 如果你打算排查错误，或者指定配置文件运行\n可以试试命令：\nnginx -c /etc/nginx/xxxx.conf 来指定一个配置文件。\n最后，无论如何，记得 nginx -s reload 哦\n(插一嘴)正常访问假象 替换默认的html文件夹(Nginx欢迎页面)。\n用上/index.php，访问正常是个假象。\n当你尝试：\ntry_files, rewrite , return 302/200 \u0026#34;xxx\u0026#34;; 就会发现配置文件没有生效，从而怀疑人生。\n所以，为什么不试试 conf.d 目录，或者删除 .conf 这个后缀呢？\n","permalink":"https://blog.dontalk.org/posts/nginx%E9%A1%B5%E9%9D%A2%E8%B7%B3%E4%B8%8B%E8%BD%BD%E6%96%87%E4%BB%B6%E7%A3%81%E7%9B%98%E5%9B%BE%E6%A0%87/","summary":"Nginx故障分析与排查 我建立了一个网站，并且文件不以 .conf 的格式存放在 conf.d 文件夹。而是存放在了 sites-available 这个文件夹，之后软链接到了 sites-enabled。\n而之后的修改，只需要修改 sites-available 里面的配置文件就好了。软链接命令：\nln -s …","title":"Nginx页面跳下载文件(磁盘图标)"},{"content":"SSH故障排查 我们使用systemctl status sshd命令，可以看到ssh服务是处于正常运行状态的。\n事实上我们可以通过ssh命令查找到主机，并要求输入密码时，就侧面证明了ssh服务在正常运行了。\n但是为什么无法登陆，是因为ssh服务的配置文件，有两条默认的配置（被注释的）阻止了我们密码登陆root（我的ssh服务是Debian安装时预装的）\n文件来自Linux下面的/etc/ssh/sshd_config\n我们可以使用vim打开文件后，在随意位置追加下面内容。也可以选择取消注释，但是PermitRootLogin后面的prohibit-password需要更改为yes\n我选择了追加内容在随意位置。配置如下：\nPermitRootLogin yes PasswordAuthenstication yes 配置解释 其中PermitRootLogin yes是允许以 root 用户身份登录 SSH。PasswordAuthentication yes是启用基于密码的身份验证。\n这个时候，保存并退出。之后重启ssh服务就可以正常ssh密码登录root了。\n重启ssh服务：systemctl restart sshd\n查看ssh服务：systemctl status sshd\n","permalink":"https://blog.dontalk.org/posts/pve%E6%96%B0%E5%AE%89%E8%A3%85%E7%9A%84debianssh%E6%9C%8D%E5%8A%A1%E5%B7%B2%E5%BC%80%E5%90%AF%E5%AF%86%E7%A0%81%E6%AD%A3%E7%A1%AE%E5%8D%B4%E6%97%A0%E6%B3%95%E7%99%BB%E9%99%86/","summary":"SSH故障排查 我们使用systemctl status sshd命令，可以看到ssh服务是处于正常运行状态的。\n事实上我们可以通过ssh命令查找到主机，并要求输入密码时，就侧面证明了ssh服务在正常运行了。\n但是为什么无法登陆，是因为ssh服务的配置文件，有两条默认的配置（被注释的）阻止了我们密码登陆root（我的ssh服务是Debian安装时预装的）\n文 …","title":"PVE新安装的Debian，SSH服务已开启，密码正确却无法登陆"}]