feat: 注册流程强制图形验证码

SSR 注册页与 /api/register 共用 CaptchaService,防刷同时保留换一张无 JS 刷新。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-29 21:13:24 +08:00
parent 5e623f67fb
commit 54f5de07a4
12 changed files with 111 additions and 6 deletions

View File

@@ -237,11 +237,17 @@ func (h *Handlers) APIRegister(c *gin.Context) {
Nickname string `json:"nickname" form:"nickname"`
Email string `json:"email" form:"email" binding:"required"`
EmailCode string `json:"email_code" form:"email_code"`
CaptchaID string `json:"captcha_id" form:"captcha_id"`
Captcha string `json:"captcha" form:"captcha"`
}
if err := c.ShouldBind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
if h.Captcha == nil || !h.Captcha.Verify(req.CaptchaID, req.Captcha) {
c.JSON(http.StatusBadRequest, gin.H{"error": "验证码错误或已过期"})
return
}
userCount := h.Auth.UserCount()
mailReady := h.Settings.MailReady()

View File

@@ -107,6 +107,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
Mail: mailSvc,
SitePage: sitePageSvc,
Report: reportSvc,
Captcha: captchaSvc,
}, authMW)
r.GET("/media/thumb/*filepath", h.ServeImageThumb)
@@ -114,6 +115,9 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
r.GET("/health", h.APIHealth)
r.GET("/robots.txt", h.RobotsTxt)
r.GET("/sitemap.xml", h.SitemapXML)
// 机器注册辅助(与 SSR 注册共用 CaptchaService
r.GET("/api/captcha", h.APICaptcha)
r.POST("/api/register", h.APIRegister)
// OIDC Provider外部机器 / Gitea SSO
r.GET("/.well-known/openid-configuration", h.OIDCDiscovery)

View File

@@ -1,6 +1,7 @@
package web
import (
"html/template"
"net/http"
"strings"
@@ -22,6 +23,8 @@ type registerData struct {
Email string
MailReady bool
RequireEmailCode bool
CaptchaID string
CaptchaSVG template.HTML
}
type registerForm struct {
@@ -151,6 +154,12 @@ func (d Deps) RegisterPost(c *gin.Context) {
d.renderRegister(ctx, "操作过于频繁,请稍后再试", form)
return
}
captchaID := strings.TrimSpace(c.PostForm("captcha_id"))
captchaAns := strings.TrimSpace(c.PostForm("captcha"))
if d.Captcha == nil || !d.Captcha.Verify(captchaID, captchaAns) {
d.renderRegister(ctx, "验证码错误或已过期", form)
return
}
mailReady := d.Settings.MailReady()
if mailReady {
code := strings.TrimSpace(c.PostForm("email_code"))
@@ -197,14 +206,21 @@ func (d Deps) renderRegister(ctx *webctx.Context, errMsg string, form registerFo
func (d Deps) renderRegisterWithChrome(ctx *webctx.Context, chrome PageChrome, errMsg string, form registerForm) {
chrome.Error = errMsg
mailReady := d.Settings.MailReady()
ctx.HTML(http.StatusOK, "auth/register", registerData{
data := registerData{
PageChrome: chrome,
Username: form.Username,
Nickname: form.Nickname,
Email: form.Email,
MailReady: mailReady,
RequireEmailCode: mailReady,
})
}
if d.Captcha != nil {
if id, svg, err := d.Captcha.Generate(); err == nil {
data.CaptchaID = id
data.CaptchaSVG = template.HTML(svg)
}
}
ctx.HTML(http.StatusOK, "auth/register", data)
}
type forgotPasswordData struct {

View File

@@ -28,6 +28,7 @@ type Deps struct {
Mail *services.MailService
SitePage *services.SitePageService
Report *services.ReportService
Captcha *services.CaptchaService
}
// SitePageLink 导航/页脚站点单页链接