验证码

This commit is contained in:
xmdhs 2023-09-25 01:04:11 +08:00
parent f0e9045e98
commit 7376cffe61
No known key found for this signature in database
GPG Key ID: E809D6D43DEFCC95
11 changed files with 134 additions and 32 deletions

View File

@ -27,5 +27,6 @@ type Config struct {
Captcha struct { Captcha struct {
Type string Type string
SiteKey string SiteKey string
Secret string
} }
} }

View File

@ -15,18 +15,19 @@ export async function login(username: string, password: string) {
return data as tokenData return data as tokenData
} }
export async function register(email: string, username: string, password: string) { export async function register(email: string, username: string, password: string, captchaToken: string) {
const v = await fetch(import.meta.env.VITE_APIADDR + "/api/v1/user/reg", { const v = await fetch(import.meta.env.VITE_APIADDR + "/api/v1/user/reg", {
method: "POST", method: "PUT",
body: JSON.stringify({ body: JSON.stringify({
"Email": email, "Email": email,
"Password": password, "Password": password,
"Name": username "Name": username,
"CaptchaToken": captchaToken
}) })
}) })
const data = await v.json() const data = await v.json()
if (!v.ok) { if (!v.ok) {
throw data throw data.msg
} }
return return
} }

View File

@ -11,7 +11,7 @@ interface prop {
onSuccess: ((token: string) => void) onSuccess: ((token: string) => void)
} }
const TurnstileWidget = memo(({ onSuccess }: prop) => { function CaptchaWidget({ onSuccess }: prop) {
const ref = useRef<TurnstileInstance>(null) const ref = useRef<TurnstileInstance>(null)
const [key, setKey] = useState(1) const [key, setKey] = useState(1)
const { data, error, isLoading } = useSWR<ApiCaptcha>(import.meta.env.VITE_APIADDR + '/api/v1/captcha') const { data, error, isLoading } = useSWR<ApiCaptcha>(import.meta.env.VITE_APIADDR + '/api/v1/captcha')
@ -21,18 +21,24 @@ const TurnstileWidget = memo(({ onSuccess }: prop) => {
return <Alert severity="warning">{String(error)}</Alert> return <Alert severity="warning">{String(error)}</Alert>
} }
if (isLoading) { if (isLoading) {
return <Skeleton variant="rectangular" width={210} height={118} /> return <Skeleton variant="rectangular" width={300} height={65} />
} }
if (data?.code != 0) { if (data?.code != 0) {
console.warn(error) console.warn(error)
return <Alert severity="warning">{String(data?.msg)}</Alert> return <Alert severity="warning">{String(data?.msg)}</Alert>
} }
if (data.data.type != "turnstile") {
onSuccess("ok")
return <></>
}
return ( return (
<> <>
<Turnstile siteKey={data?.data.siteKey ?? ""} key={key} onSuccess={onSuccess} ref={ref} scriptOptions={{ async: true }} /> <Turnstile siteKey={data?.data.siteKey ?? ""} key={key} onSuccess={onSuccess} ref={ref} scriptOptions={{ async: true }} />
<Button onClick={() => setKey(key + 1)}></Button> <Button onClick={() => setKey(key + 1)}></Button>
</> </>
) )
}) }
export default TurnstileWidget const CaptchaWidgetMemo = memo(CaptchaWidget)
export default CaptchaWidgetMemo

View File

