登录 | 魔法链接 | 设计文档

3350 字
17 分钟
登录 | 魔法链接 | 设计文档

登录 | 魔法链接 | 设计文档#

1. 需求分析#

1.1 业务背景#

为提升用户登录体验,减少密码输入步骤,新增魔法链接登录方式。用户只需输入邮箱并完成人机校验,即可通过点击邮件中的链接完成登录。

1.2 功能需求#

序号需求点描述来源
1发送魔法链接用户输入邮箱并完成人机校验后,后端发送包含魔法链接的邮件用户需求
2魔法链接验证用户点击邮件链接后,后端验证链接有效性用户需求
3自动登录验证成功后,后端写入登录态(Cookie)并跳转首页用户需求

1.3 登录流程#

sequenceDiagram participant Frontend as 前端 participant AuthService as 认证服务 participant EmailService as 邮箱服务 participant Redis as Redis缓存 participant Turnstile as Cloudflare Turnstile participant UserService as 用户服务 Frontend->>Turnstile: 完成人机校验(用户点击登录时触发) Turnstile-->>Frontend: 返回turnstileToken Note over Frontend: 用户输入邮箱 Frontend->>AuthService: POST /magic-link/send (email, turnstileToken) AuthService->>Turnstile: 验证 Turnstile Token Turnstile-->>AuthService: 验证结果 alt 人机校验失败 AuthService-->>Frontend: 返回错误(人机校验失败) else 正常 AuthService->>Redis: 缓存魔法链接Token(15分钟) AuthService->>EmailService: 发送魔法链接邮件 EmailService-->>AuthService: 发送成功 AuthService-->>Frontend: 返回成功(限流由@RateLimit注解控制) end Note over Frontend,AuthService: 用户点击邮件链接 Frontend->>AuthService: GET /magic-link/callback?token=xxx AuthService->>Redis: 验证Token有效性 alt Token无效或过期 AuthService-->>Frontend: 重定向到登录页(带错误参数) else Token有效 AuthService->>UserService: 获取用户信息 alt 用户不存在 UserService-->>AuthService: 用户不存在 AuthService->>UserService: 创建新用户 UserService-->>AuthService: 用户创建成功 else 用户存在 UserService-->>AuthService: 返回用户信息 end AuthService->>AuthService: 生成JWT Token AuthService->>Redis: 缓存登录态 AuthService-->>Frontend: 重定向首页(Set-Cookie) end

2. 技术方案#

2.1 架构设计#

2.1.1 模块划分#

模块职责状态
Controller层处理HTTP请求、参数校验、响应封装新增
Service层业务逻辑处理、Token生成验证、登录态管理新增/修改
外部服务Cloudflare Turnstile验证、邮件发送集成
缓存层Token存储、频率限制复用

2.1.2 核心流程图#

flowchart TD A[用户访问登录页] --> B[输入邮箱] B --> C[点击登录按钮] C --> D[前端完成Turnstile校验] D --> E[请求发送魔法链接] E --> F[后端验证Turnstile Token] F --> G{校验通过?} G -->|否| D G -->|是| H[生成魔法Token] H --> I[缓存Token到Redis] I --> J[发送邮件] J --> K[返回成功] L[用户点击邮件链接] --> M[访问回调接口] M --> N[验证Token] N --> O{Token有效?} O -->|否| P[重定向登录页] O -->|是| Q[获取用户信息] Q --> R{用户存在?} R -->|否| S[自动创建用户] S --> T[生成登录Token] R -->|是| T T --> U[缓存登录态] U --> V[Set-Cookie] V --> W[重定向首页]

2.2 目录结构#

zsk-auth/
├── src/main/java/com/zsk/auth/
│ ├── controller/
│ │ └── AuthController.java # 新增魔法链接接口
│ ├── service/
│ │ ├── IAuthService.java # 新增魔法链接相关方法
│ │ ├── ICaptchaService.java # 新增Turnstile验证方法
│ │ └── impl/
│ │ ├── AuthServiceImpl.java # 实现魔法链接业务逻辑
│ │ └── CaptchaServiceImpl.java # 实现Turnstile验证
│ ├── config/
│ │ └── TurnstileProperties.java # Turnstile配置
│ └── domain/
│ └── MagicLinkRequest.java # 魔法链接请求DTO
└── src/main/resources/
└── application.yml # 新增Turnstile配置项

