Skip to content

Commit

Permalink
feat(auth): 支持直接点击captcha验证进行刷新验证码 (#560)
Browse files Browse the repository at this point in the history
之前 captcha 验证码只能靠浏览器刷新,输入错误验证码提交两种方式进行刷新,现在可以直接点击验证码进行刷新
<img width="335" alt="image"
src="https://user-images.githubusercontent.com/96867690/231990830-b322890b-30f1-4895-947d-3a7fc5612284.png">

---------

Co-authored-by: yhy <yhy>
  • Loading branch information
coovy committed Apr 21, 2023
1 parent a7fd757 commit 6236865
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 33 deletions.
5 changes: 5 additions & 0 deletions .changeset/green-mugs-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@scow/auth": patch
---

之前 captcha 验证码只能靠浏览器刷新,输入错误验证码提交两种方式进行刷新,现在可以直接点击验证码进行刷新
6 changes: 6 additions & 0 deletions apps/auth/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import createError from "@fastify/error";
import { omitConfigSpec } from "@scow/lib-config";
import { readVersionFile } from "@scow/utils/build/version";
import fastify, { FastifyInstance, FastifyPluginAsync, FastifyPluginCallback } from "fastify";
import { registerCaptchaRoute } from "src/auth/captcha";
import { authConfig } from "src/config/auth";
import { config } from "src/config/env";
import { plugins } from "src/plugins";
import { routes } from "src/routes";
Expand Down Expand Up @@ -62,6 +64,10 @@ export function buildApp(pluginOverrides?: PluginOverrides) {

routes.forEach((r) => server.register(r));

if (authConfig.captcha.enabled) {
registerCaptchaRoute(server);
}

return server;
}

Expand Down
45 changes: 41 additions & 4 deletions apps/auth/src/auth/captcha/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,38 @@
* See the Mulan PSL v2 for more details.
*/

import { Static, Type } from "@sinclair/typebox";
import { randomUUID } from "crypto";
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { serveLoginHtml } from "src/auth/loginHtml";
import { authConfig } from "src/config/auth";
import svgCaptcha from "svg-captcha";

export const CAPTCHA_TOKEN_PREFIX = "captcha:";
export interface CaptchaInfo {
code: string;
token: string;
}
/**
* @param f the FastifyInstance
* @param token If a token is passed in,
* the generated text will be stored in Redis with this token as the key.
* If no token is passed in, a random token will be generated as the key to store the generated text.
*/
export async function saveCaptchaText(
f: FastifyInstance, text: string, token?: string, validSeconds: number = 120): Promise<string> {
token = token ?? randomUUID();
await f.redis.set(CAPTCHA_TOKEN_PREFIX + token, text, "EX", validSeconds);
return token;
}

export async function createCaptcha(f: FastifyInstance): Promise<CaptchaInfo> {
/**
* @param f the FastifyInstance
* @param token If a token is passed in,
* the generated text will be stored in Redis with this token as the key.
* If no token is passed in, a random token will be generated as the key to store the generated text.
*/
export async function createCaptcha(f: FastifyInstance, token?: string): Promise<CaptchaInfo> {

const options = {
size: 4,
Expand All @@ -35,8 +55,7 @@ export async function createCaptcha(f: FastifyInstance): Promise<CaptchaInfo> {

const data = captcha.data;
const text = captcha.text;
const token = randomUUID();
await f.redis.set(token, text, "EX", 120);
token = await saveCaptchaText(f, text, token);
return { code: data, token };

}
Expand All @@ -47,11 +66,29 @@ export async function validateCaptcha(

if (!authConfig.captcha.enabled) { return true; }

const redisCode = await req.server.redis.getdel(token);
const redisCode = await req.server.redis.getdel(CAPTCHA_TOKEN_PREFIX + token);
if (code.toLowerCase() === redisCode?.toLowerCase()) {
return true;
}

await serveLoginHtml(false, callbackUrl, req, res, true);
return false;
}

const bodySchema = Type.Object({
token: Type.String(),
});
export function registerCaptchaRoute(f: FastifyInstance) {
f.post<{ Body: Static<typeof bodySchema> }>(
"/public/refreshCaptcha",
{
schema:{
body: bodySchema,
},
},
async (req, res) => {
const { token } = req.body;
const data = (await createCaptcha(f, token)).code;
await res.type("image/svg+xml").send(data);
});
}
1 change: 1 addition & 0 deletions apps/auth/src/auth/loginHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export async function serveLoginHtml(
...captchaInfo,
verifyCaptchaFail,
enableCaptcha,
refreshCaptchaPath: join(config.BASE_PATH, config.AUTH_BASE_PATH, "/public/refreshCaptcha"),
});

}
17 changes: 9 additions & 8 deletions apps/auth/tests/callback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ process.env.AUTH_TYPE = "ssh";
import { FastifyInstance } from "fastify";
import { buildApp } from "src/app";
import { CallbackHostnameNotAllowedError } from "src/auth/callback";
import { saveCaptchaText } from "src/auth/captcha";
import { allowedCallbackUrl, createFormData,
notAllowedCallbackUrl, testUserPassword, testUserUsername } from "tests/utils";

const username = testUserUsername;
const password = testUserPassword;
const token = "token";
const code = "code";
const captchaToken = "captchaToken";
const captchaCode = "captchaCode";

let server: FastifyInstance;

Expand Down Expand Up @@ -67,11 +68,11 @@ it("redirects to allowed origin after login", async () => {
username: username,
password: password,
callbackUrl: allowedCallbackUrl,
token: token,
code: code,
token: captchaToken,
code: captchaCode,
});

await server.redis.set(token, code, "EX", 30);
await saveCaptchaText(server, captchaCode, captchaToken);
const resp = await server.inject({
method: "POST",
path: "/public/auth",
Expand All @@ -89,11 +90,11 @@ it("doesn't redirect to not allowed origin after login", async () => {
username: username,
password: password,
callbackUrl: notAllowedCallbackUrl,
token: token,
code: code,
token: captchaToken,
code: captchaCode,
});

await server.redis.set(token, code, "EX", 30);
await saveCaptchaText(server, captchaCode, captchaToken);
const resp = await server.inject({
method: "POST",
path: "/public/auth",
Expand Down
21 changes: 11 additions & 10 deletions apps/auth/tests/ldap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import { FastifyInstance } from "fastify";
import { Client, createClient, NoSuchObjectError, SearchEntry } from "ldapjs";
import { buildApp } from "src/app";
import { saveCaptchaText } from "src/auth/captcha";
import { extractAttr, findUser, searchOne, takeOne } from "src/auth/ldap/helpers";
import { authConfig, NewUserGroupStrategy } from "src/config/auth";
import { ensureNotUndefined } from "src/utils/validations";
Expand All @@ -32,8 +33,8 @@ const user = {
identityId: "123",
name: "name",
password: "12#",
token: "token",
code: "code",
captchaToken: "captchaToken",
captchaCode: "captchaCode",
};

const userDn = `${ldap.addUser.userIdDnKey}=${user.identityId},${ldap.addUser.userBase}`;
Expand Down Expand Up @@ -168,10 +169,10 @@ it("test to input a wrong verifyCaptcha", async () => {
username: user.identityId,
password: user.password,
callbackUrl,
token: user.token,
token: user.captchaToken,
code: "wrongCaptcha",
});
await server.redis.set(user.token, user.code, "EX", 30);
await saveCaptchaText(server, user.captchaCode, user.captchaToken);
const resp = await server.inject({
method: "POST",
url: "/public/auth",
Expand All @@ -192,10 +193,10 @@ it("should login with correct username and password", async () => {
username: user.identityId,
password: user.password,
callbackUrl,
token: user.token,
code: user.code,
token: user.captchaToken,
code: user.captchaCode,
});
await server.redis.set(user.token, user.code, "EX", 30);
await saveCaptchaText(server, user.captchaCode, user.captchaToken);
const resp = await server.inject({
method: "POST",
url: "/public/auth",
Expand All @@ -219,10 +220,10 @@ it("should not login with wrong password", async () => {
username: user.identityId,
password: user.password + "0",
callbackUrl,
token: user.token,
code: user.code,
token: user.captchaToken,
code: user.captchaCode,
});
await server.redis.set(user.token, user.code, "EX", 30);
await saveCaptchaText(server, user.captchaCode, user.captchaToken);
const resp = await server.inject({
method: "POST",
url: "/public/auth",
Expand Down
21 changes: 11 additions & 10 deletions apps/auth/tests/ssh.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ process.env.AUTH_TYPE = "ssh";

import { FastifyInstance } from "fastify";
import { buildApp } from "src/app";
import { saveCaptchaText } from "src/auth/captcha";
import { allowedCallbackUrl, createFormData, testUserPassword, testUserUsername } from "tests/utils";

const username = testUserUsername;
const password = testUserPassword;
const token = "token";
const code = "code";
const captchaToken = "captchaToken";
const captchaCode = "captchaToken";

let server: FastifyInstance;

Expand All @@ -43,10 +44,10 @@ it("test to input a wrong verifyCaptcha", async () => {
username: username,
password: password,
callbackUrl,
token: token,
token: captchaToken,
code: "wrongCaptcha",
});
await server.redis.set(token, code, "EX", 30);
await saveCaptchaText(server, captchaCode, captchaToken);
const resp = await server.inject({
method: "POST",
url: "/public/auth",
Expand All @@ -62,11 +63,11 @@ it("logs in to the ssh login", async () => {
username: username,
password: password,
callbackUrl: callbackUrl,
token: token,
code: code,
token: captchaToken,
code: captchaCode,
});

await server.redis.set(token, code, "EX", 30);
await saveCaptchaText(server, captchaCode, captchaToken);
const resp = await server.inject({
method: "POST",
path: "/public/auth",
Expand All @@ -84,11 +85,11 @@ it("fails to login with wrong credentials", async () => {
username: username,
password: password + "a",
callbackUrl: callbackUrl,
token: token,
code: code,
token: captchaToken,
code: captchaCode,
});

await server.redis.set(token, code, "EX", 30);
await saveCaptchaText(server, captchaCode, captchaToken);
const resp = await server.inject({
method: "POST",
path: "/public/auth",
Expand Down
22 changes: 21 additions & 1 deletion apps/auth/views/login.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
.button-primary {
background-color: {{ backgroundColor }}
}
svg{
height: 100%;
width: 100%;
}
</style>
</head>

Expand All @@ -39,7 +43,23 @@
<div class="flex items-center">
<input name="code" placeholder="请输入验证码" type="text" required
class=" px-8 w-full border rounded px-3 py-2 text-gray-700 focus:outline-none" />
{{ code }}
<div id="captcha" onclick="refreshCaptcha()" class="cursor-pointer">{{ code }}</div>
<script>
function refreshCaptcha(){
const captchaDiv = document.getElementById("captcha");
fetch("{{ refreshCaptchaPath }}", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
token: "{{ token }}",
}),}
).then( async function (response) {
captchaDiv.innerHTML = await response.text();
}).catch(() => {
captchaDiv.textContent = "刷新失败,请点击重试"
});
}
</script>
</div>
</div>

Expand Down

0 comments on commit 6236865

Please sign in to comment.