验证码

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 {
Type string
SiteKey string
Secret string
}
}

View File

@ -15,18 +15,19 @@ export async function login(username: string, password: string) {
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", {
method: "POST",
method: "PUT",
body: JSON.stringify({
"Email": email,
"Password": password,
"Name": username
"Name": username,
"CaptchaToken": captchaToken
})
})
const data = await v.json()
if (!v.ok) {
throw data
throw data.msg
}
return
}

View File

@ -11,7 +11,7 @@ interface prop {
onSuccess: ((token: string) => void)
}
const TurnstileWidget = memo(({ onSuccess }: prop) => {
function CaptchaWidget({ onSuccess }: prop) {
const ref = useRef<TurnstileInstance>(null)
const [key, setKey] = useState(1)
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>
}
if (isLoading) {
return <Skeleton variant="rectangular" width={210} height={118} />
return <Skeleton variant="rectangular" width={300} height={65} />
}
if (data?.code != 0) {
console.warn(error)
return <Alert severity="warning">{String(data?.msg)}</Alert>
}
if (data.data.type != "turnstile") {
onSuccess("ok")
return <></>
}
return (
<>
<Turnstile siteKey={data?.data.siteKey ?? ""} key={key} onSuccess={onSuccess} ref={ref} scriptOptions={{ async: true }} />
<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 Loading from '@/components/Loading'
import { useNavigate } from "react-router-dom";
import TurnstileWidget from '@/components/TurnstileWidget';
import CaptchaWidget from '@/components/CaptchaWidget';
export default function SignUp() {
const [regErr, setRegErr] = useState("");
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const [captchaToken, setCaptchaToken] = useState("");
const checkList = React.useRef<Map<string, refType>>(new Map<string, refType>())
@ -40,7 +41,11 @@ export default function SignUp() {
setLoading(false)
return
}
register(d.email ?? "", d.username ?? "", d.password ?? "").
if (captchaToken == "") {
setLoading(false)
setRegErr("验证码无效")
}
register(d.email ?? "", d.username ?? "", d.password ?? "", captchaToken).
then(() => navigate("/login")).
catch(v => [setRegErr(String(v)), console.warn(v)]).
finally(() => setLoading(false))
@ -121,7 +126,7 @@ export default function SignUp() {
/>
</Grid>
<Grid item xs={12}>
<TurnstileWidget onSuccess={v => console.log(v)} />
<CaptchaWidget onSuccess={setCaptchaToken} />
</Grid>
</Grid>
<Button

View File

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

View File

@ -9,9 +9,10 @@ type API[T any] struct {
}
type User struct {
Email string `validate:"required,email"`
Password string `validate:"required,min=6,max=50"`
Name string `validate:"required,min=3,max=16"`
Email string `validate:"required,email"`
Password string `validate:"required,min=6,max=50"`
Name string `validate:"required,min=3,max=16"`
CaptchaToken string
}
type TokenClaims struct {

View File

@ -6,6 +6,7 @@ import (
"database/sql"
"fmt"
"log/slog"
"net/http"
"os"
entsql "entgo.io/ent/dialect/sql"
@ -96,4 +97,8 @@ func ProvidePubKey(pri *rsa.PrivateKey) (yggdrasil.PubRsaKey, error) {
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
}
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)
router, err := route.NewRoute(yggdrasil3, handel)
if err != nil {

View File

@ -1,7 +1,12 @@
package service
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/xmdhs/authlib-skin/model"
)
@ -12,3 +17,58 @@ func (w *WebService) GetCaptcha(ctx context.Context) model.Captcha {
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 限制")
)
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
if w.config.OfflineUUID {
userUuid = utils.UUIDGen(u.Name)
} else {
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 {
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 {
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 {
count, err := tx.User.Query().Where(user.EmailEQ(u.Email)).ForUpdate().Count(ctx)
if err != nil {
@ -60,7 +68,7 @@ func (w *WebService) Reg(ctx context.Context, u model.User, ip string) error {
SetPassword(p).
SetSalt(s).
SetRegTime(time.Now().Unix()).
SetRegIP(ip).
SetRegIP(ipPrefix).
SetState(0).Save(ctx)
if err != nil {
return err

View File

@ -1,18 +1,22 @@
package service
import (
"net/http"
"github.com/xmdhs/authlib-skin/config"
"github.com/xmdhs/authlib-skin/db/ent"
)
type WebService struct {
config config.Config
client *ent.Client
config config.Config
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{
config: c,
client: e,
config: c,
client: e,
httpClient: hc,
}
}