2.3 关键类与方法设计#

2.3.1 Controller层#

方法名功能说明参数返回值所属文件
sendMagicLink发送魔法链接email: 邮箱地址turnstileToken: Turnstile验证TokenR<String>AuthController.java
magicLinkCallback魔法链接回调token: 魔法链接TokenResponseEntity<Void>(重定向)AuthController.java

2.3.2 Service层#

IAuthService 接口新增方法:

方法名功能说明参数返回值
sendMagicLink发送魔法链接email: 邮箱地址turnstileToken: Turnstile验证Tokenvoid
verifyMagicLink验证魔法链接并生成登录态token: 魔法链接TokenLoginResponse

ICaptchaService 接口方法:

方法名功能说明参数返回值
verifyTurnstileToken验证Cloudflare Turnstile Tokentoken: Turnstile验证Tokenboolean

2.3.3 配置类#

TurnstileProperties

属性名类型含义默认值
secretKeyStringCloudflare Turnstile 密钥-
siteKeyStringCloudflare Turnstile 站点密钥-
verifyUrlStringTurnstile验证API地址https://challenges.cloudflare.com/turnstile/v0/siteverify

2.4 数据库与缓存设计#

2.4.1 Redis缓存键设计#

缓存键前缀有效期存储内容
魔法链接Tokencache:magic_link:15分钟email

说明:发送频率限制使用 @RateLimit 注解(基于Sentinel限流)。

2.4.2 缓存数据结构#

// 魔法链接Token缓存
{
"key": "cache:magic_link:xxx-token-xxx",
"value": "user@example.com",
"expire": 900 // 15分钟
}

2.5 API接口设计#

2.5.1 发送魔法链接#

属性
路径/magic-link/send
方法POST
所属文件AuthController.java

请求体:

字段名类型必填含义
emailString用户邮箱地址
turnstileTokenStringCloudflare Turnstile验证Token(前端从Turnstile组件获取)

成功响应(200):

{
"code": 200,
"msg": "success",
"data": "魔法链接已发送至您的邮箱,15分钟内有效"
}

失败响应(400):

{
"code": 400,
"msg": "人机校验失败,请重试",
"data": null
}

2.5.2 魔法链接回调#

属性
路径/magic-link/callback
方法GET
所属文件AuthController.java

请求参数:

字段名类型必填含义
tokenString魔法链接中的Token

成功响应(302):

  • Location: /(首页地址,可配置)
  • Set-Cookie: access_token=xxx; HttpOnly; Secure; SameSite=Strict

失败响应(302):

  • Location: /login?error=invalid_token

3. 方案对比分析#

3.1 两种方案对比#

方案 A:预校验(已废弃)#

流程: 用户进入页面 → 完成Turnstile校验 → 获取临时凭证 → 输入邮箱 → 携带凭证调用登录接口

优点:

  • 登录时无需等待人机校验结果,登录接口响应更快
  • 可以提前拦截恶意流量,不让无效请求到达登录接口

致命缺点:

  • 多一次网络请求:页面加载就调用后端,浪费服务器资源
  • 安全漏洞:临时凭证如果没有严格的过期/防重放设计,攻击者可以批量刷凭证后暴力登录
  • 体验割裂:用户还没打算登录,就被强制完成人机校验
  • 实现复杂:需要管理临时凭证的生命周期(Redis存储、过期时间、单用户限制等)

方案 B:登录时校验(当前实现)#

流程: 用户输入邮箱 → 点击登录 → 前端完成Turnstile校验 → 携带turnstileToken调用登录接口 → 后端实时校验

优点:

  • 极致用户体验:全程静默无感,用户只操作一次登录
  • 最高安全性:登录和人机强绑定,不通过校验就绝对无法进入登录逻辑
  • 架构极简:无额外接口、无额外存储、无凭证管理逻辑
  • 抗攻击最强:每一次登录请求都必须携带全新的有效TurnstileToken,几乎无法批量刷接口

唯一小缺点:

  • 登录接口会多一步校验逻辑(调用Cloudflare API),但Turnstile接口响应极快(毫秒级),几乎无感知

3.2 方案对比表#

