この記事は前回の記事「【Discord Bot】Vue.jsでDiscordへメッセージを送る機能を実装する」の続編である。
Vue.jsからDiscordへメッセージを送る場合の簡単なコードは上のページで行っているので参考にしてほしい。
投稿するテキストエリアを増やし、ダイアログも幾つか実装した。
特別難しいコードではないのでこのあたりの解説は省略。
本日はこのVue.js上で連投防止機能を考えたので解説する。
残念ながらそれでもイタズラは出来るので抜け穴を合わせて紹介する。
※全コードは下部にすべて貼り付けてある。解説は重要なところに絞って行う。
次のようなフォームを作った。
次のようなイタズラを入力する
送信する。
送信できた。
私のDiscordに次のメッセージが送られてくる。
即座にもう一度メッセージを送信してみる。
ブロックされた。
非常に簡単。
簡単すぎて驚くと思う。
ブラウザの機能でローカルストレージというものがある。
これはクッキーとは違う。次のサイトが参考になる。
クッキーはもう古い!?HTML5 LocalStorageの使い方
これはVue.jsからも利用できる。コードは簡単で書き込みが成功した時に次のコードを実行する。
localStorage.date = new Date(); //イタズラ防止用:投稿時間を保存
これが簡単すぎて誤った使い方をする人がいるそうなので書いておくと
本当にnpm install
もいらず、宣言もいらない。いきなりlocalStorage
のメンバ変数に書き込むだけだ。
//連投制限 連続投稿には1分待つ必要がある
if (localStorage.date) {
const postSpan = new Date(
new Date() - new Date(localStorage.date)
).getMinutes();
if (postSpan < 1) {
this.dialogs.error_msg =
"連続で投稿できません。しばらくお待ち下さい。";
this.dialogs.error = true;
return;
}
}
上のようなコードで1分以上経過しないと連続投稿できないようにした。
さて、このコードの抜け穴を使ってイタズラしてやろう。
残念だがVue.jsはクライアントサイドのプログラムなのでクライアント内部のデータを弄ってやればこの機能はスルーできてしまうのだ。
Chrome以外わからないのでこれで説明する。
まずF12を押す
画面が小さい場合右上に「>>」が出ているので押す。「Application」を選択する。
①が選択されたら②の「Local Storage
」とあるところの小さな三角を押して③のサイトURLをクリックする。
ここではローカルサーバーを稼働しているので画像のようになっているが、私のサイトであればultra-noob.com
となっているはずだ。
最後に④がVue.jsによって書き込まれた変数である。Vue.jsはこの値を読み込んでイタズラかどうかを判断しているので
右クリックでDELETE
しよう。
こうすれば比較する日付がないため何度でも投稿できる。
できればイタズラはやめていただきたい。 サーバー側でフィルター処理ができる環境であればこのような処理は必要ないだろうがVue.jsはあくまでもクライアントサイドのプログラムであるためこのような抜け穴は仕方がない。
前回の書き込み日時がローカルストレージに保存されてない場合はページ読み込みから1分待たないと書き込めないなどの処理はできるだろうが、それでも抜け穴はいくらでもある。
もしかしたら良い防止策があるかもしれないが、この機能はおまけ程度だと考えれば良い。
ローカルストレージはいろいろなサイトで使われている。この手法を覚えておけばデバッグ等で役立つだろう。 覚えておいて損はないと思う。
あまり美しいコードではないかもしれない。
Webhook URLは削除したので参考にそのまま記載してある。イタズラはやめていただきたい。
<template>
<v-container>
<!-- メッセージフォーム -->
<v-card class="mx-auto" max-width="800">
<v-card-title>メッセージ送信フォーム (Contact Form)</v-card-title>
<v-card-text>
<v-form ref="messageForm">
<v-text-field v-model="formData.name" label="お名前 (Name)"></v-text-field>
<v-text-field v-model="formData.replyTo" label="返信先 (E-mail, Twitter, etc)"></v-text-field>
<v-textarea v-model="formData.message" :rules="[required]" label="[必須] メッセージ (Message)"></v-textarea>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="check">送信(Submit)</v-btn>
</v-card-actions>
</v-card>
<!-- 確認ダイアログ -->
<v-dialog v-model="dialogs.check" max-width="300">
<v-progress-linear :active="dialogs.loader" indeterminate color="blue" class="mb-0"></v-progress-linear>
<v-card>
<v-card-title>確認(Confirmation)</v-card-title>
<v-card-text>
<v-card-subtitle>この内容でよろしいですか?</v-card-subtitle>
<p>[Name] {{formData.name}}</p>
<p>[Reply-To] {{formData.replyTo}}</p>
<p>[Message]</p>
<p>{{formData.message}}</p>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="submit">OK</v-btn>
<v-btn @click="dialogs.check=!dialogs.check">Cancel</v-btn>
</v-card-actions>
</v-card>
<v-progress-linear :active="dialogs.loader" indeterminate color="blue" class="mb-0"></v-progress-linear>
</v-dialog>
<!-- エラーダイアログ -->
<v-dialog v-model="dialogs.error" max-width="300">
<v-progress-linear :active="dialogs.loader" indeterminate color="blue" class="mb-0"></v-progress-linear>
<v-card>
<v-card-title>
<v-icon color="red">mdi-alert-circle-outline</v-icon>Error!
</v-card-title>
<v-card-text>
<p>[エラー内容]</p>
<p>{{dialogs.error_msg}}</p>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="dialogs.error=!dialogs.error">OK</v-btn>
</v-card-actions>
</v-card>
<v-progress-linear :active="dialogs.loader" indeterminate color="blue" class="mb-0"></v-progress-linear>
</v-dialog>
<!-- 送信成功ダイアログ -->
<v-dialog v-model="dialogs.success" max-width="300">
<v-card>
<v-card-title>
<v-icon color="green">mdi-check-bold</v-icon>送信成功!
</v-card-title>
<v-card-text>
送信されました。
<br />ありがとうございました。
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="dialogs.success=!dialogs.success">OK</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<script>
import axios from "axios";
export default {
name: "PostForm",
data: () => ({
//ダイアログ関係のオブジェクト
dialogs: {
success: false,
check: false,
error: false,
loader: false,
error_msg: ""
},
//Webhook接続先URL
webhook_url:
"https://discordapp.com/api/webhooks/705382349235421205/k6seL_OdWEHzQb06WsST1CqVtxRXS51ziZZhPL2d2aktoC6lYvw9Kp7ZTNwdcHJAAMLY",
//必須項目チェックのバリデータ
required: value =>
!!value || "メッセージは必須項目です。 Message is required.",
//フォーム内のデータ
formData: {
name: "",
replyTo: "",
message: ""
}
}),
mounted:()=>{
document.title = "メッセージ送信フォーム - ultra-noob.com";
},
methods: {
closeAllDialogs() {
//ダイアログをすべてクローズ
this.dialogs.success = false;
this.dialogs.check = false;
this.dialogs.error = false;
},
check() {
//必須項目が入力されていれば確認用ダイアログを表示
if (this.$refs.messageForm.validate()) {
this.dialogs.check = true;
}
},
submit() {
this.dialogs.loader = true;
//データの内容はcontent
//\r で改行を入れることが出来る
const data = {
username: this.formData.name,
content: `[Reply-to] ${this.formData.replyTo}\r[Message]\r${this.formData.message}`
};
//連投制限 連続投稿には1分待つ必要がある
if (localStorage.date) {
const postSpan = new Date(
new Date() - new Date(localStorage.date)
).getMinutes();
if (postSpan < 1) {
this.dialogs.error_msg =
"連続で投稿できません。しばらくお待ち下さい。";
this.dialogs.error = true;
return;
}
}
//POSTするロジック
axios
.post(this.webhook_url, data)
.then(() => {
//投稿に成功したとき
this.closeAllDialogs();
this.dialogs.success = true;
this.dialogs.loader = false;
localStorage.date = new Date(); //イタズラ防止用:投稿時間を保存
})
.catch(err => {
//通信でエラーがあったとき
this.closeAllDialogs();
this.dialogs.error = true;
this.dialogs.error_msg = err;
this.dialogs.loader = false;
});
}
}
};
</script>