编辑
2026-01-10
java炒饭
00

姓名:艾子鸣

电话:17386110953
求职意向:Java后端开发实习生
邮箱:305

编辑
2026-01-09
redis
00

单节点的redis并发能力有限,因此我们需要搭建主从集群,实现读写分离。一般是主节点进行写,从节点进行读。

主从同步:

  • 全量同步:从结点第一次与主节点同步数据时使用的方案。从节点向主结点发送同步请求,携带参数replicationId,offset,主节点会根据replicationId来判断是否是第一次同步,如果是第一次同步(Id不一致),则主节点会把自己的replicationId和offset发给从节点,同时主节点执行bgsave生成rdb文件。在生成的同时,会开启一个缓冲区记录该期间收到的所有写命令,最后把rdb和这个缓冲区的信息(类似日志)一起发送给从节点。
  • 增量同步:还是从节点发送请求,主节点判断是不是第一次请求,不是则获取从节点的offset,并把从结点的offset和自身offset之间的数据同步给从节点。

高可用:

哨兵模式(sentinel):

  • 本质自己也是一个redis服务
  • 每1s向集群的每个实例发送ping请求。如果在规定时间收不到响应,认为该结点主观下线。
  • 当指定数量的哨兵认为某个结点都下线了,则认为该节点客观下线。这个数量一般不低于哨兵数量的一半。
  • 当主节点下线了,哨兵会推选一个新的主节点。选主规则:
    • 如果该结点与原来的主节点断开连接时间超过指定值,则不纳入考虑。
    • 根据结点的优先级来判断,优先选择优先级高的,可在配置文件中设置优先级。
    • 优先级相同时选择offset大的节点
    • 以上条件都相同时选择运行ID最小的节点

脑裂:

  • 由于网络原因,哨兵监测不到主结点,认为主节点挂了,实际上主节点并没挂,此时客户端还在与原来的主节点发送请求,然而哨兵却选择了一个新的主节点,原来的主节点变成了从节点,在同步数据的时候,期间所写的数据就失效了。
  • 解决方案:可以通过配置min-slaves-to-write(最少从节点数)和min-slaves-max-lag(最大延迟秒数)。当主节点的从节点数量少于配置值,或者从节点的延迟时间超过配置值时,主节点会拒绝写入请求,从而避免数据不一致。这是一种 “宁可拒绝写入,也要保证数据一致性” 的取舍。在脑裂期间,被孤立的旧主节点会提供高可用性。
编辑
2026-01-09
redis
00

redis实现的分布式锁:

  • 使用setnx命令时,需要设置 ttl,防止系统故障导致锁无法释放。
  • 自己实现的分布式锁的缺陷:我们并不知道准确的业务执行时间,因此这个过期时间不好控制。
  • 不可重入

因此我们使用第三方工具redisson:

  • 提供看门狗(WatchDog),一个线程获取锁成功之后,WatchDog会给持有锁的线程续期(默认每隔10s续期)
  • 可重入,底层采用了一个hash结构,用线程id和该锁锁的次数作为依据,如果发现锁已经被获取了,但是是当前线程获取的,我们就可以再次获得锁,并把次数加1。如果发现这个线程id不是自己的,则无法获取锁。释放锁的时候让次数减1即可。
  • 不能做到主从的强一致性,如果需要,可以使用zookeeper实现的分布式锁。
  • 底层还是setnx和lua
编辑
2026-01-07
java炒饭
00

JWT双Token认证方案

一、核心设计

双Token机制

  • AccessToken:短期(15分钟),前端localStorage存储,每次请求校验
  • RefreshToken:长期(7天),后端HttpOnly Cookie + MySQL存储,仅刷新时校验

验证策略

  • AccessToken:Redis黑名单验证(被吊销的才记录)
  • RefreshToken:MySQL白名单验证(有效的才记录)

二、核心流程

1. 登录流程

用户登录成功 → 生成双Token → AccessToken返回前端存入localStorage → RefreshToken存入MySQL并设置HttpOnly Cookie。

