纯前端实现项目过期
项目过期逻辑
延长方法:修改 EXPIRY_DATE 和 BUILD_DATE 和 EXPIRY_VERSION
/**
* 项目有效期检查工具
* 设置项目在指定日期后无法访问
*
* 注意:此方案通过多重验证增加绕过难度,但无法100%防止修改系统时间
* 在纯前端无网络环境下,所有验证最终都依赖系统时间
*/
// 设置过期日期(延长到2025年12月31日)
const EXPIRY_DATE = new Date('2025-10-30 23:59:59')
const EXPIRY_TIMESTAMP = EXPIRY_DATE.getTime()
// 项目部署/编译时间 - 当前时间不应该早于这个时间(根据实际情况修改)
// 注意:这个日期应该设置为项目实际构建的日期,不是过期日期!
const BUILD_DATE = new Date('2025-10-14 00:00:00')
const BUILD_TIMESTAMP = BUILD_DATE.getTime()
// 过期配置版本号 - 每次修改过期日期时递增,用于自动清除旧的锁定标记
const EXPIRY_VERSION = '1' // 从'1'改为'2'表示这是新的过期配置
// LocalStorage 存储键(使用隐蔽的键名)
const STORAGE_KEYS = {
FIRST_ACCESS: '_app_ft_', // 首次访问时间
LAST_ACCESS: '_app_lt_', // 最后访问时间
MAX_TIMESTAMP: '_app_mt_', // 历史最大时间戳
ACCESS_COUNT: '_app_ac_', // 访问次数
VERIFICATION_HASH: '_app_vh_', // 验证哈希
EXPIRED_LOCK: '_app_el_', // 过期锁定标记(一旦过期就永久锁定)
EXPIRY_VERSION: '_app_ev_', // 过期配置版本号
}
// SessionStorage 备份键(防止清除localStorage)
const SESSION_KEYS = {
BACKUP: '_app_session_backup_',
}
// Cookie 备份键
const COOKIE_NAME = '_app_ck_'
/**
* 简单的字符串哈希函数
*/
function simpleHash(str: string): string {
let hash = 0
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i)
hash = ((hash << 5) - hash) + char
hash = hash & hash
}
return Math.abs(hash).toString(36)
}
/**
* 设置Cookie
*/
function setCookie(name: string, value: string, days: number = 365): void {
try {
const expires = new Date()
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000)
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Strict`
} catch (error) {
console.error('设置Cookie失败:', error)
}
}
/**
* 获取Cookie
*/
function getCookie(name: string): string | null {
try {
const nameEQ = name + '='
const ca = document.cookie.split(';')
for (let i = 0; i < ca.length; i++) {
let c: string | undefined = ca[i]
if (!c) continue
while (c.charAt(0) === ' ') {
c = c.substring(1, c.length)
}
if (c.indexOf(nameEQ) === 0) {
return c.substring(nameEQ.length, c.length)
}
}
return null
} catch (error) {
console.error('读取Cookie失败:', error)
return null
}
}
/**
* 从多个存储位置读取时间戳
*/
function getStoredTimestamp(key: string): number | null {
try {
// 优先从localStorage读取
const localValue = localStorage.getItem(key)
if (localValue) return parseInt(localValue, 10)
// 尝试从sessionStorage读取
const sessionData = sessionStorage.getItem(SESSION_KEYS.BACKUP)
if (sessionData) {
const data = JSON.parse(sessionData)
if (data[key]) return parseInt(data[key], 10)
}
// 尝试从Cookie读取
const cookieValue = getCookie(COOKIE_NAME)
if (cookieValue) {
const data = JSON.parse(decodeURIComponent(cookieValue))
if (data[key]) return parseInt(data[key], 10)
}
return null
} catch {
return null
}
}
/**
* 保存到多个存储位置
*/
function setStoredValue(key: string, value: string): void {
try {
// 保存到localStorage
localStorage.setItem(key, value)
// 备份到sessionStorage
const sessionData = sessionStorage.getItem(SESSION_KEYS.BACKUP)
const data = sessionData ? JSON.parse(sessionData) : {}
data[key] = value
sessionStorage.setItem(SESSION_KEYS.BACKUP, JSON.stringify(data))
// 备份到Cookie
const cookieData = getCookie(COOKIE_NAME)
const cookieObj = cookieData ? JSON.parse(decodeURIComponent(cookieData)) : {}
cookieObj[key] = value
setCookie(COOKIE_NAME, encodeURIComponent(JSON.stringify(cookieObj)), 365)
} catch (error) {
console.error('保存数据失败:', error)
}
}
/**
* 生成验证哈希
*/
function generateVerificationHash(timestamp: number, count: number): string {
return simpleHash(`${timestamp}_${count}_${EXPIRY_TIMESTAMP}`)
}
/**
* 检测时间是否被回调或异常
*/
function detectTimeRollback(): boolean {
const currentTime = Date.now()
try {
// 检查0: 当前时间不应该早于项目构建时间
if (currentTime < BUILD_TIMESTAMP) {
console.warn('检测到时间异常:当前时间早于项目构建时间')
return true
}
// 检查1: 是否早于历史最大时间戳
const maxTimestamp = getStoredTimestamp(STORAGE_KEYS.MAX_TIMESTAMP)
if (maxTimestamp && currentTime < maxTimestamp - 60000) { // 允许1分钟误差
console.warn('检测到时间异常:当前时间早于历史记录')
return true
}
// 检查2: 是否早于最后访问时间
const lastAccess = getStoredTimestamp(STORAGE_KEYS.LAST_ACCESS)
if (lastAccess && currentTime < lastAccess - 60000) {
console.warn('检测到时间异常:当前时间早于上次访问')
return true
}
// 检查3: 是否早于首次访问时间
const firstAccess = getStoredTimestamp(STORAGE_KEYS.FIRST_ACCESS)
if (firstAccess && currentTime < firstAccess) {
console.warn('检测到时间异常:当前时间早于首次访问')
return true
}
return false
} catch (error) {
console.error('时间检测出错:', error)
return false
}
}
/**
* 更新访问记录
*/
function updateAccessRecord(): void {
const currentTime = Date.now()
try {
// 设置首次访问时间
const firstAccess = getStoredTimestamp(STORAGE_KEYS.FIRST_ACCESS)
if (!firstAccess) {
setStoredValue(STORAGE_KEYS.FIRST_ACCESS, currentTime.toString())
}
// 更新最后访问时间
setStoredValue(STORAGE_KEYS.LAST_ACCESS, currentTime.toString())
// 更新历史最大时间戳
const maxTimestamp = getStoredTimestamp(STORAGE_KEYS.MAX_TIMESTAMP)
if (!maxTimestamp || currentTime > maxTimestamp) {
setStoredValue(STORAGE_KEYS.MAX_TIMESTAMP, currentTime.toString())
}
// 更新访问次数
const countStr = localStorage.getItem(STORAGE_KEYS.ACCESS_COUNT) ||
sessionStorage.getItem(STORAGE_KEYS.ACCESS_COUNT) || '0'
const count = parseInt(countStr, 10)
const newCount = count + 1
setStoredValue(STORAGE_KEYS.ACCESS_COUNT, newCount.toString())
// 更新验证哈希
const hash = generateVerificationHash(currentTime, newCount)
setStoredValue(STORAGE_KEYS.VERIFICATION_HASH, hash)
} catch (error) {
console.error('更新访问记录失败:', error)
}
}
/**
* 验证数据完整性
*/
function verifyDataIntegrity(): boolean {
try {
const lastAccess = localStorage.getItem(STORAGE_KEYS.LAST_ACCESS)
const count = localStorage.getItem(STORAGE_KEYS.ACCESS_COUNT)
const storedHash = localStorage.getItem(STORAGE_KEYS.VERIFICATION_HASH)
if (!lastAccess || !count || !storedHash) {
return true // 首次访问,验证通过
}
const expectedHash = generateVerificationHash(parseInt(lastAccess, 10), parseInt(count, 10))
if (expectedHash !== storedHash) {
console.warn('检测到数据完整性异常')
return false
}
return true
} catch (error) {
console.error('数据完整性验证出错:', error)
return true // 出错时放行,避免误判
}
}
/**
* 检查是否存在过期锁定标记
*/
function isLocked(): boolean {
try {
// 从多个存储位置检查锁定标记
const localLock = localStorage.getItem(STORAGE_KEYS.EXPIRED_LOCK)
const sessionData = sessionStorage.getItem(SESSION_KEYS.BACKUP)
const sessionLock = sessionData ? JSON.parse(sessionData)[STORAGE_KEYS.EXPIRED_LOCK] : null
const cookieValue = getCookie(COOKIE_NAME)
const cookieLock = cookieValue ? JSON.parse(decodeURIComponent(cookieValue))[STORAGE_KEYS.EXPIRED_LOCK] : null
// 只要任何一个位置有锁定标记,就认为已锁定
return localLock === 'true' || sessionLock === 'true' || cookieLock === 'true'
} catch {
return false
}
}
/**
* 设置过期锁定标记
*/
function setExpiredLock(): void {
try {
const lockValue = 'true'
setStoredValue(STORAGE_KEYS.EXPIRED_LOCK, lockValue)
console.warn('项目已过期并锁定')
} catch (error) {
console.error('设置过期锁定失败:', error)
}
}
/**
* 检查项目是否已过期
* @returns {boolean} true表示已过期,false表示未过期
*/
export function isExpired(): boolean {
const currentDate = new Date()
const currentTime = currentDate.getTime()
// 0. 检查版本号,如果版本不匹配,自动清除旧的锁定标记(延长过期时间时使用)
const storedVersion = localStorage.getItem(STORAGE_KEYS.EXPIRY_VERSION)
if (storedVersion !== EXPIRY_VERSION) {
console.log('检测到过期配置已更新,清除旧的锁定标记')
clearExpiredLock()
setStoredValue(STORAGE_KEYS.EXPIRY_VERSION, EXPIRY_VERSION)
}
// 1. 检查过期锁定标记(在版本检查之后)
if (isLocked()) {
console.warn('检测到过期锁定标记,项目已被永久锁定')
return true
}
// 2. 基础过期检查
if (currentTime > EXPIRY_TIMESTAMP) {
setExpiredLock() // 设置过期锁定
return true
}
// 3. 检测时间回调
if (detectTimeRollback()) {
console.warn('检测到时间被回调,视为已过期')
setExpiredLock() // 设置过期锁定
return true
}
// 4. 验证数据完整性
if (!verifyDataIntegrity()) {
console.warn('数据完整性验证失败,视为已过期')
setExpiredLock() // 设置过期锁定
return true
}
// 5. 更新访问记录
updateAccessRecord()
return false
}
/**
* 获取过期日期
* @returns {Date} 过期日期
*/
export function getExpiryDate(): Date {
return EXPIRY_DATE
}
/**
* 获取剩余天数
* @returns {number} 剩余天数,如果已过期则返回负数
*/
export function getRemainingDays(): number {
const currentDate = new Date()
const timeDiff = EXPIRY_TIMESTAMP - currentDate.getTime()
return Math.ceil(timeDiff / (1000 * 3600 * 24))
}
/**
* 格式化日期显示
* @param date 日期对象
* @returns {string} 格式化后的日期字符串
*/
export function formatDate(date: Date): string {
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
}
/**
* 清除过期锁定标记(延长过期时间时使用)
* 使用场景:修改了EXPIRY_DATE延长过期时间后,需要清除旧的锁定标记
*/
export function clearExpiredLock(): void {
try {
// 清除localStorage中的锁定标记
localStorage.removeItem(STORAGE_KEYS.EXPIRED_LOCK)
// 清除sessionStorage中的锁定标记
const sessionData = sessionStorage.getItem(SESSION_KEYS.BACKUP)
if (sessionData) {
const data = JSON.parse(sessionData)
delete data[STORAGE_KEYS.EXPIRED_LOCK]
sessionStorage.setItem(SESSION_KEYS.BACKUP, JSON.stringify(data))
}
// 清除Cookie中的锁定标记
const cookieValue = getCookie(COOKIE_NAME)
if (cookieValue) {
const cookieObj = JSON.parse(decodeURIComponent(cookieValue))
delete cookieObj[STORAGE_KEYS.EXPIRED_LOCK]
setCookie(COOKIE_NAME, encodeURIComponent(JSON.stringify(cookieObj)), 365)
}
console.log('过期锁定标记已清除')
} catch (error) {
console.error('清除过期锁定标记失败:', error)
}
}
/**
* 重置所有验证数据(仅用于开发测试)
* 注意:这会清除所有历史记录,包括首次访问时间、访问计数等
*/
export function resetVerification(): void {
try {
// 清除localStorage
Object.values(STORAGE_KEYS).forEach(key => {
localStorage.removeItem(key)
})
// 清除sessionStorage
sessionStorage.removeItem(SESSION_KEYS.BACKUP)
// 清除Cookie
document.cookie = `${COOKIE_NAME}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;`
console.log('所有验证数据已重置')
} catch (error) {
console.error('重置验证数据失败:', error)
}
}
// 开发环境下暴露到全局对象(方便调试)
if (typeof window !== 'undefined' && import.meta.env.DEV) {
interface WindowWithDebug extends Window {
__clearExpiredLock?: () => void
__resetVerification?: () => void
}
(window as WindowWithDebug).__clearExpiredLock = clearExpiredLock;
(window as WindowWithDebug).__resetVerification = resetVerification;
console.log('开发模式:已暴露调试函数到全局');
console.log('使用 window.__clearExpiredLock() 清除过期锁定');
console.log('使用 window.__resetVerification() 重置所有验证数据');
}
项目过期页面
<template>
<div class="expired-wrapper">
<div class="expired-panel">
<div class="expired-panel-header">
<svg class="expired-panel-icon" viewBox="0 0 1024 1024" width="68" height="68">
<path fill="#D32F2F" d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 0 1 0-96 48.01 48.01 0 0 1 0 96z"/>
</svg>
<div class="expired-title">
系统授权已到期
</div>
</div>
<div class="divider"></div>
<div class="expired-message">
<p>
您好,当前系统的使用有效期已到期。为保证您的账号和数据安全,系统已暂停访问。
</p>
<div class="expired-info-row">
<svg class="expired-panel-clock" viewBox="0 0 1024 1024" width="16" height="16">
<path fill="#D32F2F" d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"/>
<path fill="#D32F2F" d="M686.7 638.6L544.1 535.5V288c0-4.4-3.6-8-8-8H488c-4.4 0-8 3.6-8 8v275.4c0 2.6 1.2 5 3.3 6.5l165.4 120.6c3.6 2.6 8.6 1.8 11.2-1.7l28.6-39c2.6-3.7 1.8-8.7-1.8-11.2z"/>
</svg>
<span>到期时间:{{ formatDate(expiryDate) }}</span>
</div>
<div class="expired-alert">
<svg class="alert-icon" viewBox="0 0 1024 1024" width="16" height="16">
<path fill="#E6A23C" d="M955.7 856l-416-720c-6.2-10.7-16.9-16-27.7-16s-21.6 5.3-27.7 16l-416 720C56 877.4 71.4 904 96 904h832c24.6 0 40-26.6 27.7-48zM480 416c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v184c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V416zm32 352a48.01 48.01 0 0 1 0-96 48.01 48.01 0 0 1 0 96z"/>
</svg>
<span>如需续期或有其他需求,请联系相关负责人</span>
</div>
</div>
<div class="divider"></div>
<div class="expired-panel-actions">
<button class="exp-btn exp-btn-primary" @click="refreshPage">
<svg class="btn-icon" viewBox="0 0 1024 1024" width="16" height="16">
<path fill="currentColor" d="M909.1 209.3l-56.4 44.1C775.8 155.1 656.2 92 521.9 92 290 92 102.3 279.5 102 511.5 101.7 743.7 289.8 932 521.9 932c181.3 0 335.8-115 394.6-276.1 1.5-4.2-.7-8.9-4.9-10.3l-56.7-19.5a8 8 0 0 0-10.1 4.8c-1.8 5-3.8 10-5.9 14.9-17.3 41-42.1 77.8-73.7 109.4A344.77 344.77 0 0 1 655.9 856c-42.3 17.9-87.4 27-133.8 27-46.5 0-91.5-9.1-133.8-27A344.77 344.77 0 0 1 279 755.8a344.72 344.72 0 0 1-73.7-109.4c-17.9-42.4-27-87.4-27-133.9s9.1-91.5 27-133.9c17.3-41 42.1-77.8 73.7-109.4 31.6-31.6 68.4-56.4 109.3-73.8 42.3-17.9 87.4-27 133.8-27 46.5 0 91.5 9.1 133.8 27a344.77 344.77 0 0 1 109.3 73.8c9.9 9.9 19.2 20.4 27.8 31.4l-60.2 47a8 8 0 0 0 3 14.1l175.6 43c5 1.2 9.9-2.6 9.9-7.7l.8-180.9c-.1-6.6-7.8-10.3-13-6.2z"/>
</svg>
刷新
</button>
<button class="exp-btn exp-btn-default" @click="closeWindow">
<svg class="btn-icon" viewBox="0 0 1024 1024" width="16" height="16">
<path fill="currentColor" d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 0 0 203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"/>
</svg>
关闭
</button>
</div>
<div class="expired-panel-footer">
© {{ new Date().getFullYear() }} 杭州汇知思行 版权所有
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { getExpiryDate, formatDate } from '@/utils/expiryCheck'
const expiryDate = getExpiryDate()
const refreshPage = () => {
window.location.reload()
}
const closeWindow = () => {
window.close()
if (!window.closed) {
// 简单的警告提示
setTimeout(() => {
alert('如无法自动关闭,请手动关闭当前页面')
}, 100)
}
}
</script>
<style scoped>
.expired-wrapper {
min-height: 100vh;
width: 100vw;
background: #f4f6fa;
display: flex;
justify-content: center;
align-items: center;
}
.expired-panel {
background: #fff;
border-radius: 10px;
padding: 48px 40px 24px 40px;
box-shadow: 0 3px 24px 0 rgba(44, 41, 80, 0.10);
min-width: 310px;
max-width: 405px;
width: 94vw;
display: flex;
flex-direction: column;
align-items: center;
}
.expired-panel-header {
width: 100%;
text-align: center;
margin-bottom: 12px;
}
.expired-panel-icon {
margin-bottom: 8px;
}
.expired-title {
font-size: 1.45rem;
font-weight: 600;
color: #D32F2F;
margin-bottom: 4px;
letter-spacing: 1px;
}
.expired-message {
text-align: left;
width: 100%;
color: #444;
font-size: 1rem;
margin-bottom: 6px;
padding: 0 2px;
}
.expired-info-row {
display: flex;
align-items: center;
font-size: 1.01rem;
font-weight: 500;
margin: 16px 0 12px 0;
color: #D32F2F;
gap: 7px;
}
/* 分割线样式 */
.divider {
width: 100%;
height: 1px;
background: #e8e8e8;
margin: 20px 0;
}
/* 警告框样式 */
.expired-alert {
display: flex;
align-items: center;
gap: 8px;
margin: 10px 0 18px 0;
padding: 8px 12px;
font-size: 15px;
border-radius: 4px;
background: #fff8ea;
border: 1px solid #faecd8;
color: #8b7330;
line-height: 1.5;
}
.alert-icon {
flex-shrink: 0;
}
.expired-panel-actions {
display: flex;
justify-content: center;
gap: 14px;
margin-top: 14px;
margin-bottom: 6px;
width: 100%;
}
/* 按钮基础样式 */
.exp-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 22px;
font-size: 15px;
border-radius: 16px;
border: 1px solid;
cursor: pointer;
transition: all 0.3s;
font-weight: 500;
outline: none;
}
.btn-icon {
flex-shrink: 0;
}
/* 主要按钮样式 */
.exp-btn-primary {
background: #409eff;
border-color: #409eff;
color: #fff;
}
.exp-btn-primary:hover {
background: #66b1ff;
border-color: #66b1ff;
}
.exp-btn-primary:active {
background: #3a8ee6;
border-color: #3a8ee6;
}
/* 默认按钮样式 */
.exp-btn-default {
background: #fff;
border-color: #dcdfe6;
color: #606266;
}
.exp-btn-default:hover {
color: #409eff;
border-color: #c6e2ff;
background: #ecf5ff;
}
.exp-btn-default:active {
color: #3a8ee6;
border-color: #3a8ee6;
}
.expired-panel-footer {
text-align: center;
width: 100%;
margin-top: 20px;
color: #bdbdbd;
font-size: 13px;
letter-spacing: 1px;
border-top: 1px solid #f0f0f0;
padding-top: 8px;
}
@media (max-width: 600px) {
.expired-panel {
padding: 22px 5vw 18px 5vw;
min-width: unset;
max-width: 98vw;
}
.expired-title {
font-size: 1.12rem;
}
.expired-panel-footer {
font-size: 12px;
padding-top: 6px;
margin-top: 11px;
}
}
</style>
使用地点 main.ts
import './assets/main.css'
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import { createPinia } from 'pinia'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
import router from './router'
import { isExpired } from './utils/expiryCheck'
import ExpiredPage from './components/ExpiredPage.vue'
import 'element-plus/dist/index.css'
if(isExpired()){
// 如果已过期,显示过期页面
const expiredApp = createApp(ExpiredPage)
expiredApp.mount('#app')
} else {
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.mount('#app')
}

浙公网安备 33010602011771号