Skip to content

Commit

Permalink
feat: add CAPTCHA verification for anonymous comments to enhance secu…
Browse files Browse the repository at this point in the history
…rity (#133)

#### What type of PR is this?
/kind feature

#### What this PR does / why we need it:
在匿名评论时增加图形验证码验证机制以提高安全性

#### Which issue(s) this PR fixes:
Fixes #132

#### Does this PR introduce a user-facing change?
```release-note
在匿名评论时增加图形验证码验证机制以提高安全性
```
  • Loading branch information
guqing committed Jun 27, 2024
1 parent 944bf37 commit 1549f84
Show file tree
Hide file tree
Showing 22 changed files with 841 additions and 66 deletions.
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ repositories {
}

dependencies {
implementation platform('run.halo.tools.platform:plugin:2.9.0-SNAPSHOT')
implementation platform('run.halo.tools.platform:plugin:2.13.0-SNAPSHOT')
compileOnly 'run.halo.app:api'

testImplementation 'run.halo.app:api'
Expand All @@ -42,4 +42,5 @@ build {

halo {
version = "2.15.0-rc.1"
debug = true
}
77 changes: 70 additions & 7 deletions packages/comment-widget/src/base-form.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import './emoji-button';
import type { User } from '@halo-dev/api-client';
import { consume } from '@lit/context';
import { css, html, LitElement } from 'lit';
import { property, state } from 'lit/decorators.js';
import { createRef, Ref, ref } from 'lit/directives/ref.js';
import {
allowAnonymousCommentsContext,
baseUrlContext,
captchaEnabledContext,
currentUserContext,
groupContext,
kindContext,
nameContext,
toastContext,
} from './context';
import { property, state } from 'lit/decorators.js';
import type { User } from '@halo-dev/api-client';
import './emoji-button';
import './icons/icon-loading';
import { ToastManager } from './lit-toast';
import baseStyles from './styles/base';
import { consume } from '@lit/context';
import varStyles from './styles/var';
import './icons/icon-loading';

export class BaseForm extends LitElement {
@consume({ context: baseUrlContext })
Expand All @@ -29,6 +32,10 @@ export class BaseForm extends LitElement {
@state()
allowAnonymousComments = false;

@consume({ context: captchaEnabledContext, subscribe: true })
@state()
captchaEnabled = false;

@consume({ context: groupContext })
@state()
group = '';
Expand All @@ -41,9 +48,17 @@ export class BaseForm extends LitElement {
@state()
name = '';

@property({ type: String })
@state()
captcha = '';

@property({ type: Boolean })
submitting = false;

@consume({ context: toastContext, subscribe: true })
@state()
toastManager: ToastManager | undefined;

textareaRef: Ref<HTMLTextAreaElement> = createRef<HTMLTextAreaElement>();

get customAccount() {
Expand All @@ -58,6 +73,25 @@ export class BaseForm extends LitElement {
return `/console/login?redirect_uri=${encodeURIComponent(window.location.href + parentDomId)}`;
}

get showCaptcha() {
return this.captchaEnabled && !this.currentUser;
}

async handleFetchCaptcha() {
if (!this.showCaptcha) {
return;
}

const response = await fetch(`/apis/api.commentwidget.halo.run/v1alpha1/captcha/-/generate`);

if (!response.ok) {
this.toastManager?.error('获取验证码失败');
return;
}

this.captcha = await response.text();
}

handleOpenLoginPage() {
window.location.href = this.loginUrl;
}
Expand Down Expand Up @@ -124,6 +158,7 @@ export class BaseForm extends LitElement {
override connectedCallback(): void {
super.connectedCallback();
this.addEventListener('keydown', this.onKeydown);
this.handleFetchCaptcha();
}

override disconnectedCallback(): void {
Expand Down Expand Up @@ -182,6 +217,20 @@ export class BaseForm extends LitElement {
</button> `
: ''}
<div class="form__actions">
${this.showCaptcha
? html`
<div class="form__action--captcha">
<input name="captchaCode" type="text" placeholder="请输入验证码" />
<img
@click=${this.handleFetchCaptcha}
src="${this.captcha}"
alt="captcha"
width="100%"
/>
</div>
`
: ''}
<emoji-button @emoji-select=${this.onEmojiSelect}></emoji-button>
<button .disabled=${this.submitting} type="submit" class="form__button--submit">
${this.submitting
Expand Down Expand Up @@ -286,7 +335,7 @@ export class BaseForm extends LitElement {
border: 0.05em solid var(--component-form-input-border-color);
font-size: 0.875em;
display: block;
height: 2.25em;
height: 2.65em;
max-width: 100%;
outline: 0;
padding: 0.4em 0.75em;
Expand Down Expand Up @@ -349,12 +398,26 @@ export class BaseForm extends LitElement {
.form__actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75em;
flex: 1 1 auto;
width: 100%;
justify-content: flex-end;
}
.form__action--captcha {
display: flex;
align-items: center;
gap: 0.3em;
flex-direction: row-reverse;
}
.form__action--captcha img {
height: 2.25em;
width: auto;
border-radius: var(--base-border-radius);
}
.form__button--submit {
border-radius: var(--base-border-radius);
background-color: var(--component-form-button-submit-bg-color);
Expand Down
27 changes: 21 additions & 6 deletions packages/comment-widget/src/comment-form.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { html, LitElement } from 'lit';
import { state } from 'lit/decorators.js';
import { Comment, CommentRequest, User } from '@halo-dev/api-client';
import { consume } from '@lit/context';
import { LitElement, html } from 'lit';
import { state } from 'lit/decorators.js';
import { Ref, createRef, ref } from 'lit/directives/ref.js';
import './base-form';
import { BaseForm } from './base-form';
import {
allowAnonymousCommentsContext,
baseUrlContext,
Expand All @@ -11,11 +15,8 @@ import {
toastContext,
versionContext,
} from './context';
import { Comment, CommentRequest, User } from '@halo-dev/api-client';
import { createRef, Ref, ref } from 'lit/directives/ref.js';
import { BaseForm } from './base-form';
import './base-form';
import { ToastManager } from './lit-toast';
import { getCaptchaCodeHeader, isRequireCaptcha } from './utils/captcha';

export class CommentForm extends LitElement {
@consume({ context: baseUrlContext })
Expand Down Expand Up @@ -53,11 +54,15 @@ export class CommentForm extends LitElement {
@state()
submitting = false;

@state()
captcha = '';

baseFormRef: Ref<BaseForm> = createRef<BaseForm>();

override render() {
return html` <base-form
.submitting=${this.submitting}
.captcha=${this.captcha}
${ref(this.baseFormRef)}
@submit="${this.onSubmit}"
></base-form>`;
Expand Down Expand Up @@ -110,10 +115,20 @@ export class CommentForm extends LitElement {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...getCaptchaCodeHeader(data.captchaCode),
},
body: JSON.stringify(commentRequest),
});

if (isRequireCaptcha(response)) {
const { captcha, detail } = await response.json();
this.captcha = captcha;
this.toastManager?.warn(detail);
return;
}

this.baseFormRef.value?.handleFetchCaptcha();

if (!response.ok) {
throw new Error('评论失败,请稍后重试');
}
Expand Down
47 changes: 26 additions & 21 deletions packages/comment-widget/src/comment-widget.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,40 @@
import { css, html, LitElement } from 'lit';
import { property, state } from 'lit/decorators.js';
import { CommentVoList, User } from '@halo-dev/api-client';
import { repeat } from 'lit/directives/repeat.js';
import baseStyles from './styles/base';
import { provide } from '@lit/context';
import { LitElement, css, html } from 'lit';
import { property, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import {
AllUserPolicy,
AnonymousUserPolicy,
AvatarPolicyEnum,
NoAvatarUserPolicy,
setPolicyInstance,
} from './avatar/avatar-policy';
import { setAvatarProvider } from './avatar/providers';
import './comment-form';
import './comment-item';
import './comment-pagination';
import {
allowAnonymousCommentsContext,
avatarPolicyContext,
avatarProviderContext,
avatarProviderMirrorContext,
baseUrlContext,
captchaEnabledContext,
currentUserContext,
emojiDataUrlContext,
groupContext,
kindContext,
nameContext,
replySizeContext,
toastContext,
useAvatarProviderContext,
versionContext,
withRepliesContext,
allowAnonymousCommentsContext,
useAvatarProviderContext,
avatarPolicyContext,
avatarProviderContext,
avatarProviderMirrorContext,
} from './context';
import './comment-form';
import './comment-item';
import './comment-pagination';
import varStyles from './styles/var';
import { ToastManager } from './lit-toast';
import {
AnonymousUserPolicy,
AllUserPolicy,
NoAvatarUserPolicy,
AvatarPolicyEnum,
setPolicyInstance,
} from './avatar/avatar-policy';
import { setAvatarProvider } from './avatar/providers';
import baseStyles from './styles/base';
import varStyles from './styles/var';

export class CommentWidget extends LitElement {
@provide({ context: baseUrlContext })
Expand Down Expand Up @@ -98,6 +99,10 @@ export class CommentWidget extends LitElement {
@state()
allowAnonymousComments = false;

@provide({ context: captchaEnabledContext })
@property({ type: Boolean, attribute: 'enable-captcha' })
captchaEnabled = false;

@provide({ context: toastContext })
@state()
toastManager: ToastManager | undefined;
Expand Down
2 changes: 2 additions & 0 deletions packages/comment-widget/src/context/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const allowAnonymousCommentsContext = createContext<boolean>(
Symbol('allowAnonymousComments')
);

export const captchaEnabledContext = createContext<boolean>(Symbol('captchaEnabledContext'));

export const currentUserContext = createContext<User | undefined>(Symbol('currentUser'));

export const emojiDataUrlContext = createContext<string>(Symbol('emojiDataUrl'));
Expand Down
27 changes: 21 additions & 6 deletions packages/comment-widget/src/reply-form.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import './base-form';
import { CommentVo, Reply, ReplyRequest, ReplyVo, User } from '@halo-dev/api-client';
import { html, LitElement } from 'lit';
import { createRef, Ref, ref } from 'lit/directives/ref.js';
import { consume } from '@lit/context';
import { LitElement, html } from 'lit';
import { property, state } from 'lit/decorators.js';
import { Ref, createRef, ref } from 'lit/directives/ref.js';
import './base-form';
import { BaseForm } from './base-form';
import {
allowAnonymousCommentsContext,
baseUrlContext,
currentUserContext,
toastContext,
} from './context';
import { property, state } from 'lit/decorators.js';
import { BaseForm } from './base-form';
import { consume } from '@lit/context';
import { ToastManager } from './lit-toast';
import { getCaptchaCodeHeader, isRequireCaptcha } from './utils/captcha';

export class ReplyForm extends LitElement {
@consume({ context: baseUrlContext })
Expand Down Expand Up @@ -39,6 +40,9 @@ export class ReplyForm extends LitElement {
@state()
submitting = false;

@state()
captcha = '';

baseFormRef: Ref<BaseForm> = createRef<BaseForm>();

override connectedCallback(): void {
Expand All @@ -53,6 +57,7 @@ export class ReplyForm extends LitElement {
override render() {
return html` <base-form
.submitting=${this.submitting}
.captcha=${this.captcha}
${ref(this.baseFormRef)}
@submit="${this.onSubmit}"
></base-form>`;
Expand Down Expand Up @@ -105,11 +110,21 @@ export class ReplyForm extends LitElement {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...getCaptchaCodeHeader(data.captchaCode),
},
body: JSON.stringify(replyRequest),
}
);

if (isRequireCaptcha(response)) {
const { captcha, detail } = await response.json();
this.captcha = captcha;
this.toastManager?.warn(detail);
return;
}

this.baseFormRef.value?.handleFetchCaptcha();

if (!response.ok) {
throw new Error('评论失败,请稍后重试');
}
Expand Down
12 changes: 12 additions & 0 deletions packages/comment-widget/src/utils/captcha.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const getCaptchaCodeHeader = (code: string): Record<string, string> => {
if (!code || code.trim().length === 0) {
return {};
}
return {
'X-Captcha-Code': code,
};
};

export const isRequireCaptcha = (response: Response) => {
return response.status === 403 && response.headers.get('X-Require-Captcha');
};
Loading

0 comments on commit 1549f84

Please sign in to comment.