GQ Music

项目中的登录是如何通过JWT+Redis实现的

在项目里,我们的登录鉴权采用 JWT + Redis 的组合来做,既保证了无状态、高性能,又能解决 JWT 不能主动失效的问题。


① 登录发 token

用户登录时,会先校验账号、密码、验证码。
校验通过后系统会生成两个令牌:

  • accessToken:短期有效,用于访问接口
  • refreshToken:长期有效,用于无感续期

这两个 token 都会存进 Redis,用来做:

  • 单端登录控制(同一用户只能保持一个登录)
  • 权限缓存(把用户角色/权限也缓存起来,避免频繁查库)

② 请求时的校验

用户请求时,网关或 Spring Security 会做两件事:

  1. 验证 JWT(签名、有效期等)
  2. 到 Redis 里查一下这个 token 是否还有效

这样一来,token 虽然是无状态的,但我们可以通过 Redis 实现“即时失效”,强制登出。


③ 自动续期

如果 accessToken 快过期了:

  • 系统会检测 Redis 中的登录态是否有效
  • 若有效,就自动生成一个新的 accessToken
  • 如果 accessToken 过期,但 refreshToken 还有效,就通过 refreshToken 换取一对新的 token

实现用户的“无感续期”。


④ 登出与强制失效

用户退出时,我们直接删除 Redis 中的 token。
如果用户修改密码或账号被封禁,我们会更新用户的 版本号(ver),使所有旧 token 全部失效。


⑤ 风控与并发控制

Redis 还用来做:

  • 登录失败次数限制(防爆破)
  • 设备登录控制(单端、多端可配置)

⑥ 为什么用 Redis?

因为纯 JWT 无法主动失效,而 Redis 解决了:

  • 及时踢人
  • 并发控制
  • 权限缓存
  • 单端登录
  • 高性能 & 易扩展

微信扫码登录是怎么实现的?

微信扫码登录整体是“前端跳微信 → 微信回调给后端 → 后端用 code 换用户信息 → 生成本地登录态”的流程。我用的是 YunGouOS + 微信 OAuth,最终落地成扫码授权 + 本地账号绑定 + JWT + Redis 会话管理


① 前端获取授权链接并跳转微信

前端点击“微信扫码登录”,会先请求后端一个接口。
后端通过 YunGouOS SDK 生成微信的授权 URL,然后前端跳转过去,让用户在微信里确认授权。


② 微信回调 → 我们拿到 code

用户确认后,微信把一个 code 回调给后端的 callback 域名。
我这边的回调页面做了一个简单中转:把这个 code 安全地传回前端指定路由,准备发给后端换取登录态。


③ 后端用 code 换 openId,再生成自己的登录态

前端拿到 code 后,再调一个后端接口:

  1. 后端调用 YunGouOS/微信接口,用 code 换到用户信息(openId、昵称、头像 等)
  2. 用 openId 去查本地用户
    • 没有的话自动创建一个本地账号(做一下昵称清洗、头像补写)
  3. 给这个本地用户签发 JWT,并把 token 的 jti 写进 Redis,用来实现:
    • 单端登录
    • 令牌即时失效
    • 后续续期和权限控制

最后把 token 返回给前端。


④ 前端存 token 并拉取用户资料

前端拿到 JWT 存到 localStorage 或 Pinia,然后请求一次 /user/info 拉用户头像昵称,再跳首页。
整个扫码流程就完成了。


⑤ 安全与实际细节

  • 只有扫码相关的接口放行,其余都走 JWT 校验
  • code 只能用一次,所以前端拿到后要立即换 token
  • Redis 控制 token 状态,实现登出、封禁、单端登录等
  • 限流、防刷、失败日志都有做保护

总结

微信扫码登录就是:前端跳转微信 → 后端用 code 换 openId → 查/建本地用户 → 发 JWT 写 Redis → 前端存 token 完成登录。


项目安全风控是怎么实现的

下面是 2 分钟以内、非常适合面试口述、结构清晰、容易记忆的 “安全与风控三件套”总结版回答

