NETFLIXとかアマプラとかその他諸々のサービスでブラウザが変わったりすると、登録しているメールアドレス宛にセキュリティ警告的なメールが届くと思いますが、今回はFingerprintjsを利用して実装してみたいと思います。
Fingerprintの詳細は以下ページよりご確認ください。
詳細は下記ページよりご確認ください。
前提
コンテナ環境での動作確認となります。前回の記事をベースに作業をしていますのでまずはこちらの記事を参考にコンテナを作成してください。
1. API作成
コンテナにログイン後、以下コマンドでライブラリをインストールしておきましょう。
pip3 install fastapi uvicorn
続いて、main.pyを作成します。
"""
ユーザー認証、FingerPrint、および電子メール通知を実行の為各種
モジュールをImport
"""
import smtplib
import sqlite3
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from pydantic import BaseModel
from fastapi import FastAPI, Depends, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
# ログイン用テストユーザ情報
fake_users_db = {
"user1": {
"username": "user1",
"password": "password1",
},
"user2": {
"username": "user2",
"password": "password2",
},
}
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class LoginRequest(BaseModel):
"""
Loginユーザ用クラス
"""
username: str
password: str
class FingerprintCreate(BaseModel):
"""
FingerprintInser用クラス
"""
visitor_id: str
timestamp: str
username: str
def init_db():
"""
データベース初期化
"""
conn = sqlite3.connect("fingerprints.db")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS fingerprints (
id INTEGER PRIMARY KEY AUTOINCREMENT,
visitor_id TEXT,
ip_address TEXT,
timestamp TEXT,
username TEXT
)
""")
conn.commit()
conn.close()
init_db()
def send_email(subject: str, body: str, to_email: str):
"""
FingerPrint値が違う場合、管理者にメールを送信する関数
"""
from_email = "xxxxxx.xxxxx@live-est.com" # 送信用メールアドレス。ご自身の環境に合わせてください。
from_password = "xxxxxxxxxx" # メールパスワード
msg = MIMEMultipart()
msg['From'] = from_email
msg['To'] = to_email
msg['Subject'] = subject
msg.attach(MIMEText(body, 'plain'))
try:
server = smtplib.SMTP('live-est.com', 587) # SMTPサーバ。ご自身の環境に合わせてください。
server.starttls()
server.login(from_email, from_password)
text = msg.as_string()
server.sendmail(from_email, to_email, text)
server.quit()
print("Email sent successfully!")
except smtplib.SMTPException as e:
print(f"Failed to send email: {e}")
def get_db():
"""
リクエスト ハンドラーで使用する SQLite データベース接続を生成します。
リクエストが完了したら接続を閉じます。
"""
conn = sqlite3.connect("fingerprints.db", check_same_thread=False)
try:
yield conn
finally:
conn.close()
@app.post("/login/")
def login(login_request: LoginRequest):
"""
ユーザーのログインを処理し、成功した場合はユーザ名を返却します。
"""
user = fake_users_db.get(login_request.username)
if user and user["password"] == login_request.password:
return {"username": user["username"]}
else:
raise HTTPException(status_code=401, detail="Invalid username or password")
@app.post("/fingerprints/")
def create_fingerprint(fingerprint: FingerprintCreate, request: Request,
db: sqlite3.Connection = Depends(get_db)):
"""
FingerPrint値が違う場合警告メールを管理者へ送信する
"""
client_ip = request.client.host
cursor = db.cursor()
cursor.execute(
"SELECT visitor_id FROM fingerprints WHERE username = ? ORDER BY id DESC LIMIT 1",
(fingerprint.username,)
)
previous_fingerprint = cursor.fetchone()
if previous_fingerprint and previous_fingerprint[0] != fingerprint.visitor_id:
subject = "Alert: Fingerprint Change Detected"
body = (
f"{fingerprint.username}は異なる環境からのアクセスが検出されました。\n\n"
f"以前のFingerPrint値: {previous_fingerprint[0]}\n"
f"新しいFingerprint: {fingerprint.visitor_id}\n"
f"検出されたIPアドレス: {client_ip}"
)
to_email = "xxxxxx.xxxxxx@live-est.com" # 管理者用メールアドレス。ご自身の環境に合わせてください。
send_email(subject, body, to_email)
cursor.execute(
"INSERT INTO fingerprints (visitor_id, ip_address, timestamp, username) "
"VALUES (?, ?, ?, ?)",
(fingerprint.visitor_id, client_ip, fingerprint.timestamp, fingerprint.username)
)
db.commit()
fingerprint_id = cursor.lastrowid
return {
"id": fingerprint_id,
"visitor_id": fingerprint.visitor_id,
"ip_address": client_ip,
"timestamp": fingerprint.timestamp,
"username": fingerprint.username
}
main.pyについて
- Reactからはログイン用のエンドポイントとFingerPrint/IP/時刻/usernameを受け付けるPOST用のエンドポイントを作成
- これに伴い、Sqliteを使ったDB作成/ログイン認証処理(簡易)/前回利用時のFingerPrint値との突合/メール送信処理といった具合にそれぞれ作成
- メール送信用アドレス、パスワード、SMTPサーバ、通知先メールアドレス等は自身の環境に変更してください
- ※あくまでFingerPrintを使った異なる環境からのアクセス検出を行うためだけの簡易コードですのであしからず
作成したら以下コマンドで起動します。
uvicorn main:app --host 0.0.0.0 --port 4002
2. Reactコード作成
では、次はReactのコードになります。いつも通り必要となるpackageを事前にインストールします。
npm install @fingerprintjs/fingerprintjs
npm install @mui/material @emotion/react @emotion/styled
npm install @mui/icons-material
それでは、 src/App.js を変更しましょう。
import React, { useState, useEffect } from 'react';
import FingerprintJS from '@fingerprintjs/fingerprintjs';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import Container from '@mui/material/Container';
import Box from '@mui/material/Box';
import CssBaseline from '@mui/material/CssBaseline';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import { createTheme, ThemeProvider } from '@mui/material/styles';
const theme = createTheme();
function App() {
const [username, setUsername] = useState(null);
const [fingerprint, setFingerprint] = useState('');
const handleLogin = async (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const username = formData.get('username');
const password = formData.get('password');
const response = await fetch('http://x.x.x.x:4002/login/', { // dockerホストのIP
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
});
if (response.ok) {
const data = await response.json();
setUsername(data.username);
} else {
alert('Login failed');
}
};
useEffect(() => {
if (username) {
const fpPromise = FingerprintJS.load();
fpPromise
.then(fp => fp.get())
.then(result => {
const visitorId = result.visitorId;
setFingerprint(visitorId);
fetch('http://x.x.x.x:4002/fingerprints/', { // dockerホストのIP
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
visitor_id: visitorId,
timestamp: new Date().toISOString(),
username: username
}),
});
});
}
}, [username]);
if (!username) {
return (
<ThemeProvider theme={theme}>
<Container component="main" maxWidth="xs">
<CssBaseline />
<Box
sx={{
marginTop: 8,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Login
</Typography>
<Box component="form" onSubmit={handleLogin} noValidate sx={{ mt: 1 }}>
<TextField
margin="normal"
required
fullWidth
id="username"
label="Username"
name="username"
autoComplete="username"
autoFocus
/>
<TextField
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
/>
<Button
type="submit"
fullWidth
variant="contained"
sx={{ mt: 3, mb: 2 }}
>
Login
</Button>
</Box>
</Box>
</Container>
</ThemeProvider>
);
}
return (
<Container component="main" maxWidth="xs">
<Box
sx={{
marginTop: 8,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Typography component="h1" variant="h5">
あなたのFingerprintIDです。
</Typography>
<Typography variant="body1" sx={{ mt: 2 }}>
{fingerprint || : 'Loading...'}
</Typography>
</Box>
</Container>
);
}
export default App;
Sourceも変更できたので、これでアプリを起動できます。いつも通り、起動前に.envファイルを作成して起動するポートを4001番に変更します。
PORT=4001
では下記コマンドで起動しましょう。
npm start
3. アプリケーション確認
ブラウザでアクセスします。http://x.x.x.x:4001 でアクセスしてAPIに記載しているユーザ(user1/passowrd1)でログインしましょう。
続いて、別ブラウザから同様に実施します。FingerPrintの値が異なっているのがわかります。
メールを確認すると、以下のように検出された結果がわかります。
4. その他
異なる環境からのアクセス検出を取り入れる際、お役に立てば幸いです。