a5e25b444f
实现 server/internal/auth 控制面鉴权底座,对应 doc/02 §4.1、doc/06 §3:
- POST /v1/auth/code:IP+邮箱双维度 Redis 滑窗限频(同邮箱 1/min、同 IP 10/h)
+ 一次性邮箱域黑名单(embed 词表)→ 6 位数字码写 auth:code:{email} TTL 10min
→ 异步发信(SMTP / 开发态 log mailer)。
- POST /v1/auth/register:验码(一次性,删 key;超次数烧码)→ 单事务建号
(uuid + dp_uuid 应用层生成、argon2id)+ 7 天 PRO 试用(source='trial')→ 签发 JWT。
- POST /v1/auth/login:argon2id 校验(失败恒定时、未知用户走 dummy hash);
失败计数限流 rl:login:{email} + 锁定;banned 拒绝。
- POST /v1/auth/refresh:RS256,access 15min + refresh 30d 落 Redis 白名单
jwt:refresh:{jti},旋转时删旧写新;JWT 头带 kid,验证端接受新旧公钥支持轮换。
- RequireAuth 中间件:解析 Bearer access,注入 user id(复用 codes.CtxKeyUserID
避免循环依赖)+ uuid + claims 到 context。
- 错误体统一走 internal/apierr 的 {code, message_zh, message_en},文案双语脱敏。
- 文件拆分:handler/service/password/token/ratelimit/emailcheck/store/mailer/
middleware/errors/keyloader;config 增加可选 JWT PEM 路径/kid 加载。
测试:password/token/ratelimit/emailcheck/service/handler/middleware 单测
(miniredis,含注册全流程、重复邮箱 409、验证码错误/过期/复用/烧码、限频 429
带 Retry-After、登录失败锁定、banned 拒绝、refresh 旋转后旧 token 失效),
go test -race 通过;集成测试 integration_test.go(testcontainers MySQL8+Redis,
register→login→refresh→受保护接口全链路,构建 tag integration)与 codes 模块同构。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
87 lines
3.2 KiB
Go
87 lines
3.2 KiB
Go
package auth
|
|
|
|
import "github.com/wangjia/pangolin/server/internal/apierr"
|
|
|
|
// Auth-module error values. Codes use the dotted `auth.*` namespace documented
|
|
// in the OpenAPI contract (components.schemas.Error). All messages are bilingual
|
|
// and desensitised (no destination / traffic wording, no enumeration leaks).
|
|
var (
|
|
// ErrInvalidRequest — malformed body or failed field validation.
|
|
ErrInvalidRequest = &apierr.Error{
|
|
Code: "auth.invalid_request",
|
|
MessageZH: "请求参数有误,请检查后重试",
|
|
MessageEn: "Invalid request parameters, please verify and try again",
|
|
}
|
|
|
|
// ErrEmailDisposable — the email domain is on the disposable-domain blocklist.
|
|
ErrEmailDisposable = &apierr.Error{
|
|
Code: "auth.email_disposable",
|
|
MessageZH: "暂不支持该邮箱服务商,请更换邮箱",
|
|
MessageEn: "This email provider is not supported, please use another address",
|
|
}
|
|
|
|
// ErrCodeInvalid — verification code wrong, expired, or already used.
|
|
// Intentionally generic to avoid distinguishing the three cases.
|
|
ErrCodeInvalid = &apierr.Error{
|
|
Code: "auth.code_invalid",
|
|
MessageZH: "验证码无效或已过期,请重新获取",
|
|
MessageEn: "Verification code is invalid or expired, please request a new one",
|
|
}
|
|
|
|
// ErrEmailExists — the email is already registered.
|
|
ErrEmailExists = &apierr.Error{
|
|
Code: "auth.email_exists",
|
|
MessageZH: "该邮箱已注册,请直接登录",
|
|
MessageEn: "This email is already registered, please sign in",
|
|
}
|
|
|
|
// ErrRateLimited — rate-limit window exceeded. Carries a Retry-After header.
|
|
ErrRateLimited = &apierr.Error{
|
|
Code: "auth.rate_limited",
|
|
MessageZH: "操作过于频繁,请稍后再试",
|
|
MessageEn: "Too many attempts, please try again later",
|
|
}
|
|
|
|
// ErrInvalidCredentials — wrong email or password (login).
|
|
ErrInvalidCredentials = &apierr.Error{
|
|
Code: "auth.invalid_credentials",
|
|
MessageZH: "邮箱或密码不正确",
|
|
MessageEn: "Incorrect email or password",
|
|
}
|
|
|
|
// ErrAccountLocked — too many failed logins; temporarily locked.
|
|
ErrAccountLocked = &apierr.Error{
|
|
Code: "auth.account_locked",
|
|
MessageZH: "登录失败次数过多,账户已临时锁定,请稍后再试",
|
|
MessageEn: "Too many failed sign-in attempts, account temporarily locked",
|
|
}
|
|
|
|
// ErrAccountBanned — the account has been disabled.
|
|
ErrAccountBanned = &apierr.Error{
|
|
Code: "auth.account_banned",
|
|
MessageZH: "该账户已被停用",
|
|
MessageEn: "This account has been disabled",
|
|
}
|
|
|
|
// ErrInvalidToken — refresh token absent from whitelist, malformed, or expired.
|
|
ErrInvalidToken = &apierr.Error{
|
|
Code: "auth.invalid_token",
|
|
MessageZH: "登录已失效,请重新登录",
|
|
MessageEn: "Your session has expired, please sign in again",
|
|
}
|
|
|
|
// ErrUnauthorized — missing or invalid bearer access token (middleware).
|
|
ErrUnauthorized = &apierr.Error{
|
|
Code: "auth.unauthorized",
|
|
MessageZH: "请先登录",
|
|
MessageEn: "Authentication required",
|
|
}
|
|
|
|
// ErrInternal — unexpected server-side failure.
|
|
ErrInternal = &apierr.Error{
|
|
Code: "auth.internal",
|
|
MessageZH: "服务器内部错误,请稍后重试",
|
|
MessageEn: "Internal server error, please try again later",
|
|
}
|
|
)
|