我这块整体分成 三层防护:入口校验、会话可控、前后端联动风控。


① 入口安全:邮箱 + 图形验证码双重校验

为了防止撞库、遍历邮箱、暴力发验证码,我做了几层保护:

  • 登录/注册 先验证图形验证码,失败次数通过 Redis 计数,再失败会进入“强制验证码 + 冷却期”。
  • 邮箱验证码严格控制:一次性、短期、限频次。验证码哈希存 Redis,校验后立即作废,并限制 IP / 邮箱发码频率(分钟/小时/日三级)。
  • 错误提示和响应时延做统一,避免被枚举账号或被 timing attack 判断邮箱是否存在

一句话:入口先拦截,把机器流量挡在最外圈。


② 会话可控:JWT + Redis 即时失效体系

会话做的是“可控、可踢、可失效”的体系:

  • accessToken 短期、refreshToken 长期,JWT 里带 jti + 用户版本号 ver
  • Redis 记录用户当前有效的 jti,实现 单端登录 / 踢下线
  • 需要即时失效时(如登出、封禁、改密码),我们会:
    • 删除 Redis 的 jti
    • 或提升用户 ver
      旧 token 立刻变成 401
  • refreshToken 刷新时做轮换,旧的 refresh 会标记失效,防止重放。
  • 前端统一拦截 401/403,做清理与跳转。

一句话:JWT 不可控的问题,用 Redis 做“会话白名单”把它补齐。


③ 前后端联防与风控体系

实现的是“请求没进应用就被网关挡掉 + 后端精细校验 + 前端减少暴露”。

  • 网关/Nginx 层
    • 登录 / 发验证码类接口限流(IP、UA)
    • 黑名单短封
    • 强 CORS 白名单 / HTTPS / 安全 Header(HSTS、XFO、CSP 等)
  • 后端层
    • 所有写操作都鉴权、参数校验
    • 敏感操作二次确认
    • 文件上传做 MIME / 大小验证
    • 审计日志上报:登录失败、验证码异常、多次 401 及时告警
  • 前端层
    • token 只放内存/Pinia,不打印到日志
    • 登录表单失败一定次数后强制验证码
    • 使用 encodeURI、规范 URL 构造,避免注入

一句话:网关挡机器流量,后端拦恶意行为,前端减少攻击面。

我把安全做成了三段式:入口用验证码体系挡掉黑产,会话用 JWT + Redis 做即时失效与单端登录,外围通过网关限流 + 后端鉴权 + 前端约束做联防,整体既安全又不影响用户体验。


项目中的缓存是怎么做的?(Spring Cache + Redis)

设计思想(Spring Cache + RedisTemplate 双轨方案、一致性策略、命名空间清理等):

“我们项目的缓存体系主要是两条线并行:Spring Cache 管业务热点数据RedisTemplate 管特殊 Key,整体用的是“读缓存、写删即清、命名空间化”的一致性策略。

第一条线:Spring Cache 管热点数据

像歌曲、歌单、歌手、专辑这些“读远大于写”的业务,我都统一放在 Spring Cache 下面 ——
使用 @Cacheable 做缓存,@CacheEvict(allEntries=true) 做命名空间级清理。

举例:

  • 查歌曲、查专辑 → @Cacheable(songCache)@Cacheable(albumCache)
  • 更新或删除时 → @CacheEvict(songCache, allEntries=true)
    这样一次更新操作能把整个命名空间清掉,避免复杂的局部逐 Key 删除导致漏删问题

这一条线的核心就是 透明、好维护、强一致


第二条线:RedisTemplate 管特殊缓存

有些数据不适合用注解,比如:

  • 用户推荐列表(recommended_songs:{userId})
  • 验证码(verificationCode:{email})
  • 热搜榜
    我就用 RedisTemplate 手动读写,TTL 也可以独立控制,比如推荐列表 30 分钟、验证码 5 分钟。

因为这些 Key 不在 Spring Cache 管理范围,所以我写了一个 CachePurger 清理器(应用级缓存清理器),在删除歌手、删除歌曲、删除专辑、删除歌单等链路里统一清掉相关前缀,保证和数据库的最终一致性。