@ -16,12 +16,13 @@ import Alert from '@mui/material/Alert';
import Snackbar from '@mui/material/Snackbar'; import Snackbar from '@mui/material/Snackbar';
import Loading from '@/components/Loading' import Loading from '@/components/Loading'
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import TurnstileWidget from '@/components/TurnstileWidget'; import CaptchaWidget from '@/components/CaptchaWidget';
export default function SignUp() { export default function SignUp() {
const [regErr, setRegErr] = useState(""); const [regErr, setRegErr] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const [captchaToken, setCaptchaToken] = useState("");
const checkList = React.useRef<Map<string, refType>>(new Map<string, refType>()) const checkList = React.useRef<Map<string, refType>>(new Map<string, refType>())
@ -40,7 +41,11 @@ export default function SignUp() {
setLoading(false) setLoading(false)
return return
} }
register(d.email ?? "", d.username ?? "", d.password ?? ""). if (captchaToken == "") {
setLoading(false)
setRegErr("验证码无效")
}
register(d.email ?? "", d.username ?? "", d.password ?? "", captchaToken).
then(() => navigate("/login")). then(() => navigate("/login")).
catch(v => [setRegErr(String(v)), console.warn(v)]). catch(v => [setRegErr(String(v)), console.warn(v)]).
finally(() => setLoading(false)) finally(() => setLoading(false))
@ -121,7 +126,7 @@ export default function SignUp() {
/> />
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<TurnstileWidget onSuccess={v => console.log(v)} /> <CaptchaWidget onSuccess={setCaptchaToken} />
</Grid> </Grid>
</Grid> </Grid>
<Button <Button

View File

@ -1,6 +1,7 @@
package handle package handle
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@ -17,19 +18,26 @@ func (h *Handel) Reg() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
ctx := r.Context() ctx := r.Context()
ip, err := utils.GetIP(r, h.config.RaelIP)
if err != nil {
h.logger.InfoContext(ctx, err.Error())
handleError(ctx, w, err.Error(), model.ErrInput, 400)
return
}
u, err := utils.DeCodeBody[model.User](r.Body, h.validate) u, err := utils.DeCodeBody[model.User](r.Body, h.validate)
if err != nil { if err != nil {
h.logger.InfoContext(ctx, err.Error()) h.logger.InfoContext(ctx, err.Error())
handleError(ctx, w, err.Error(), model.ErrInput, 400) handleError(ctx, w, err.Error(), model.ErrInput, 400)
return return
} }
rip, err := getPrefix(r, h.config.RaelIP) rip, err := getPrefix(ip)
if err != nil { if err != nil {
h.logger.WarnContext(ctx, err.Error()) h.logger.WarnContext(ctx, err.Error())
handleError(ctx, w, err.Error(), model.ErrUnknown, 500) handleError(ctx, w, err.Error(), model.ErrUnknown, 500)
return return
} }
err = h.webService.Reg(ctx, u, rip) err = h.webService.Reg(ctx, u, rip, ip)
if err != nil { if err != nil {
if errors.Is(err, service.ErrExistUser) { if errors.Is(err, service.ErrExistUser) {
h.logger.DebugContext(ctx, err.Error()) h.logger.DebugContext(ctx, err.Error())
@ -45,14 +53,16 @@ func (h *Handel) Reg() httprouter.Handle {
handleError(ctx, w, err.Error(), model.ErrService, 500) handleError(ctx, w, err.Error(), model.ErrService, 500)
return return
} }
json.NewEncoder(w).Encode(model.API[any]{
Code: 0,
Data: nil,
Msg: "",
})
} }
} }
func getPrefix(r *http.Request, fromHeader bool) (string, error) { func getPrefix(ip string) (string, error) {
ip, err := utils.GetIP(r, fromHeader)
if err != nil {
return "", fmt.Errorf("getPrefix: %w", err)
}
ipa, err := netip.ParseAddr(ip) ipa, err := netip.ParseAddr(ip)
if err != nil { if err != nil {
return "", fmt.Errorf("getPrefix: %w", err) return "", fmt.Errorf("getPrefix: %w", err)

View File

@ -12,6 +12,7 @@ type User struct {
Email string `validate:"required,email"` Email string `validate:"required,email"`
Password string `validate:"required,min=6,max=50"` Password string `validate:"required,min=6,max=50"`
Name string `validate:"required,min=3,max=16"` Name string `validate:"required,min=3,max=16"`
CaptchaToken string
} }
type TokenClaims struct { type TokenClaims struct {

View File

@ -6,6 +6,7 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"log/slog" "log/slog"
"net/http"
"os" "os"
entsql "entgo.io/ent/dialect/sql" entsql "entgo.io/ent/dialect/sql"
@ -96,4 +97,8 @@ func ProvidePubKey(pri *rsa.PrivateKey) (yggdrasil.PubRsaKey, error) {
return yggdrasil.PubRsaKey(s), nil return yggdrasil.PubRsaKey(s), nil
} }
var Set = wire.NewSet(ProvideSlog, ProvideDB, ProvideEnt, ProvideValidate, ProvideCache, ProvidePriKey, ProvidePubKey) func ProvideHttpClient() *http.Client {
return &http.Client{}
}
var Set = wire.NewSet(ProvideSlog, ProvideDB, ProvideEnt, ProvideValidate, ProvideCache, ProvidePriKey, ProvidePubKey, ProvideHttpClient)

View File

@ -51,7 +51,8 @@ func InitializeRoute(ctx context.Context, c config.Config) (*http.Server, func()
return nil, nil, err return nil, nil, err
} }
yggdrasil3 := yggdrasil2.NewYggdrasil(logger, validate, yggdrasilYggdrasil, c, pubRsaKey) yggdrasil3 := yggdrasil2.NewYggdrasil(logger, validate, yggdrasilYggdrasil, c, pubRsaKey)
webService := service.NewWebService(c, client) httpClient := ProvideHttpClient()
webService := service.NewWebService(c, client, httpClient)
handel := handle.NewHandel(webService, validate, c, logger) handel := handle.NewHandel(webService, validate, c, logger)
router, err := route.NewRoute(yggdrasil3, handel) router, err := route.NewRoute(yggdrasil3, handel)
if err != nil { if err != nil {

View File

@ -1,7 +1,12 @@
package service package service
import ( import (
"bytes"
"context" "context"
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/xmdhs/authlib-skin/model" "github.com/xmdhs/authlib-skin/model"
) )
@ -12,3 +17,58 @@ func (w *WebService) GetCaptcha(ctx context.Context) model.Captcha {
SiteKey: w.config.Captcha.SiteKey, SiteKey: w.config.Captcha.SiteKey,
} }
} }
type turnstileRet struct {
Success bool `json:"success"`
ErrorCodes []string `json:"error-codes"`
}
type ErrTurnstile struct {
ErrorCodes []string
}
func (e ErrTurnstile) Error() string {
return strings.Join(e.ErrorCodes, " ")
}
func (w *WebService) verifyTurnstile(ctx context.Context, token, ip string) error {
bw := &bytes.Buffer{}
err := json.NewEncoder(bw).Encode(turnstileResponse{
Secret: w.config.Captcha.Secret,
Response: token,
Remoteip: ip,
})
if err != nil {
return fmt.Errorf("verifyTurnstile: %w", err)
}
reqs, err := http.NewRequestWithContext(ctx, "POST", "https://challenges.cloudflare.com/turnstile/v0/siteverify", bw)
if err != nil {
return fmt.Errorf("verifyTurnstile: %w", err)
}
reqs.Header.Set("Accept", "*/*")
reqs.Header.Set("Content-Type", "application/json")
rep, err := w.httpClient.Do(reqs)
if err != nil {
return fmt.Errorf("verifyTurnstile: %w", err)
}
defer rep.Body.Close()
var t turnstileRet
err = json.NewDecoder(rep.Body).Decode(&t)
if err != nil {
return fmt.Errorf("verifyTurnstile: %w", err)
}
if !t.Success {
return fmt.Errorf("verifyTurnstile: %w", ErrTurnstile{
ErrorCodes: t.ErrorCodes,
})
}
return nil
}
type turnstileResponse struct {
Secret string `json:"secret"`
Response string `json:"response"`
Remoteip string `json:"remoteip"`
}

View File

@ -21,17 +21,23 @@ var (
ErrRegLimit = errors.New("超过注册 ip 限制") ErrRegLimit = errors.New("超过注册 ip 限制")
) )
func (w *WebService) Reg(ctx context.Context, u model.User, ip string) error { func (w *WebService) Reg(ctx context.Context, u model.User, ipPrefix, ip string) error {
var userUuid string var userUuid string
if w.config.OfflineUUID { if w.config.OfflineUUID {
userUuid = utils.UUIDGen(u.Name) userUuid = utils.UUIDGen(u.Name)
} else { } else {
userUuid = strings.ReplaceAll(uuid.New().String(), "-", "") userUuid = strings.ReplaceAll(uuid.New().String(), "-", "")
} }
p, s := utils.Argon2ID(u.Password)
if w.config.Captcha.Type == "turnstile" {
err := w.verifyTurnstile(ctx, u.CaptchaToken, ip)
if err != nil {
return fmt.Errorf("Reg: %w", err)
}
}
if w.config.MaxIpUser != 0 { if w.config.MaxIpUser != 0 {
c, err := w.client.User.Query().Where(user.RegIPEQ(ip)).Count(ctx) c, err := w.client.User.Query().Where(user.RegIPEQ(ipPrefix)).Count(ctx)
if err != nil { if err != nil {
return fmt.Errorf("Reg: %w", err) return fmt.Errorf("Reg: %w", err)
} }
@ -40,6 +46,8 @@ func (w *WebService) Reg(ctx context.Context, u model.User, ip string) error {
} }
} }
p, s := utils.Argon2ID(u.Password)
err := utils.WithTx(ctx, w.client, func(tx *ent.Tx) error { err := utils.WithTx(ctx, w.client, func(tx *ent.Tx) error {
count, err := tx.User.Query().Where(user.EmailEQ(u.Email)).ForUpdate().Count(ctx) count, err := tx.User.Query().Where(user.EmailEQ(u.Email)).ForUpdate().Count(ctx)
if err != nil { if err != nil {
@ -60,7 +68,7 @@ func (w *WebService) Reg(ctx context.Context, u model.User, ip string) error {
SetPassword(p). SetPassword(p).
SetSalt(s). SetSalt(s).
SetRegTime(time.Now().Unix()). SetRegTime(time.Now().Unix()).
SetRegIP(ip). SetRegIP(ipPrefix).
SetState(0).Save(ctx) SetState(0).Save(ctx)
if err != nil { if err != nil {
return err return err

View File

@ -1,6 +1,8 @@
package service package service
import ( import (
"net/http"
"github.com/xmdhs/authlib-skin/config" "github.com/xmdhs/authlib-skin/config"
"github.com/xmdhs/authlib-skin/db/ent" "github.com/xmdhs/authlib-skin/db/ent"
) )
@ -8,11 +10,13 @@ import (
type WebService struct { type WebService struct {
config config.Config config config.Config
client *ent.Client client *ent.Client
httpClient *http.Client
} }
func NewWebService(c config.Config, e *ent.Client) *WebService { func NewWebService(c config.Config, e *ent.Client, hc *http.Client) *WebService {
return &WebService{ return &WebService{
config: c, config: c,
client: e, client: e,
httpClient: hc,
} }
} }