维度方案 A(预校验)方案 B(登录时校验)
用户体验一般(多一步请求)优秀(全程无感)
安全性中(存在凭证复用风险)高(强绑定,无法提前准备)
架构复杂度高(需要管理凭证生命周期)低(无额外组件)
服务器开销高(页面加载就请求)低(仅登录时请求)
抗攻击能力中(可批量刷凭证)高(每请求都需新Token)
实现难度复杂简单

3.3 选型结论#

方案 B(登录时校验)是最优选择。

它以几乎可以忽略的性能损耗,换取了:

  • 更简单的架构设计
  • 更高的安全性
  • 更好的用户体验

4. 部署与集成方案#

4.1 依赖与环境#

依赖名称GroupIdArtifactId版本用途
Spring Weborg.springframework.bootspring-boot-starter-web3.2.xWeb服务
Spring Data Redisorg.springframework.bootspring-boot-starter-data-redis3.2.x缓存
RestTemplateorg.springframework.bootspring-boot-starter-web3.2.xHTTP请求
Lombokorg.projectlomboklombok1.18.x简化代码

4.2 配置与运行#

4.2.1 application.yml 新增配置#

# Cloudflare Turnstile 配置
turnstile:
secret-key: ${TURNSTILE_SECRET_KEY:your-secret-key}
site-key: ${TURNSTILE_SITE_KEY:your-site-key}
verify-url: https://challenges.cloudflare.com/turnstile/v0/siteverify
# 魔法链接配置
magic-link:
redirect-url: ${MAGIC_LINK_REDIRECT_URL:http://localhost:8080}

说明

  • expire-minutes: 魔法链接有效期固定为15分钟,无需配置
  • rate-limit: 限流由 @RateLimit 注解控制,无需在此配置

5. 代码安全性#

5.1 注意事项#

序号风险点风险等级关联模块
1Turnstile Token伪造CaptchaServiceImpl
2魔法链接Token暴力破解AuthServiceImpl
3邮箱发送频率攻击AuthController
4邮箱枚举攻击AuthServiceImpl
5Cookie安全配置AuthController
6自动注册用户风险AuthServiceImpl

5.2 解决方案#

序号风险点解决方案
1Turnstile Token伪造调用Cloudflare官方API验证,仅信任服务端验证结果。后端接收到turnstileToken后,立即调用 https://challenges.cloudflare.com/turnstile/v0/siteverify 接口验证Token有效性,验证失败则直接返回错误,不进入后续业务逻辑。验证时需携带配置的secretKey,确保请求来源可信。
2魔法链接Token暴力破解使用UUID生成Token,长度32位,15分钟过期,验证后立即删除。Token存储在Redis中,键为 cache:magic_link:{token},值为用户邮箱。验证流程:1) 根据token查找Redis获取邮箱;2) 验证成功后立即删除缓存(防止重复使用);3) 无论验证成功或失败,都不泄露任何关于Token是否存在的信息。
3邮箱发送频率攻击使用 @RateLimit 注解(基于Sentinel)限制同一邮箱3分钟内最多调用3次。限流策略:以邮箱地址为key,时间窗口3分钟,阈值3次。超过阈值时返回限流错误,防止恶意用户批量发送邮件。
4邮箱枚举攻击支持自动注册,用户不存在时自动创建,无需区分响应。后端验证魔法链接时,先查询用户是否存在,若不存在则自动创建新用户。返回结果统一,不区分”用户不存在”和”链接无效”,避免攻击者通过响应差异枚举有效邮箱。
5Cookie安全配置设置HttpOnly、Secure、SameSite=Strict属性。HttpOnly防止JavaScript访问Cookie,降低XSS攻击风险;Secure确保Cookie仅通过HTTPS传输;SameSite=Strict限制Cookie仅在同站请求时发送,防止CSRF攻击。Cookie有效期与Token保持一致。
6日志敏感信息泄露禁止打印邮箱地址、Token等敏感信息。在日志配置中过滤敏感字段,使用占位符或脱敏处理。禁止在异常堆栈或调试信息中暴露用户凭证。
7自动注册用户风险新用户默认状态为正常,用户类型为普通用户(1001)。自动创建用户时,用户名取邮箱@前部分,昵称与用户名相同,邮箱为用户输入的邮箱地址。新用户权限为最低级别,仅拥有基础访问权限。

