feat(安全): 实现反欺诈检测和强制打卡功能

- 添加反欺诈服务,定期检查设备上的黑名单应用
- 检测到黑名单应用时自动强制用户打卡下班
- 在登录后启动反欺诈检查,每5分钟执行一次
- 更新后端API支持强制打卡事件
- 调整Android最低SDK版本至24
- 添加全局事件监听处理强制打卡事件
This commit is contained in:
sudomarcma
2025-07-09 18:31:21 +08:00
parent 9e576dcdf1
commit 8ecbd44948
7 changed files with 134 additions and 47 deletions
+2 -2
View File
@@ -5,8 +5,8 @@ android {
compileSdk rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.ouji.factory.myapp"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
minSdk 24
targetSdk 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+19 -15
View File
@@ -309,72 +309,76 @@ const geofence = polygon([
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.sendStatus(403)
return res.status(403).json({ message: 'Forbidden' })
}
req.user = user
next()
})
} else {
res.sendStatus(401)
res.status(401).json({ message: 'Unauthorized' })
}
}
// Worker Clock In/Out Endpoint
app.post('/api/clock', authenticateJWT, async (req, res) => {
try {
const { userId, eventType, qrCodeValue, latitude, longitude } = req.body
const { userId, eventType, qrCodeValue, latitude, longitude, notes } = req.body
// Bypass geofence and QR code validation for forced events
if (qrCodeValue !== 'FORCE_CLOCK_OUT' && qrCodeValue !== 'BLACKLIST_APP_DETECTED') {
// Geofencing check using the directly imported functions
const userLocation = point([longitude, latitude]);
const isWithinGeofence = booleanPointInPolygon(userLocation, geofence);
if (!isWithinGeofence) {
// User is outside the geofence, log a 'failed' attempt
// Calculate the distance from the geofence
const distance = pointToLineDistance(userLocation, geofence.geometry.coordinates[0], { units: 'meters' });
// Create a descriptive note
const notes = `Clock-in outside of the zone: ${distance.toFixed(2)} meters.`;
// Insert the failed attempt into the database
await db.execute(
'INSERT INTO clock_records (worker_id, event_type, timestamp, qr_code_id, latitude, longitude, notes) VALUES (?, ?, ?, ?, ?, ?, ?)',
[userId, 'failed', new Date(), qrCodeValue, latitude, longitude, notes]
);
// Return an error to the user
return res.status(403).json({ message: `You are not within the allowed work area.` });
// --- MODIFICATION END ---
}
const [qrRows] = await db.execute('SELECT name, is_active FROM qr_codes WHERE id = ?', [
qrCodeValue,
])
]);
if (qrRows.length === 0) {
// This code is not in the database at all.
return res.status(400).json({ message: 'Invalid QR Code scanned.' })
return res.status(400).json({ message: 'Invalid QR Code scanned.' });
}
if (!qrRows[0].is_active) {
// This code exists but has been deactivated.
return res
.status(400)
.json({ message: 'This QR Code has expired and is no longer active.' })
.json({ message: 'This QR Code has expired and is no longer active.' });
}
}
const [lastEventRows] = await db.execute(
'SELECT event_type FROM clock_records WHERE worker_id = ? ORDER BY timestamp DESC LIMIT 1',
[userId],
)
if (lastEventRows.length > 0 && lastEventRows[0].event_type === eventType) {
if (qrCodeValue === 'FORCE_CLOCK_OUT') {
// If it's a forced clock-out on an already clocked-out user, log it as a failed event
await db.execute(
'INSERT INTO clock_records (worker_id, event_type, timestamp, qr_code_id, latitude, longitude, notes) VALUES (?, ?, ?, ?, ?, ?, ?)',
[userId, 'failed', new Date(), qrCodeValue, latitude, longitude, `FAKE GPS APP Detected.`]
);
return res.status(200).json({ message: 'Forced clock-out attempt on already clocked-out user was logged.' });
}
return res
.status(400)
.json({ message: `You are already clocked ${eventType === 'clock_in' ? 'in' : 'out'}.` })
}
const timestamp = new Date()
await db.execute(
'INSERT INTO clock_records (worker_id, event_type, timestamp, qr_code_id, latitude, longitude) VALUES (?, ?, ?, ?, ?, ?)',
[userId, eventType, timestamp, qrCodeValue, latitude, longitude],
'INSERT INTO clock_records (worker_id, event_type, timestamp, qr_code_id, latitude, longitude, notes) VALUES (?, ?, ?, ?, ?, ?, ?)',
[userId, eventType, timestamp, qrCodeValue, latitude, longitude, notes],
)
res.status(201).json({ message: 'Clock event recorded successfully' })
} catch (error) {
+7
View File
@@ -114,6 +114,7 @@ watch(
onMounted(async () => {
// Add app blocked event listener
window.addEventListener('app-blocked', handleAppBlocked)
window.addEventListener('user-forced-clock-out', handleForcedClockOut)
// Initialize SafeArea plugin for proper safe area handling
if (Capacitor.isNativePlatform()) {
@@ -197,9 +198,15 @@ const handleAppBlocked = (event) => {
blockMessage.value = event.detail.message
}
const handleForcedClockOut = (event) => {
isBlocked.value = true
blockMessage.value = `You have been automatically clocked out. Reason: ${event.detail.note}`
}
onBeforeUnmount(() => {
// Clean up listeners
window.removeEventListener('app-blocked', handleAppBlocked)
window.removeEventListener('user-forced-clock-out', handleForcedClockOut)
if (Capacitor.isNativePlatform()) {
App.removeAllListeners()
}
+37 -8
View File
@@ -1,5 +1,6 @@
import { Capacitor } from '@capacitor/core'
import { apiFetch } from '@/api.js'
import { authService } from './authService'
// Since we are using a custom plugin, we need to define it for Capacitor
import { registerPlugin } from '@capacitor/core';
@@ -28,9 +29,21 @@ class AntiSpoofingService {
console.warn('Anti-spoofing detection is only available on native platforms')
return
}
console.log('Anti-spoofing service initialized')
// Perform a check on initialization
this.performSecurityCheck()
console.log('Anti-spoofing service initialized, but security checks are deferred until after login.')
}
/**
* Start periodic security checks. This should be called after a user is authenticated.
*/
startSecurityChecks() {
if (!this.isNative) {
return;
}
console.log('Starting periodic security checks...');
// Perform an immediate check
this.performSecurityCheck();
// And then check periodically (e.g., every 5 minutes)
setInterval(() => this.performSecurityCheck(), 5 * 60 * 1000);
}
/**
@@ -61,17 +74,27 @@ class AntiSpoofingService {
if (!this.isNative) {
return
}
console.log('Performing security check...')
console.log('ANTI-SPOOFING: Performing security check...')
try {
const isAuthenticated = await authService.isAuthenticated();
if (!isAuthenticated) {
console.warn('ANTI-SPOOFING: User not authenticated, skipping security check.');
return;
}
console.log('ANTI-SPOOFING: User is authenticated, proceeding with security check.');
const blacklist = await this.fetchBlacklistFromServer()
console.log('Blacklist:', blacklist)
console.log('ANTI-SPOOFING: Blacklist:', blacklist)
const result = await AppSecurity.getInstalledApps()
const installedApps = result.packages
console.log('Installed apps:', installedApps)
console.log('ANTI-SPOOFING: Installed apps:', installedApps)
for (const appPackage of installedApps) {
if (blacklist.includes(appPackage)) {
console.warn(`Blacklisted app detected: ${appPackage}`)
console.warn(`ANTI-SPOOFING: Blacklisted app detected: ${appPackage}`)
// Dispatch an event to notify the UI
const event = new CustomEvent('app-blocked', {
detail: {
appName: appPackage,
@@ -79,12 +102,18 @@ class AntiSpoofingService {
},
})
window.dispatchEvent(event)
// Force clock out the user with a note
console.log('ANTI-SPOOFING: Forcing clock out due to blacklisted app.');
await authService.forceClockOut('Blacklisted App Detected')
// Stop checking after finding one blacklisted app
return
}
}
console.log('ANTI-SPOOFING: Security check complete, no blacklisted apps found.');
} catch (error) {
console.error('Security check failed:', error)
console.error('ANTI-SPOOFING: Security check failed:', error)
}
}
}
+37
View File
@@ -423,6 +423,43 @@ class AuthService {
localStorage.removeItem(key)
}
}
async forceClockOut(note) {
try {
const userId = await this.getUserId()
if (!userId) {
console.warn('No user ID found, cannot force clock out.')
return
}
// Get current location to associate with the clock-out event
let latitude, longitude
latitude = 0
longitude = 0
await apiFetch('/api/clock', {
method: 'POST',
body: JSON.stringify({
userId,
eventType: 'clock_out',
notes: note,
latitude,
longitude,
qrCodeValue: 'FORCE_CLOCK_OUT' // Use a special value for QR code
}),
})
// Perform a full logout on the client-side
await this.logout()
// Dispatch a global event to notify the UI
window.dispatchEvent(new CustomEvent('user-forced-clock-out', { detail: { note } }))
console.log(`User ${userId} has been forcibly clocked out with note: ${note}`)
} catch (error) {
console.error('Failed to force clock out:', error)
}
}
}
// Create and export a singleton instance
+3
View File
@@ -64,6 +64,7 @@ import { useI18n } from 'vue-i18n'
import { ArrowRightOnRectangleIcon } from '@heroicons/vue/24/outline'
import { authService } from '@/services/authService.js'
import { nativeServicesManager } from '@/services/nativeServicesManager.js'
import { antiSpoofingService } from '@/services/antiSpoofingService.js'
const { t } = useI18n()
@@ -103,6 +104,8 @@ const handleLogin = async () => {
try {
await nativeServicesManager.onUserLogin()
console.log('✅ NATIVE SERVICES STARTED')
// Start anti-spoofing checks after native services are ready
antiSpoofingService.startSecurityChecks();
} catch (serviceError) {
console.error('❌ NATIVE SERVICES FAILED:', serviceError)
}
+8 -1
View File
@@ -164,6 +164,8 @@ const sendClockEvent = async (qrCodeValue, latitude, longitude) => {
}
onMounted(async () => {
window.addEventListener('user-forced-clock-out', handleForcedClockOut)
if (!userId) {
userId = await authService.getUserId()
}
@@ -207,6 +209,8 @@ onMounted(async () => {
})
onBeforeUnmount(() => {
window.removeEventListener('user-forced-clock-out', handleForcedClockOut)
if (html5QrCode && html5QrCode.isScanning) {
stopScanner()
}
@@ -219,6 +223,10 @@ onBeforeUnmount(() => {
}
})
const handleForcedClockOut = () => {
isClockedIn.value = false
}
const clearMessages = () => {
errorMessage.value = ''
successMessage.value = ''
@@ -292,4 +300,3 @@ const onScanFailure = () => {
// errorMessage.value = t('qrNotDetectedTryAgain')
}
</script>