This commit is contained in:
2025-10-23 23:52:26 +08:00
parent 3961285b32
commit e86afd8487
8 changed files with 188 additions and 50 deletions

View File

@@ -21,9 +21,9 @@
<script setup lang="ts">
import { ref, onMounted, computed, shallowRef } from 'vue';
import { useI18n } from 'vue-i18n';
import { LicenseService } from '@/services/LicenseService';
import { LicenseService } from '@/services/license-service';
import type { LicenseInfo } from '@/types/license';
import { LicenseStorage } from '@/utils/LicenseStorage';
import { LicenseStorage } from '@/utils/license-storage';
const { t: $t } = useI18n();// 配置国际化

View File

@@ -1,6 +1,6 @@
import i18n from '@/i18n';
import type { Setting } from '@/types';
import { LicenseService } from '@/services/LicenseService';
import { LicenseService } from '@/services/license-service';
import client from '@/plugins/kintoneClient.ts'
import { Button } from 'kintone-ui-component/lib/button';

View File

@@ -1,6 +1,6 @@
import i18n from '@/i18n';
import type { Setting } from '@/types';
import { LicenseService } from '@/services/LicenseService';
import { LicenseService } from '@/services/license-service';
import client from '@/plugins/kintoneClient.ts'
import { MobileButton } from 'kintone-ui-component/lib/mobile/button';

View File