5.3 前端操作流程#

5.3.1 发送魔法链接#

步骤1:初始化Turnstile组件

前端页面加载时,初始化Cloudflare Turnstile组件:

<!-- 登录页面嵌入Turnstile -->
<div
class="cf-turnstile"
data-sitekey="your-site-key"
data-callback="onTurnstileSuccess"
></div>

步骤2:用户输入邮箱并点击登录

用户输入邮箱后点击登录按钮,触发Turnstile校验:

// 前端发送魔法链接请求
import { sendMagicLink } from '@/api/auth'
const handleSendMagicLink = async (email: string) => {
// 等待Turnstile校验完成获取token
const turnstileToken = await getTurnstileToken()
// 调用后端接口
await sendMagicLink({
email,
turnstileToken
})
}

步骤3:处理响应

后端返回成功后,提示用户检查邮箱;校验失败则提示用户重试。

5.3.2 魔法链接回调处理#

步骤1:用户点击邮件链接

邮件中的链接格式:https://your-domain/magic-link/callback?token=xxx

步骤2:后端验证并重定向

后端验证Token成功后,设置Cookie并重定向到首页:

  • Set-Cookie: access_token=xxx; HttpOnly; Secure; SameSite=Strict
  • Location: /

5.3.3 通过Cookie获取UserInfo#

步骤1:应用初始化时检查Cookie

前端应用启动时,从Cookie读取access_token

src/App.tsx
import { useEffect } from 'react'
import { useUserStore } from '@/stores/user'
import { getCurrentUser } from '@/api/auth'
import { getStorageValue, STORAGE_KEYS } from '@/utils/storage'
useEffect(() => {
const initUser = async () => {
// 从Cookie读取access_token
const token = getStorageValue<string>(STORAGE_KEYS.TOKEN, undefined, 'cookie')
if (token && !userInfo) {
// 调用接口获取用户信息
const user = await getCurrentUser()
if (user) {
setUserInfo(user)
}
}
}
initUser()
}, [])

步骤2:请求拦截器自动携带Token

Axios请求拦截器自动从Cookie读取Token并添加到请求头:

src/api/request.ts
request.interceptors.request.use((config) => {
const token = getStorageValue<string>(STORAGE_KEYS.TOKEN, undefined, 'cookie')
if (token && config.withToken !== false) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})

步骤3:获取用户信息接口

调用/system/user/current接口获取当前登录用户信息:

src/api/auth.ts
export function getCurrentUser() {
return get<UserInfo>('/system/user/current')
}

步骤4:响应拦截器处理Token过期

当返回401状态码时,清除Cookie并跳转到登录页:

src/api/request.ts
request.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// 清除Cookie和本地存储
removeStorage(STORAGE_KEYS.TOKEN, 'cookie')
removeStorage(STORAGE_KEYS.USER_INFO, 'local')
// 跳转到登录页
window.location.href = '/login'
}
return Promise.reject(error)
}
)

5.3.4 Cookie操作工具函数#

前端使用js-cookie库封装Cookie操作:

src/utils/storage.ts
import Cookies from 'js-cookie'
export function getStorageValue<T>(
key: string,
defaultValue?: T,
type: 'local' | 'session' | 'cookie' = 'local'
): T | undefined {
if (type === 'cookie') {
const item = Cookies.get(key)
if (item === undefined) return defaultValue
try {
return JSON.parse(item) as T
} catch {
return item as unknown as T
}
}
// ... localStorage/sessionStorage 处理
}
export const STORAGE_KEYS = {
TOKEN: 'access_token', // 与后端设置的Cookie名称一致
USER_INFO: 'zsk_user_info',
// ... 其他键名
} as const

5.4 自动注册用户字段说明#

当用户通过魔法链接登录且不存在时,系统会自动创建用户,字段默认值如下:

字段名默认值说明
userName邮箱@前部分user@example.comuser
nickName同userName昵称与用户名相同
email用户输入的邮箱用于后续登录和通知
status0正常状态
userType1001普通注册用户
登录 | 魔法链接 | 设计文档
https://tblog.mmzhiku.xyz/posts/projects/projects-magic-link-login-design/
作者
MmzMing
发布于
2026-05-01
许可协议
CC BY-NC-SA 4.0