一致性策略:写后清理、读路径回填

我用的是最稳的做法:

  1. 读操作:先查缓存 → 没命中就回库并回写缓存。
  2. 写操作:先写数据库 → 再清缓存(命名空间整体删)。

这样能保证:

  • 不会出现脏读
  • 删除或更新后立即生效
  • 而且逻辑简单、维护成本低

如果遇到数据规模增长,我们也可以把前缀删除升级到 SCAN 分批处理。


最终效果

  • 热门列表、详情页命中率非常高
  • 更新后不会读到旧数据
  • 特殊缓存也通过 CachePurger 保证一致性
  • 整体结构简单、好维护,也方便以后扩展

为什么我用 Spring Cache + Redis,而不是单用一个?

“我当时是做过对比的,单用 Spring Cache 或单用 Redis 都能跑,但都不够优雅,所以我最后用了“Spring Cache + Redis 双轨”的方式,各取所长。

先说可不可以单用:

1)只用 Spring Cache 也能做缓存,但有几个明显短板:

  • Spring Cache 默认底层是本地内存,多实例不共享,服务重启缓存清空。
  • 即便换成 RedisCacheManager,它的注解模式也更适合“读多写少”的业务缓存,比如详情页、列表页。
  • 但像验证码、热搜榜、计数器、限流、推荐列表这些需要
    ZSET、List、Hash、原子递增、TTL 精细控制 的场景,它就不适合了。

简单说就是:Spring Cache 很方便,但能力比较标准化,没有 Redis 那么灵活。


2)只用 Redis(RedisTemplate)当然也能做,但问题是:

  • 所有业务缓存都要自己写 set/get
    → 很容易代码到处散落重复逻辑。
  • 每个 Key 的命名、过期、失效策略都要自己维护
    → 非常容易“漏删”。
  • 特别是跨对象的缓存一致性(比如改了歌曲,要清多个缓存)
    → 手写成本高,还容易埋坑。

换句话说:Redis 功能强,但太“底层”,全靠自己维护,很容易越写越乱。


所以我最后用了“Spring Cache + Redis”双轨方案:

Spring Cache 负责业务缓存(标准、透明、可维护)

  • 查详情、查歌单、查专辑走 @Cacheable
  • 更新、删除走 @CacheEvict(allEntries=true) 命名空间整删
  • 代码简洁、可读性强、维护成本低
  • 底层还是 Redis,所以是分布式一致的

这是“优雅且稳定”的业务缓存方式。


RedisTemplate 负责“特殊 Key”与“风控类数据”

  • 验证码(一次性、短 TTL、限流)
  • 热搜榜(ZSET)
  • 推荐列表(List + TTL)
  • 登录失败计数/限流(INCR + EXPIRE)

这些都需要 Redis 的高级数据结构和原子操作,是 Spring Cache 做不了的。


最后是统一一致性策略:

  • 读:先查缓存 → miss 才回库
  • 写:先写数据库 → 再按命名空间清缓存
  • 自定义 Key 统一由清理器按前缀清理,避免散落删除

这样业务缓存和特殊 Key 各管一类,非常清晰、也不容易出错。


一句话总结

Spring Cache 让我把业务缓存写得很优雅,Redis 给我足够的灵活性处理验证码、限流、推荐等特殊场景;两者结合就是“优雅 + 强大”。单用任何一个,要么不好维护,要么能力不够。


如果你想,我还能帮你准备一个 30 秒极速版本用来快速回击面试官。

MinIO + 分片上传 + 断点续传 + FFmpeg 转码,是怎么做的?

“这一块我当时是按小文件、大文件和转码处理三条线来设计的,尽量保证体验、性能和安全三个维度都能兼顾。”


① 小文件上传:走简单快速通道

像头像、封面这类小文件,我直接让后端用 MinIO 的 PutObject 走流式上传,文件不会落在服务器本地,上传完就给一个 对象 key,数据库只存 key,不存外链,这样更安全、也便于后期换域名。


