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

@@ -13,6 +13,8 @@
},
"dependencies": {
"@kintone/rest-api-client": "^5.7.5",
"jsrsasign": "^11.1.0",
"jsrsasign-util": "^1.0.5",
"kintone-ui-component": "1.22.0",
"rollup-plugin-css-only": "^4.5.2",
"vue": "^3.5.13",
@@ -23,6 +25,7 @@
"@kintone/dts-gen": "^8.1.1",
"@kintone/plugin-packer": "^8.1.3",
"@kintone/plugin-uploader": "^9.1.2",
"@types/jsrsasign": "^10.5.15",
"@types/node-rsa": "^1.1.4",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/tsconfig": "^0.7.0",

28
rsa_private.pem Normal file
View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDOAqDRabJn6vga
YXnWuYKPomQVNV0R0HPmuN1N4qo7OMDkFH2mln86w68/9/bHMb7TcVaFGxNblpe2
tIjr/u/XfnS2+NK6ckTWOhmPUrqLG6A3a4v49dymvFdfyjAYOCBTlKzrdQTjdFAX
5EUE007n/G+RxYiEAm0NXpVqa+aw/Yvm5+Lr7GrKpyamAAygu5jYDE3rateyfJ1h
P2fEXEqR4RPD5pgyKD+4ZVbygaYqAw4rZu4OOZ4QtPCtksSenn94Yx9UUlex3TNW
oTBrFp+8z9wjg+oF6/5Ai/PsXLDpbkIusnQo1yYvrULwaAHBvL6wgj1FOL+g6Tmp
P6MhU7EbAgMBAAECggEAAyia0vXQv0jPgAa26a9ozI9RWIU7xZOmy0anV1Peh0+X
SYf05efQez6tLQkTbA9xB73o9TEqlVC+8mqx32hxFffsOIf3zIBcWiqEN3nYaVya
6BlKiUkqa8C0guqkh80zKwTlt9XQpYokK5GblfeFcgd2pctbt1Ea30XFewm80kGH
OrHP4bi25KYkS+6PL5wUI+OqfaR/LsWevwt+cLlZPGmDDgE6+2Bvcg0JWnKKwk1k
jhKpSAek6zUkweaysxx1BscNBvGtDnRRRy0+2jMK66ad80Cg0G8xLQ/tu8kBwfml
T/9ViSRUjUrMdcfSVkrdOV3xAsOXdWspz1aljJAwcQKBgQDxebBimIBgUhNJ+2/9
27OQXsHLBYbqPurR4fKCM7a5qAIpXJcgth0YAuNvJT+rxEHTPypQCTF1fvvJuU+v
w1HjUz1CHzGYWdj0xibQDdtgkPlKqQQeT3Cnpwy3RkT4IF6FCH9YfKcUzYdamVZy
S8kFqLzEStwWIoaQX2HoBZLoUQKBgQDaZtZJoIHr+d0fLz+7rAoB8WEBA8lOaEhU
/OHwrUbuAez6k7zEDsTXgLuB8N34p1yMSaOqdGJ8rj3Pg8AlrXGELfXOF7Fe9zG0
LxujNmftmJKE8XTIALPpSBJgF5LDnh13LcNMd4b6w0DDjF7wCA2T3DTLjjqi3fo7
nDW9WQyTqwKBgEXlbXL8pZw75a1yhHY80/skEoBLt0OytpHODz407d1LjmSekng7
fqxmmaKga4+ynUMic4L7Rj+2Y/d+FlzP8rIUdBThpp9s0mn3uWBbwnZvQFmmFrUX
VYqRxhJ+2pPf+rwTO5lHa62P2HAXFni7CxMCRrGi4ZXepIjBsztP8bghAoGBAMj5
Bsmb1NJkDBGNNhWpm0/sYbpAVLc9CQqD5hnGKdYMmZh/6J11hbdVM7bAAlK1F1nU
zbGmBZb7888ISwGg2Cus61tpvANKb0eCbelDwGEIHBQP6Mm+s8/ATYB1UM2Hq0+n
Iec0ulX45JjNi/NPRcdBRKfnypdissi111HVJtifAoGADrT/t2j1bMHd1zY76Jv5
M/BWhOAgHwBo683ROEL3p6G98Xla2kd7hP6NEdo6OLJnlfK3RfoPWnotLZKEsfQM
Wog9GAcYtQbDDaYWxNBgyscSdVousY1EXs+t5vkt3XSIwUwLcEVJOu7/Fvt+YXpn
nR7aD8F62ARYxI+bv+bQR5w=
-----END PRIVATE KEY-----

9
rsa_public.pem Normal file
View File

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzgKg0WmyZ+r4GmF51rmC
j6JkFTVdEdBz5rjdTeKqOzjA5BR9ppZ/OsOvP/f2xzG+03FWhRsTW5aXtrSI6/7v
1350tvjSunJE1joZj1K6ixugN2uL+PXcprxXX8owGDggU5Ss63UE43RQF+RFBNNO
5/xvkcWIhAJtDV6VamvmsP2L5ufi6+xqyqcmpgAMoLuY2AxN62rXsnydYT9nxFxK
keETw+aYMig/uGVW8oGmKgMOK2buDjmeELTwrZLEnp5/eGMfVFJXsd0zVqEwaxaf
vM/cI4PqBev+QIvz7Fyw6W5CLrJ0KNcmL61C8GgBwby+sII9RTi/oOk5qT+jIVOx
GwIDAQAB
-----END PUBLIC KEY-----

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);
// 如果获取失败,清空本地存储