评论区

看板娘
公告
友链 互换友链

正在招募技术类博客友链,要求原创、稳定更新。点击了解更多。

查看详情
维护 服务器升级

本周日凌晨 2:00-4:00 进行服务器维护,期间站点可能短暂无法访问。

欢迎 关于我的介绍

欢迎来到我的博客,我是深耕java、python和react技术开发。热爱技术、持续学习,欢迎同好交流探讨,也欢迎大佬互换友链。

查看详情
音乐
封面

音乐

暂未播放

0:00
0:00
暂无歌词
标签
# AI 6 # 认证 5 # 安全 4 # 登录 3 # Skill 2 # Redis 2 # Bitmap 2 # 部署 2 # Java 2 # 并发编程 2 # 性能优化 2 # 前端 1 # 博客 1 # Prompt 1 # 工作流 1 # RAG 1 # Cloudflare 1 # 缓存设计 1 # 高性能 1 # Bot 1 # Umami 1 # Vercel 1 # 线程池 1 # 虚拟线程 1 # 分布式 1 # JWT 1 # OAuth2 1 # MinIO 1 # 文件存储 1 # 扫码登录 1 # WebSocket 1 # Agent 1 # Oracle 1 # 数据库 1
目录

隐私政策

更新日期: 2026/5/19
生效日期: 2026/5/19

导言#

MmzMing的知识库 是一款由 MmzMing(以下简称“我们”)提供的产品。您在使用我们的服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明,在使用我们的服务时,我们如何收集、使用、储存和分享这些信息,以及我们为您提供的访问、更新、控制和保护这些信息的方式。

本《隐私政策》与您所使用的 MmzMing的知识库 服务息息相关,希望您仔细阅读,在需要时,按照本《隐私政策》的指引,作出您认为适当的选择。本《隐私政策》中涉及的相关技术词汇,我们尽量以简明扼要的表述,并提供进一步说明的链接,以便您的理解。

您使用或继续使用我们的服务,即意味着同意我们按照本《隐私政策》收集、使用、储存和分享您的相关信息。

如对本《隐私政策》或相关事宜有任何问题,请通过 784774835@qq.com 与我们联系。

1. 我们收集的信息#

我们或我们的第三方合作伙伴提供服务时,可能会收集、储存和使用下列与您有关的信息。如果您不提供相关信息,可能无法注册成为我们的用户或无法享受我们提供的某些服务,或者无法达到相关服务拟达到的效果。

  • 个人信息:您在注册账户或使用我们的服务时,向我们提供的相关个人信息,例如电话号码、电子邮件等。
  • 日志信息:指您使用我们的服务时,系统可能通过 cookies、标识符及相关技术收集的信息,包括您的 设备信息浏览信息点击信息,并将该等信息储存为日志信息,为您提供个性化的用户体验、保障服务安全。您可以通过浏览器设置拒绝或管理 cookie、标识符或相关技术的使用。
  • 位置信息:指您开启设备定位功能并使用我们基于位置提供的相关服务时,收集的有关您位置的信息,包括:
    • 您通过具有定位功能的移动设备使用我们的服务时,通过 GPS 或 WiFi 等方式收集的您的地理位置信息;
    • 您可以通过关闭定位功能,停止对您的地理位置信息的收集。

2. 信息的存储#

2.1 信息存储的方式和期限#

我们会通过安全的方式存储您的信息,包括本地存储(例如利用 APP 进行数据缓存)、数据库和服务器日志。

一般情况下,我们只会在为实现服务目的所必需的时间内或法律法规规定的条件下存储您的个人信息。

2.2 信息存储的地域#

我们会按照法律法规规定,将境内收集的用户个人信息存储于中国境内。

目前我们不会跨境传输或存储您的个人信息。将来如需跨境传输或存储的,我们会向您告知信息出境的目的、接收方、安全保证措施和安全风险,并征得您的同意。

2.3 产品或服务停止运营时的通知#

当我们的产品或服务发生停止运营的情况时,我们将以推送通知、公告等形式通知您,并在合理期限内删除您的个人信息或进行匿名化处理,法律法规另有规定的除外。

3. 信息安全#

