GQ Music项目相关解答
GQ Music
项目中的登录是如何通过JWT+Redis实现的
在项目里,我们的登录鉴权采用 JWT + Redis 的组合来做,既保证了无状态、高性能,又能解决 JWT 不能主动失效的问题。
① 登录发 token
用户登录时,会先校验账号、密码、验证码。
校验通过后系统会生成两个令牌:
- accessToken:短期有效,用于访问接口
- refreshToken:长期有效,用于无感续期
这两个 token 都会存进 Redis,用来做:
- 单端登录控制(同一用户只能保持一个登录)
- 权限缓存(把用户角色/权限也缓存起来,避免频繁查库)
② 请求时的校验
用户请求时,网关或 Spring Security 会做两件事:
- 验证 JWT(签名、有效期等)
- 到 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 后,再调一个后端接口:
- 后端调用 YunGouOS/微信接口,用 code 换到用户信息(openId、昵称、头像 等)
- 用 openId 去查本地用户
- 没有的话自动创建一个本地账号(做一下昵称清洗、头像补写)
- 给这个本地用户签发 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 清理器(应用级缓存清理器),在删除歌手、删除歌曲、删除专辑、删除歌单等链路里统一清掉相关前缀,保证和数据库的最终一致性。
一致性策略:写后清理、读路径回填
我用的是最稳的做法:
- 读操作:先查缓存 → 没命中就回库并回写缓存。
- 写操作:先写数据库 → 再清缓存(命名空间整体删)。
这样能保证:
- 不会出现脏读
- 删除或更新后立即生效
- 而且逻辑简单、维护成本低
如果遇到数据规模增长,我们也可以把前缀删除升级到 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 做分片上传:
- 前端先调 init 接口
后端生成 uploadId,并在 Redis 记录文件的 hash、size 和已上传的分片,用 24 小时 TTL 做续传窗口。 - 断点续传
前端每次上传前会查一下 alreadyUploadedParts,是 Redis 或 MinIO listParts 返回的,直接跳过已经传成功的 part。 - 合并分片
全部分片上传成功后,前端调 complete 接口,MinIO 在服务端合并,后端做一次 MD5/ETag 校验。
整个过程前后端都没落本地文件,走网关直流式转发,所以大文件也能传得很稳。
③ FFmpeg 转码处理:异步执行
上传成功后我会把转码任务丢到异步队列里做:
- 音频我会统一转成 mp3/aac 标准格式
- 生成 30 秒试听音频、波形图、封面图
- 视频我会额外抽封面 poster,有需要也会做压缩或重新编码
所有转码产物也都会按目录规范写回 MinIO,例如:
songs/original、songs/encoded、songs/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 分片上传和断点续传的扩展空间,也支持后续加批量预检或回滚功能。”