2. API请求流程

前端携带AccessToken → 后端解析JWT(验证签名和过期)→ 查询Redis黑名单 → 不在黑名单则放行 → 处理业务返回结果。

3. 刷新流程(关键)

前端收到401 → 调用刷新接口(Cookie自动携带RefreshToken)→ 后端从MySQL查询RefreshToken进行匹配验证 → 验证通过则:

  1. 生成新双Token
  2. 旧AccessToken加入Redis黑名单(15分钟TTL)
  3. 更新MySQL中的RefreshToken为新值
  4. 新RefreshToken设置到HttpOnly Cookie
  5. 返回新AccessToken给前端

重要:RefreshToken验证只查MySQL,不查Redis黑名单。

4. 登出流程

用户登出 → 当前AccessToken加入Redis黑名单 → 删除MySQL中的RefreshToken记录 → 清除HttpOnly Cookie。

5. 安全事件处理

密码修改等安全事件 → 删除MySQL中的RefreshToken记录 → 强制重新登录。

三、存储设计

Redis黑名单

只存储被吊销的AccessToken,键格式:blacklist:access_token:{token_hash},TTL 15分钟(与AccessToken有效期一致)。

MySQL白名单

user_tokens表存储有效的RefreshToken,每个用户一条记录(user_id唯一约束),验证时只查询此表。

四、验证逻辑

AccessToken验证(黑名单逻辑)

默认所有AccessToken都有效,除非在Redis黑名单中。验证顺序:1.JWT解析 → 2.查Redis黑名单。

RefreshToken验证(白名单逻辑)

默认所有RefreshToken都无效,除非在MySQL白名单中且匹配。验证时只查MySQL,不查Redis。

五、性能与安全

性能优化

  • AccessToken高频验证:Redis内存操作
  • RefreshToken低频验证:MySQL查询,15分钟一次,可接受

安全设计

  • AccessToken:短期有效,泄露风险小,可加入黑名单立即吊销
  • RefreshToken:HttpOnly Cookie防XSS,每次刷新后失效(MySQL更新)
  • 无RefreshToken黑名单:简化设计,通过MySQL更新保证旧Token失效

六、运维管理

监控重点

  • Redis:黑名单大小、内存使用
  • MySQL:查询性能、连接数
  • 业务:刷新频率、401错误率

数据清理

  • Redis:自动过期,无需手动清理
  • MySQL:定期清理过期记录(每天凌晨)

故障处理

  • Redis故障:降级为仅JWT验证(牺牲吊销能力)
  • MySQL故障:刷新功能不可用,需重新登录

七、方案优势

  1. 高性能:高频操作用Redis,低频操作用MySQL
  2. 高安全:双Token分离,HttpOnly Cookie防护
  3. 简化设计:无RefreshToken黑名单,通过MySQL更新保证安全
  4. 易管理:AccessToken黑名单自动清理,MySQL便于审计

核心要点:RefreshToken验证只查MySQL白名单,不查Redis黑名单;安全事件通过删除MySQL记录使RefreshToken失效。

编辑
2025-12-29
java炒饭
00

从OSS叛逃到安全炼狱:我的全栈开源硬核之路

凌晨1点的账单惊吓

“一个月300块?就存点头像图片?”

我盯着阿里云OSS的测试账单,手抖得比咖啡因过量还厉害。这还只是测试期的费用,真上线了还得了?

“不行,”我咬牙,“这违背开源精神。”

作为一个开源项目的维护者,我有个固执的原则:绝不让用户为开源项目花一分钱。OSS?拜拜了您嘞。

一周内的技术栈大迁移

周一,我做出了那个改变一切的决定:从OSS迁移到本地存储。

“简单,”我想,“Docker + Nginx,文件存本地,零成本。”

周二,我开始改造。后端Java代码好办,但前端上传怎么办?我瞄了一眼Vue组件——一个头像上传组件就400多行代码,各种校验、预览、裁剪。

“让trae帮我弄吧。”我偷了个懒。