② 大文件上传:分片 + 断点续传

视频、音频体积大,所以我用 MinIO 的 Multipart Upload 做分片上传:

  1. 前端先调 init 接口
    后端生成 uploadId,并在 Redis 记录文件的 hash、size 和已上传的分片,用 24 小时 TTL 做续传窗口。
  2. 断点续传
    前端每次上传前会查一下 alreadyUploadedParts,是 Redis 或 MinIO listParts 返回的,直接跳过已经传成功的 part。
  3. 合并分片
    全部分片上传成功后,前端调 complete 接口,MinIO 在服务端合并,后端做一次 MD5/ETag 校验。

整个过程前后端都没落本地文件,走网关直流式转发,所以大文件也能传得很稳。


③ FFmpeg 转码处理:异步执行

上传成功后我会把转码任务丢到异步队列里做:

  • 音频我会统一转成 mp3/aac 标准格式
  • 生成 30 秒试听音频、波形图、封面图
  • 视频我会额外抽封面 poster,有需要也会做压缩或重新编码

所有转码产物也都会按目录规范写回 MinIO,例如:
songs/originalsongs/encodedsongs/preview 等。

同时在数据库记录转码状态(PROCESSING → SUCCESS/FAILED),失败会自动清理孤儿文件,防止垃圾数据堆积。


④ 出链和性能优化

数据库永远只存 对象 key,前端访问文件时统一通过
https://域名/oss/
由 Nginx 反向代理到 MinIO。

  • 加了磁盘缓存
  • 支持 Range(206)缓存
  • 第二次访问几乎都是本地命中,明显提速

换域名或接 CDN 时只要改 Nginx,不改数据库,不影响业务。


⑤ 可靠性与安全

  • Redis 管分片进度、限流、续传
  • 服务端严格校验 MIME 和大小
  • 所有文件名 encodeURI,避免中文/空格问题
  • 删除/读取都只走对象 key,不暴露真实存储路径

一句话总结(面试必杀)

“MinIO 我主要做了三块:上传、续传和转码。小文件用后端直传 MinIO,返回对象 key,库里只存 key,不存外链。大文件就走分片上传,Redis 记录 uploadId 和已传分片,支持断点续传,最后 complete 一次性合并。上传成功后把任务丢到异步队列,用 FFmpeg 统一做转码、抽封面、生成试听文件等,再把产物写回 MinIO。前端访问文件统一走 /oss/,Nginx 反代 MinIO 还能做缓存,加速很明显。整体就是:小文件直传、大文件分片续传、后台异步转码、只存对象 key、安全又好维护。”


歌曲批量导入怎么做的?

“我们的歌曲批量导入我做成了一个两步式、强校验、可部分成功的流程,重点是提升效率又不让脏数据入库。

首先在交互上,是两步走:先选或新建一个专辑,然后进入批量录入页面。这里支持拖拽上传音频和歌词,上传过程有实时进度,音频上传后我会自动解析时长、自动填充发行日期、封面等专辑信息,减少操作量。为了适配听书类内容,还加了一个‘听书模式’,自动同步风格,进一步降低重复输入。

在校验上,我做得比较严格:必填项校验、音频/歌词格式校验、同名重复校验、歌手存在性校验,全都在前端和后端双层兜底。导入失败的行会高亮,并给出原因汇总,方便用户快速修正。

后端处理也是逐行校验、逐行入库的策略。接口接受 FormData,每一行数据通过后再落库,不会因为某一行错误导致整个批次失败。对象存储仍然只存对象键,保证整套系统的数据格式一致。事务上是单行粒度提交,可部分成功、可重试、具备幂等性,例如重复歌曲会被安全跳过。

整体收益也比较明显:一次能导几十首,上线后数据录入效率提升很大;强校验减少了脏数据;拖拽、自动填充、进度条这些细节让整体体验更顺滑。

如果追问,我会补充:这套逻辑已经预留了接入 MinIO 分片上传和断点续传的扩展空间,也支持后续加批量预检或回滚功能。”