第 02 章

认证与鉴权

手机号注册 / 登录(密码、验证码双模式)、JWT 令牌机制、拦截规则、401 行为与登出。

2.1 认证机制总览

  1. 登录成功签发 Token 服务端以 JwtUtil(JJWT 0.11.5,HS256)签发 JWT,密钥来自 app.jwt.secret(支持 JWT_SECRET 环境变量覆盖,要求 ≥32 字节)。
  2. 写入 Redis 会话 Token 与用户映射存入 Redis:auth:token:{token} → User;登出即删除该键。
  3. 下发双通道凭证 响应体返回 token 与 expiresIn,同时通过 Set-Cookie: auth_token={token} 下发 Cookie,浏览器端免手动携带。
  4. 请求拦截鉴权 AuthTokenInterceptor 对 /api/** 生效:先取 Authorization: Bearer {token},缺失时回退 auth_token Cookie。
  5. 上下文注入与清理 校验通过后将用户写入 UserContext(ThreadLocal),请求结束(afterCompletion)自动清理。
拦截器校验规则
1. 取 Token:Authorization: Bearer {token} 优先,其次 auth_token Cookie
2. JWT 解析(JwtUtil.parse + isExpired)
3. Redis 查询 auth:token:{token} 获取用户
4. 失败/过期/不存在 → 401,UserContext 不注入
401 未认证响应(原始 JSON,非统一包装结构)
// Content-Type: application/json
{
  "code": 401,
  "message": "Missing token"      // 或 "Token expired" / "Session expired" / "Invalid token"
}

⚠️ 注意:鉴权拦截器返回的 401 是裸 JSON(不含 success/data/error 字段), 与业务层统一包装结构不同。客户端统一处理时建议对 HTTP 状态码 401 直接判定为未登录。

免认证白名单(完整列表)

以下路径不经过登录校验,其余 /api/** 一律要求有效 Token:

/api/auth/** /api/payment/** /api/membership/order /api/membership/order/** /api/translation/platform-stats /static/** /css/** /js/** /images/** /webjars/** /favicon.ico /error

集成方须知:支付回调(/api/payment/notify/**)与会员下单均为免认证接口, 部署到公网时应通过网关 / 防火墙策略保护下单接口,回调接口依赖签名校验保证安全。

2.2 接口总览(前缀 /api/auth)

POST/api/auth/register/phone手机号 + 密码注册
POST/api/auth/register/phone-code手机号 + 验证码注册
POST/api/auth/code/register发送注册验证码
POST/api/auth/login/phone手机号 + 密码登录
POST/api/auth/login/phone-code验证码登录(可附加密码双校验)
POST/api/auth/code/phone发送登录验证码
POST/api/auth/logout退出登录(删除 Redis 会话 + 清除 Cookie)

2.3 接口详情

POST

/api/auth/register/phone

手机号 + 密码注册新账号。手机号唯一,重复注册报错。

请求体(application/json)
字段类型必填校验 / 说明
phoneString必填正则 ^\+?[0-9]{6,20}$,支持国际区号前缀 +
passwordString必填长度 6–64
nicknameString可选最长 100 字符
响应 data(AuthUserResponse)
{
  "id": 1, "username": "13800138000", "phone": "13800138000",
  "nickname": "小明", "avatarUrl": null,
  "role": "USER", "status": "ACTIVE"
}
POST

/api/auth/register/phone-code

手机号 + 验证码注册。先调用「发送注册验证码」获取短信验证码。

请求体
字段类型必填校验 / 说明
phoneString必填同上正则
passwordString必填长度 6–64
codeString必填正则 ^[0-9]{4,8}$(4–8 位数字)
⚠️ 部署须知(验证码校验现状)

当前版本中 login/phone-code 与 register/phone-code 的验证码比对逻辑暂被注释(源码 TODO:待接入阿里云短信服务后恢复), 且验证码通过后端日志打印(开发演示模式)。生产交付时必须恢复校验逻辑并接入真实短信通道,详见随附 README「短信验证码接入指引」。

POST

/api/auth/code/register

发送注册验证码。6 位数字,有效期 300 秒,发送冷却 60 秒。

请求体
字段类型必填说明
phoneString必填手机号
响应 data(SendCodeResponse)
{
  "sent": true,
  "cooldownSeconds": 60,        // 发送冷却
  "expiresInSeconds": 300,      // 验证码有效期
  "message": "注册验证码已发送"   // 冷却中为 "发送过于频繁,请稍后再试"
}
POST

/api/auth/login/phone

手机号 + 密码登录。

请求体
字段类型必填校验
phoneString必填手机号正则
passwordString必填长度 6–64
响应 data(LoginResponse)
{
  "user": { "id": 1, "username": "13800138000", "phone": "13800138000", "nickname": "小明", ... },
  "token": "eyJhbGciOiJIUzI1NiJ9...",
  "expiresIn": 86400
}
POST

/api/auth/login/phone-code

验证码登录,支持两种模式:仅验证码;或同时携带密码做双重校验。

请求体
字段类型必填校验 / 说明
phoneString必填手机号正则
codeString必填正则 ^[0-9]{4,8}$
passwordString可选长度 6–64;传入时走「验证码 + 密码」双校验(loginByPhone),不传则仅验证码登录

响应同 login/phone(LoginResponse)。

POST

/api/auth/code/phone

发送登录验证码。规则与注册验证码一致(6 位 / 300 秒有效 / 60 秒冷却)。

请求体
字段类型必填说明
phoneString必填手机号
响应 data(SendCodeResponse)
{
  "sent": true,
  "cooldownSeconds": 60,
  "expiresInSeconds": 300,
  "message": "验证码已发送"   // 冷却中为 "发送过于频繁,请稍后再试"
}
POST

/api/auth/logout

退出登录:尽力删除 Redis 中的 auth:token:{token} 会话,并通过 Set-Cookie: auth_token=; Path=/; Max-Age=0 清除浏览器 Cookie。

请求体

无请求体(携带当前 Token 即可)。

响应 data(LogoutResponse)
{ "success": true, "message": "已退出登录" }

登出为幂等操作:Token 已失效时同样返回成功。

2.4 完整调用示例

# 1. 注册
curl -X POST http://localhost:8090/api/auth/register/phone \
  -H "Content-Type: application/json" \
  -d '{"phone":"13800138000","password":"pass123456","nickname":"小明"}'

# 2. 登录,取得 token
TOKEN=$(curl -s -X POST http://localhost:8090/api/auth/login/phone \
  -H "Content-Type: application/json" \
  -d '{"phone":"13800138000","password":"pass123456"}' \
  | jq -r '.data.token')

# 3. 携带 token 调用业务接口
curl http://localhost:8090/api/translation/supported-languages \
  -H "Authorization: Bearer $TOKEN"

# 4. 未携带 token 的响应示例(HTTP 401)
curl -i http://localhost:8090/api/user/profile
# HTTP/1.1 401
# {"code":401,"message":"Missing token"}
const BASE = 'http://localhost:8090';

// 登录并保存 token
const loginResp = await fetch(`${BASE}/api/auth/login/phone`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ phone: '13800138000', password: 'pass123456' })
}).then(r => r.json());

const token = loginResp.data.token;

// 后续请求统一携带 Authorization 头
const profile = await fetch(`${BASE}/api/user/profile`, {
  headers: { 'Authorization': `Bearer ${token}` }
}).then(r => r.json());

if (!profile.success) {
  throw new Error(profile.message);
}
console.log(profile.data);