trae的天真设计

我对IDE里的trae说:“帮我改一下头像上传,不用OSS了,存本地Nginx。”

trae秒回方案:“收到。使用Vite反向代理,前端直接上传到/uploads目录,Nginx直接服务。”

代码很简单:

javascript
// 上传 POST /uploads // 前端直接处理,不走Java

我测试了一下,速度飞快。Node.js处理文件上传,确实比Java省资源。

但周三,我发现一个问题:用户上传新头像,旧头像还在服务器上躺着!

“这不行,”我的资源洁癖发作了,“如果有人恶意上传,服务器不就炸了?”

我命令trae:“加删除逻辑。用户上传新头像前,先删除旧头像。”

trae很听话,把单次上传改成了两个操作:

  1. 删除旧头像(DELETE /uploads/旧文件
  2. 上传新头像(POST /uploads

“完美。”我当时想。

周四的噩梦降临

周四晚上,预上线前的最后一次安全测试。

我随手在APIFox里试了一下:

DELETE http://test.zymusic.top/uploads/admin.png

200 OK。

我又试了试:

POST http://test.zymusic.top/uploads (随便传个文件)

200 OK。

我的血液凝固了。

这两个接口完全没有权限验证!任何人,不需要登录,就能删文件、传文件!

“我草!trae你……”我骂到一半,突然意识到:trae只是个AI,它懂个屁的权限验证。

问题的核心矛盾

凌晨2点,我盯着架构图,发现了几个致命矛盾:

矛盾一:性能 vs 安全

  • Node.js处理文件IO性能好,但不会解析JWT(Java生成的)
  • Java会解析JWT,但处理文件IO性能差
  • 我既想要Node.js的性能,又想要Java的安全

矛盾二:资源洁癖 vs 安全风险

  • 不删旧文件 → 服务器可能被恶意上传撑爆
  • 删旧文件 → 权限漏洞可能被黑客利用
  • 我的资源洁癖逼我选择了更危险的路

矛盾三:开源理想 vs 工程现实

  • 理想:完全免费,不依赖任何付费服务
  • 现实:安全方案要么花钱(OSS),要么花精力(自研)
  • 我选择了最硬核也最危险的路

周五凌晨的绝地求生

凌晨3点,我必须在几个小时内解决这个问题,否则周一预上线就是个笑话。

方案一:让前端直接问Java权限(不行) 因为最终文件操作还是要和Nginx交互,JavaScript处理IO比Java高效得多。而且,让JS解析JWT?不可接受!JWT是Java生成的,就该Java来解析。

方案二:让Java直接管理文件(不行) 性能下降不说,还要大改前后端,一周时间根本不够。

方案三:在Node.js和Nginx之间加一层验证(可行!)

我的方案渐渐清晰:

  1. 前端调用Node.js的上传/删除接口
  2. Node.js先向Java后端请求授权
  3. Java验证JWT,返回是否允许操作
  4. Node.js根据授权结果执行文件操作

用Redis搭建“操作锁”

但还有个问题:如何确保用户是“先删后传”,而不是只传不删,或者只删不传?

我的解决方案:用Redis记录操作状态

javascript
// Node.js上传服务伪代码 // 删除接口 app.delete('/uploads/:filename', async (req, res) => { // 1. 从header拿到JWT const token = req.headers.authorization; // 2. 问Java:这人有权限删这个文件吗? const canDelete = await javaBackend.verifyDelete(token, filename); if (!canDelete) { return res.status(403).send('滚犊子'); } // 3. 执行删除 await fs.unlink(filepath); // 4. 在Redis标记:这个用户已删除旧头像,可以上传新的了 await redis.setex(`upload_ok:${userId}`, 5, '1'); res.send('删除成功'); }); // 上传接口 app.post('/uploads', async (req, res) => { const token = req.headers.authorization; // 1. 问Java:这人有权限上传吗? const canUpload = await javaBackend.verifyUpload(token); if (!canUpload) { return res.status(403).send('没权限'); } // 2. 检查Redis:他是不是刚删了旧头像? const canProceed = await redis.get(`upload_ok:${userId}`); if (!canProceed) { return res.status(403).send('请先删除旧头像'); } // 3. 执行上传 const newFilename = await saveFile(req.file); // 4. 清理Redis标记 await redis.del(`upload_ok:${userId}`); res.send({filename: newFilename}); });

Java后端的验证逻辑:

java
// Java伪代码 public boolean verifyDelete(String token, String filename) { // 1. 解析JWT,获取用户ID String userId = decodeJWT(token); // 2. 查数据库,这个用户的当前头像是不是这个文件? User user = userDao.findById(userId); if (user.getAvatar().equals(filename)) { return true; // 是自己的头像,可以删 } return false; // 不是自己的头像,滚 } public boolean verifyUpload(String token) { // 管理员?直接放行! if (isAdmin(token)) { return true; } // 普通用户:检查是否有上传权限(比如是不是被封禁了) return userCanUpload(token); }

周六凌晨的缝合怪架构

凌晨4点,我部署完这个“四不像”的架构:

前端Node.js上传服务Java权限验证Redis状态管理Nginx文件服务

绕了地球一圈,但:

  1. 安全了(所有操作经过Java验证)
  2. 性能保留了(Node.js处理文件IO)
  3. 资源洁癖满足了(旧文件会被删除)
  4. 开源精神坚持了(没花一分钱)

“这就是工程现实吗?”我苦笑,“为了不花钱,我造了个比OSS复杂十倍的轮子。”

周日的最后反思

系统终于安全了。我瘫在椅子上,回顾这一周的过山车:

1. 开源不等于免费

我以为“不花钱”就是开源精神,但忽略了“不花钱”可能意味着“花更多精力”。OSS的300块,买的是别人解决好的安全方案。我省了300块,搭进去一周的睡眠。

2. 资源洁癖是种病,但得治

我的担心是对的——恶意上传真的能搞垮服务器。但我的解决方案错了——不应该为了省资源而牺牲安全。应该先保证安全,再考虑资源。

3. AI是工具,不是工程师

trae能写出漂亮的代码,但不懂架构、不懂安全、不懂权衡。它能执行命令,但不能思考后果。把AI当工程师用,就像让厨子开飞机——早晚出事。

4. 性能与安全的永恒战争

我既想要Node.js的IO性能,又想要Java的安全验证。鱼和熊掌不可兼得,但我用Redis和HTTP调用强行兼得了。代价是:复杂度爆炸。

5. 一周能改变什么?

一周前,我以为只是换个存储方案。一周后,我造了个分布式文件权限系统。有时候,工程进度不是按计划走的,是按问题走的。

给所有开源硬核玩家的忠告

如果你也在维护开源项目,想坚持“完全免费”,记住我的教训:

1. 安全永远第一

用户能接受功能少,但不能接受数据丢。安全漏洞是开源项目的死刑。

2. 复杂度是隐形成本

你省了云服务的钱,但付出了开发、维护、调试的精力。这些精力也是成本。

3. 测试,测试,再测试

特别是安全测试。我要是早做安全测试,周二就能发现问题,不用拖到周四凌晨。

4. 承认工具的限制

AI能帮你写代码,但不能帮你做架构决策。你是工程师,它是工具。别搞反了。

5. 有时候,花钱买时间是对的

300块买OSS服务,还是300小时造轮子?在开源项目里,时间也是稀缺资源。

最后

窗外天空泛白,一周的挣扎结束了。

我的开源项目依然坚持“完全免费”,但代价是:一个比商业方案复杂十倍的权限系统。

这值得吗?我不知道。

但我知道的是:当用户不用为这个项目花一分钱时,他们不会知道,有个傻子在深夜用Redis、Node.js、Java和Nginx,造了个丑陋但安全的轮子。

而那个傻子,现在需要睡一觉。

预上线倒计时:12小时。

这次,真的准备好了。🚀