feat(安全): 实现反欺诈检测和强制打卡功能
- 添加反欺诈服务,定期检查设备上的黑名单应用 - 检测到黑名单应用时自动强制用户打卡下班 - 在登录后启动反欺诈检查,每5分钟执行一次 - 更新后端API支持强制打卡事件 - 调整Android最低SDK版本至24 - 添加全局事件监听处理强制打卡事件
This commit is contained in:
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user