@@ -1,10 +1,13 @@
import type { LicenseInfo, LicenseCheckResult } from '@/types/license';
import i18n from '@/i18n';
import { LicenseStorage } from '@/utils/LicenseStorage';
import { LicenseStorage } from '@/utils/license-storage';
import { PermissionService } from '@/utils/permissions';
import { Notification } from 'kintone-ui-component/lib/notification';
import { MobileNotification } from 'kintone-ui-component/lib/mobile/notification';
import manifestJson from '@/manifest.json';
import { KJUR } from 'jsrsasign';
import rsaPublicKey from '../../rsa_public.pem?raw';
import rsaPrivateKey from '../../rsa_private.pem?raw';
const { t: $t } = i18n.global;
@@ -22,6 +25,16 @@ export class LicenseService {
return window.location.hostname;
}
/**
*
*/
static getPluginName(): string {
const pluginName = manifestJson.name as Record<string, string>;
const userLang = kintone.getLoginUser().language;
const plugin = pluginName[userLang] || pluginName.ja;
return plugin;
}
/**
* ID
*/
@@ -31,37 +44,50 @@ export class LicenseService {
/**
*
* / true
* JWT结构payload进行原有检查
*/
static checkLicenseAvailable(license: LicenseInfo) {
const domain = this.getDomain()
const pluginId = this.getPluginId()
// TODO jwt null
// 检查域名和插件ID是否与当前环境一致
if (license.domain !== domain || license.pluginId !== pluginId) {
static checkLicenseAvailable(jwtString: string, decodedJWT: KJUR.jws.JWS.JWSResult): boolean {
try {
// 验证完整的JWT
const result = KJUR.jws.JWS.verifyJWT(jwtString, rsaPublicKey.trim(), {alg: ['RS256']});
if (!result) {
console.warn('JWT signature verification failed');
return false;
}
// 检查是否付费
if (license.isPaid) {
return true;
}
} catch (error) {
console.error('JWT verification or parsing failed:', error);
return false;
}
// JWT验证通过从payloadPP解析license进行原有检查
const license: LicenseInfo = JSON.parse(decodedJWT.payloadPP);
// 检查存储是否过期(是否同一天)
if (!this.isToday(license.fetchTime)) {
// 不是同一天,已过期
return false;
}
const domain = this.getDomain()
const pluginId = this.getPluginId()
// 检查试用是否到期
const expiredTime = new Date(license.expiredTime);
if (expiredTime < new Date()) {
return false;
}
// 检查域名和插件ID是否与当前环境一致
if (license.domain !== domain || license.pluginId !== pluginId) {
return false;
}
return true
// 检查是否付费
if (license.isPaid) {
return true;
}
// 检查存储是否过期(是否同一天)
if (!this.isToday(license.fetchTime)) {
// 不是同一天,已过期
return false;
}
// 检查试用是否到期
const expiredTime = new Date(license.expiredTime);
if (expiredTime < new Date()) {
return false;
}
return true;
}
static isToday(timestamp: number): boolean {
@@ -89,14 +115,29 @@ export class LicenseService {
*/
static async checkLicenseRemote(): Promise<LicenseCheckResult> {
try {
// 这里应该是实际的API调用暂时模拟创建试用许可证
// 这里应该是实际的API调用暂时模拟创建加密的试用许可证
const response = await this.mockRemoteCheck(this.getDomain(), this.getPluginId());
const license = response.license!;
LicenseStorage.saveLicense(license);
if (!response.success || !response.jwt) {
return {
isLicenseValid: false,
isRemote: true,
};
}
const jwt = response.jwt;
// 保存 JWT 到本地存储,获取解码后的结构
const decodedJWT = LicenseStorage.saveLicense(jwt);
// 从解码后的JWT提取许可证信息
const license: LicenseInfo = JSON.parse(decodedJWT.payloadPP);
// 使用解码后的JWT进行验证
const isValid = this.checkLicenseAvailable(jwt, decodedJWT);
return {
isLicenseValid: this.checkLicenseAvailable(license),
isLicenseValid: isValid,
license,
isRemote: true,
};
@@ -104,7 +145,8 @@ export class LicenseService {
} catch (error) {
console.error($t('license.error.fetchFailed', { e: error }));
return {
isLicenseValid: true,
isLicenseValid: false,
isRemote: true,
};
}
}
@@ -146,7 +188,7 @@ export class LicenseService {
*/
static async showNotification(license: LicenseInfo | undefined, isWarning: boolean) {
let message;
const plugin = manifestJson.name[kintone.getLoginUser().language];
const plugin = this.getPluginName();
if (isWarning) {
// 尚未到期
const remainingDays = this.getDaysRemaining(license!.expiredTime);
@@ -228,7 +270,15 @@ export class LicenseService {
*
*/
static getLocalLicenseInfo(): LicenseInfo | null {
return LicenseStorage.getLicense(this.getPluginId());
const decodedJWT = LicenseStorage.getLicense(this.getPluginId());
if (!decodedJWT) return null;
try {
return JSON.parse(decodedJWT.payloadPP) as LicenseInfo;
} catch (error) {
console.error('Failed to parse payloadPP from decoded JWT:', error);
return null;
}
}
/**
@@ -260,14 +310,36 @@ export class LicenseService {
};
}
/**
* JWT token
*/
private static generateJWT(licenseInfo: LicenseInfo): string {
const header = {
alg: 'RS256',
typ: 'JWT'
};
const payload = {
...licenseInfo,
iat: Math.floor(Date.now() / 1000)
};
const sHeader = JSON.stringify(header);
const sPayload = JSON.stringify(payload);
const jwt = KJUR.jws.JWS.sign(null, sHeader, sPayload, rsaPrivateKey.trim());
return jwt;
}
/**
* API替换
*/
private static async mockRemoteCheck(domain: string, pluginId: string): Promise<{ success: boolean; license?: LicenseInfo }> {
// 模拟API调用这里总是返回试用许可证
private static async mockRemoteCheck(domain: string, pluginId: string): Promise<{ success: boolean; jwt?: string }> {
// 模拟API调用这里总是返回加密的试用许可证
// 生产环境这里会调用后端API: POST /api/license/check
const license = this.mockCreateTrialLicense();
const jwt = this.generateJWT(license);
// 模拟有时创建失败的情况1%概率)
if (Math.random() < 0.01) {
@@ -276,7 +348,7 @@ export class LicenseService {
return {
success: true,
license,
jwt,
};
}
}

View File

@@ -1,11 +1,24 @@
// 本地存储加密工具类
import { LicenseService } from '@/services/LicenseService';
import { LicenseService } from '@/services/license-service';
import type { LicenseInfo, LicenseSetting } from '@/types/license';
import { KJUR } from 'jsrsasign';
export class LicenseStorage {
private static readonly STORAGE_KEY_PREFIX = 'alicorns_plugin_';
private static readonly STORAGE_SETTING_KEY_PREFIX = this.STORAGE_KEY_PREFIX + 'setting_';
/**
* JWT
*/
private static parseJWT(jwt: string) {
try {
return KJUR.jws.JWS.parse(jwt);
} catch (error) {
console.error('JWT parsing failed:', error);
return null;
}
}
/**
* key
*/
@@ -14,13 +27,20 @@ export class LicenseStorage {
}
/**
*
* JWT
*/
static saveLicense(licenseInfo: LicenseInfo): void {
static saveLicense(jwt: string) {
try {
// 直接存储LicenseInfo对象其中已包含fetchTime
// 从 JWT 中提取 pluginId 以生成存储key
const decoded = this.parseJWT(jwt);
if (!decoded) {
throw new Error('Failed to parse JWT');
}
const licenseInfo = JSON.parse(decoded.payloadPP) as LicenseInfo;
const key = this.generateStorageKey(licenseInfo.pluginId);
localStorage.setItem(key, JSON.stringify(licenseInfo));
localStorage.setItem(key, jwt);
return decoded;
} catch (error) {
console.error('Failed to save license:', error);
throw error;
@@ -30,23 +50,29 @@ export class LicenseStorage {
/**
*
*/
static getLicense(pluginId: string): LicenseInfo | null {
static getLicense(pluginId: string) {
try {
const key = this.generateStorageKey(pluginId);
const storedData = localStorage.getItem(key);
const storedJWT = localStorage.getItem(key);
if (!storedData) return null;
if (!storedJWT) return null;
const parsedData: LicenseInfo = JSON.parse(storedData);
// 解码JWT
const decodedJWT = this.parseJWT(storedJWT);
if (!decodedJWT) {
// JWT解析失败清理存储
this.clearLicense(pluginId);
return null;
}
const isValid = LicenseService.checkLicenseAvailable(parsedData);
const isValid = LicenseService.checkLicenseAvailable(storedJWT, decodedJWT);
if (!isValid) {
// 获取许可证信息失败,清理存储
this.clearLicense(pluginId);
return null;
}
return parsedData;
return decodedJWT;
} catch (error) {
console.error('Failed to get license:', error);
// 如果获取失败,清空本地存储