验证码
This commit is contained in:
parent
f0e9045e98
commit
7376cffe61
@ -27,5 +27,6 @@ type Config struct {
|
|||||||
Captcha struct {
|
Captcha struct {
|
||||||
Type string
|
Type string
|
||||||
SiteKey string
|
SiteKey string
|
||||||
|
Secret string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
@ -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
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -9,9 +9,10 @@ type API[T any] struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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"`
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -1,18 +1,22 @@
|
|||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user