我们使用各种安全技术和程序,以防信息的丢失、不当使用、未经授权阅览或披露。例如,在某些服务中,我们将利用加密技术(例如 SSL)来保护您提供的个人信息。但请您理解,由于技术的限制以及可能存在的各种恶意手段,在互联网行业,即便竭尽所能加强安全措施,也不可能始终保证信息百分之百的安全。您需要了解,您接入我们的服务所用的系统和通讯网络,有可能因我们可控范围外的因素而出现问题。

4. 我们如何使用信息#

我们可能将在向您提供服务的过程之中所收集的信息用作下列用途:

  • 向您提供服务;
  • 在我们提供服务时,用于身份验证、客户服务、安全防范、诈骗监测、存档和备份用途,确保我们向您提供的产品和服务的安全性;
  • 帮助我们设计新服务,改善我们现有服务;
  • 使我们更加了解您如何接入和使用我们的服务,从而针对性地回应您的个性化需求,例如语言设定、位置设定、个性化的帮助服务和指示,或对您和其他用户作出其他方面的回应;
  • 向您提供与您更加相关的广告以替代普遍投放的广告;
  • 评估我们服务中的广告和其他促销及推广活动的效果,并加以改善;
  • 软件认证或管理软件升级;
  • 让您参与有关我们产品和服务的调查。

5. 信息共享#

目前,我们不会主动共享或转让您的个人信息至第三方,如存在其他共享或转让您的个人信息或您需要我们将您的个人信息共享或转让至第三方情形时,我们会直接或确认第三方征得您对上述行为的明示同意。

为了投放广告,评估、优化广告投放效果等目的,我们需要向广告主及其代理商等第三方合作伙伴共享您的部分数据,要求其严格遵守我们关于数据隐私保护的措施与要求,包括但不限于根据数据保护协议、承诺书及相关数据处理政策进行处理,避免识别出个人身份,保障隐私安全。

我们不会向合作伙伴分享可用于识别您个人身份的信息(例如您的姓名或电子邮件地址),除非您明确授权。

我们不会对外公开披露所收集的个人信息,如必须公开披露时,我们会向您告知此次公开披露的目的、披露信息的类型及可能涉及的敏感信息,并征得您的明示同意。

随着我们业务的持续发展,我们有可能进行合并、收购、资产转让等交易,我们将告知您相关情形,按照法律法规及不低于本《隐私政策》所要求的标准继续保护或要求新的控制者继续保护您的个人信息。

另外,根据相关法律法规及国家标准,以下情形中,我们可能会共享、转让、公开披露个人信息无需事先征得您的授权同意:

  • 与国家安全、国防安全直接相关的;
  • 与公共安全、公共卫生、重大公共利益直接相关的;
  • 犯罪侦查、起诉、审判和判决执行等直接相关的;
  • 出于维护个人信息主体或其他个人的生命、财产等重大合法权益但又很难得到本人同意的;
  • 个人信息主体自行向社会公众公开个人信息的;
  • 从合法公开披露的信息中收集个人信息的,如合法的新闻报道、政府信息公开等渠道。

6. 您的权利#

在您使用我们的服务期间,我们可能会视产品具体情况为您提供相应的操作设置,以便您可以查询、删除、更正或撤回您的相关个人信息,您可参考相应的具体指引进行操作。此外,我们还设置了投诉举报渠道,您的意见将会得到及时的处理。如果您无法通过上述途径和方式行使您的个人信息主体权利,您可以通过本《隐私政策》中提供的联系方式提出您的请求,我们会按照法律法规的规定予以反馈。

当您决定不再使用我们的产品或服务时,可以申请注销账户。注销账户后,除法律法规另有规定外,我们将删除或匿名化处理您的个人信息。

7. 变更#

我们可能适时修订本《隐私政策》的条款。当变更发生时,我们会在版本更新时向您提示新的《隐私政策》,并向您说明生效日期。请您仔细阅读变更后的《隐私政策》内容,若您继续使用我们的服务,即表示您同意我们按照更新后的《隐私政策》处理您的个人信息。

8. 未成年人保护#

我们鼓励父母或监护人指导未满十八岁的未成年人使用我们的服务。我们建议未成年人鼓励他们的父母或监护人阅读本《隐私政策》,并建议未成年人在提交的个人信息之前寻求父母或监护人的同意和指导。