diff --git a/packages/libs/lib-server/src/basic/constants.ts b/packages/libs/lib-server/src/basic/constants.ts index aba37e772..00dc0197a 100644 --- a/packages/libs/lib-server/src/basic/constants.ts +++ b/packages/libs/lib-server/src/basic/constants.ts @@ -12,6 +12,8 @@ export const Constants = { authOnly: '_authOnly_', //仅需要登录 loginOnly: '_authOnly_', + + open: '_open_', }, res: { serverError(message: string) { @@ -68,5 +70,29 @@ export const Constants = { code: 10001, message: '对不起,预览环境不允许修改此数据', }, + openKeyError: { + code: 20000, + message: 'openKey错误', + }, + openKeySignError: { + code: 20001, + message: 'openKey签名错误', + }, + openKeyExpiresError: { + code: 20002, + message: 'openKey时间戳错误', + }, + openKeySignTypeError: { + code: 20003, + message: 'openKey签名类型不支持', + }, + openParamError: { + code: 20010, + message: '请求参数错误', + }, + openCertNotFound: { + code: 20011, + message: '证书不存在', + }, }, }; diff --git a/packages/libs/lib-server/src/basic/exception/auth-exception.ts b/packages/libs/lib-server/src/basic/exception/auth-exception.ts index 2b027177a..65a255915 100644 --- a/packages/libs/lib-server/src/basic/exception/auth-exception.ts +++ b/packages/libs/lib-server/src/basic/exception/auth-exception.ts @@ -5,10 +5,6 @@ import { BaseException } from './base-exception.js'; */ export class AuthException extends BaseException { constructor(message) { - super( - 'AuthException', - Constants.res.auth.code, - message ? message : Constants.res.auth.message - ); + super('AuthException', Constants.res.auth.code, message ? message : Constants.res.auth.message); } } diff --git a/packages/libs/lib-server/src/basic/exception/common-exception.ts b/packages/libs/lib-server/src/basic/exception/common-exception.ts index c9f980684..1873d545f 100644 --- a/packages/libs/lib-server/src/basic/exception/common-exception.ts +++ b/packages/libs/lib-server/src/basic/exception/common-exception.ts @@ -8,3 +8,9 @@ export class CommonException extends BaseException { super('CommonException', Constants.res.error.code, message ? message : Constants.res.error.message); } } + +export class CodeException extends BaseException { + constructor(res: { code: number; message: string }) { + super('CodeException', res.code, res.message); + } +} diff --git a/packages/libs/lib-server/src/system/basic/index.ts b/packages/libs/lib-server/src/system/basic/index.ts index 859303a56..920e96115 100644 --- a/packages/libs/lib-server/src/system/basic/index.ts +++ b/packages/libs/lib-server/src/system/basic/index.ts @@ -1,2 +1,3 @@ export * from './service/plus-service.js'; export * from './service/file-service.js'; +export * from './service/encryptor.js'; diff --git a/packages/libs/lib-server/src/system/basic/service/encryptor.ts b/packages/libs/lib-server/src/system/basic/service/encryptor.ts new file mode 100644 index 000000000..ee9fe3a55 --- /dev/null +++ b/packages/libs/lib-server/src/system/basic/service/encryptor.ts @@ -0,0 +1,29 @@ +import crypto from 'crypto'; + +export class Encryptor { + secretKey: Buffer; + constructor(encryptSecret: string) { + this.secretKey = Buffer.from(encryptSecret, 'base64'); + } + // 加密函数 + encrypt(text: string) { + const iv = crypto.randomBytes(16); // 初始化向量 + // const secretKey = crypto.randomBytes(32); + // const key = Buffer.from(secretKey); + const cipher = crypto.createCipheriv('aes-256-cbc', this.secretKey, iv); + let encrypted = cipher.update(text); + encrypted = Buffer.concat([encrypted, cipher.final()]); + return iv.toString('hex') + ':' + encrypted.toString('hex'); + } + + // 解密函数 + decrypt(encryptedText: string) { + const textParts = encryptedText.split(':'); + const iv = Buffer.from(textParts.shift(), 'hex'); + const encrypted = Buffer.from(textParts.join(':'), 'hex'); + const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(this.secretKey), iv); + let decrypted = decipher.update(encrypted); + decrypted = Buffer.concat([decrypted, decipher.final()]); + return decrypted.toString(); + } +} diff --git a/packages/libs/lib-server/src/user/access/service/encrypt-service.ts b/packages/libs/lib-server/src/user/access/service/encrypt-service.ts index 409984dfb..950b5723b 100644 --- a/packages/libs/lib-server/src/user/access/service/encrypt-service.ts +++ b/packages/libs/lib-server/src/user/access/service/encrypt-service.ts @@ -1,6 +1,5 @@ import { Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core'; -import crypto from 'crypto'; -import { SysSecret, SysSettingsService } from '../../../system/index.js'; +import { Encryptor, SysSecret, SysSettingsService } from '../../../system/index.js'; /** * 授权 @@ -8,7 +7,7 @@ import { SysSecret, SysSettingsService } from '../../../system/index.js'; @Provide() @Scope(ScopeEnum.Singleton) export class EncryptService { - secretKey: Buffer; + encryptor: Encryptor; @Inject() sysSettingService: SysSettingsService; @@ -16,28 +15,16 @@ export class EncryptService { @Init() async init() { const secret: SysSecret = await this.sysSettingService.getSecret(); - this.secretKey = Buffer.from(secret.encryptSecret, 'base64'); + this.encryptor = new Encryptor(secret.encryptSecret); } // 加密函数 encrypt(text: string) { - const iv = crypto.randomBytes(16); // 初始化向量 - // const secretKey = crypto.randomBytes(32); - // const key = Buffer.from(secretKey); - const cipher = crypto.createCipheriv('aes-256-cbc', this.secretKey, iv); - let encrypted = cipher.update(text); - encrypted = Buffer.concat([encrypted, cipher.final()]); - return iv.toString('hex') + ':' + encrypted.toString('hex'); + return this.encryptor.encrypt(text); } // 解密函数 decrypt(encryptedText: string) { - const textParts = encryptedText.split(':'); - const iv = Buffer.from(textParts.shift(), 'hex'); - const encrypted = Buffer.from(textParts.join(':'), 'hex'); - const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(this.secretKey), iv); - let decrypted = decipher.update(encrypted); - decrypted = Buffer.concat([decrypted, decipher.final()]); - return decrypted.toString(); + return this.encryptor.decrypt(encryptedText); } } diff --git a/packages/ui/certd-server/src/controller/open/base-open-controller.ts b/packages/ui/certd-server/src/controller/open/base-open-controller.ts new file mode 100644 index 000000000..303365a93 --- /dev/null +++ b/packages/ui/certd-server/src/controller/open/base-open-controller.ts @@ -0,0 +1,15 @@ +import { BaseController, Encryptor } from '@certd/lib-server'; +import { OpenKey } from '../../modules/open/service/open-key-service.js'; + +export class BaseOpenController extends BaseController { + ok(res: any) { + const openKey: OpenKey = this.ctx.openKey; + if (openKey.encrypt) { + const data = JSON.stringify(res); + const encryptor = new Encryptor(openKey.keySecret); + const encrypted = encryptor.encrypt(data); + return this.ok(encrypted); + } + super.ok(res); + } +} diff --git a/packages/ui/certd-server/src/controller/open/cert-controller.ts b/packages/ui/certd-server/src/controller/open/cert-controller.ts new file mode 100644 index 000000000..a0b37d9bc --- /dev/null +++ b/packages/ui/certd-server/src/controller/open/cert-controller.ts @@ -0,0 +1,42 @@ +import { ALL, Body, Controller, Get, Inject, Post, Provide, Query } from '@midwayjs/core'; +import { CodeException, Constants, EncryptService } from '@certd/lib-server'; +import { CertInfoService } from '../../modules/monitor/service/cert-info-service.js'; +import { CertInfo } from '@certd/plugin-cert'; +import { OpenKey } from '../../modules/open/service/open-key-service.js'; +import { BaseOpenController } from './base-open-controller.js'; + +export type CertGetReq = { + domains: string; +}; + +/** + */ +@Provide() +@Controller('/open/cert') +export class OpenCertController extends BaseOpenController { + @Inject() + certInfoService: CertInfoService; + @Inject() + encryptService: EncryptService; + + @Get('/get', { summary: Constants.per.open }) + @Post('/get', { summary: Constants.per.open }) + async get(@Body(ALL) bean: CertGetReq, @Query(ALL) query: CertGetReq) { + const openKey: OpenKey = this.ctx.openKey; + const userId = openKey.userId; + if (!userId) { + return Constants.res.openKeyError; + } + + const domains = bean.domains || query.domains; + if (!domains) { + throw new CodeException(Constants.res.openParamError); + } + const domainArr = domains.split(','); + const res: CertInfo = await this.certInfoService.getCertInfo({ + userId, + domains: domainArr, + }); + return this.ok(res); + } +} diff --git a/packages/ui/certd-server/src/middleware/authority.ts b/packages/ui/certd-server/src/middleware/authority.ts index 850f01081..8859f6fb2 100644 --- a/packages/ui/certd-server/src/middleware/authority.ts +++ b/packages/ui/certd-server/src/middleware/authority.ts @@ -1,11 +1,11 @@ import { Init, Inject, MidwayWebRouterService, Provide, Scope, ScopeEnum } from '@midwayjs/core'; import { IMidwayKoaContext, IWebMiddleware, NextFunction } from '@midwayjs/koa'; import jwt from 'jsonwebtoken'; -import { Constants } from '@certd/lib-server'; +import { Constants, SysPrivateSettings, SysSettingsService } from '@certd/lib-server'; import { logger } from '@certd/basic'; import { AuthService } from '../modules/sys/authority/service/auth-service.js'; -import { SysSettingsService } from '@certd/lib-server'; -import { SysPrivateSettings } from '@certd/lib-server'; +import { Next } from 'koa'; +import { OpenKeyService } from '../modules/open/service/open-key-service.js'; /** * 权限校验 @@ -18,6 +18,8 @@ export class AuthorityMiddleware implements IWebMiddleware { @Inject() authService: AuthService; @Inject() + openKeyService: OpenKeyService; + @Inject() sysSettingsService: SysSettingsService; secret: string; @@ -48,6 +50,10 @@ export class AuthorityMiddleware implements IWebMiddleware { return; } + if (permission === Constants.per.open) { + return this.doOpenHandler(ctx, next); + } + let token = ctx.get('Authorization') || ''; token = token.replace('Bearer ', '').trim(); if (!token) { @@ -79,4 +85,21 @@ export class AuthorityMiddleware implements IWebMiddleware { await next(); }; } + + async doOpenHandler(ctx: IMidwayKoaContext, next: Next) { + //开放接口 + let openKey = ctx.get('Authorization') || ''; + openKey = openKey.replace('Bearer ', '').trim(); + if (!openKey) { + ctx.status = 401; + ctx.body = Constants.res.auth; + return; + } + + //校验 openKey + const openKeyRes = await this.openKeyService.verifyOpenKey(openKey); + ctx.user = { id: openKeyRes.userId }; + ctx.openKey = openKeyRes; + await next(); + } } diff --git a/packages/ui/certd-server/src/modules/monitor/index.ts b/packages/ui/certd-server/src/modules/monitor/index.ts deleted file mode 100644 index 2e836e704..000000000 --- a/packages/ui/certd-server/src/modules/monitor/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./entity/site-info.js"; -export * from "./entity/cert-info.js"; - -export * from "./service/cert-info-service.js"; -export * from "./service/site-info-service.js"; diff --git a/packages/ui/certd-server/src/modules/monitor/service/cert-info-service.ts b/packages/ui/certd-server/src/modules/monitor/service/cert-info-service.ts index b58c62a11..73271fc28 100644 --- a/packages/ui/certd-server/src/modules/monitor/service/cert-info-service.ts +++ b/packages/ui/certd-server/src/modules/monitor/service/cert-info-service.ts @@ -1,8 +1,10 @@ import { Provide } from '@midwayjs/core'; -import { BaseService, PageReq } from '@certd/lib-server'; +import { BaseService, CodeException, Constants, PageReq } from '@certd/lib-server'; import { InjectEntityModel } from '@midwayjs/typeorm'; import { Repository } from 'typeorm'; import { CertInfoEntity } from '../entity/cert-info.js'; +import { utils } from '@certd/basic'; +import { CertInfo, CertReader } from '@certd/plugin-cert'; @Provide() export class CertInfoService extends BaseService { @@ -68,4 +70,25 @@ export class CertInfoService extends BaseService { pipelineId: id, }); } + + async getCertInfo(param: { domains: string[]; userId: number }) { + const { domains, userId } = param; + + const list = await this.find({ + where: { + userId, + }, + }); + //遍历查找 + const matched = list.find(item => { + const itemDomains = item.domains.split(','); + return utils.domain.match(domains, itemDomains); + }); + if (!matched || !matched.certInfo) { + throw new CodeException(Constants.res.openCertNotFound); + } + const certInfo = JSON.parse(matched.certInfo) as CertInfo; + const certReader = new CertReader(certInfo); + return certReader.toCertInfo(); + } } diff --git a/packages/ui/certd-server/src/modules/open/entity/open-key.ts b/packages/ui/certd-server/src/modules/open/entity/open-key.ts new file mode 100644 index 000000000..26860d93a --- /dev/null +++ b/packages/ui/certd-server/src/modules/open/entity/open-key.ts @@ -0,0 +1,22 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('cd_open_key') +export class OpenKeyEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'user_id', comment: '用户id', unique: true }) + userId: number; + + @Column({ name: 'key_id', comment: 'keyId', unique: true }) + keyId: string; + + @Column({ name: 'key_secret', comment: 'keySecret', unique: true }) + keySecret: string; + + @Column({ name: 'create_time', comment: '创建时间', default: () => 'CURRENT_TIMESTAMP' }) + createTime: Date; + + @Column({ name: 'update_time', comment: '修改时间', default: () => 'CURRENT_TIMESTAMP' }) + updateTime: Date; +} diff --git a/packages/ui/certd-server/src/modules/open/service/open-key-service.ts b/packages/ui/certd-server/src/modules/open/service/open-key-service.ts new file mode 100644 index 000000000..79dd852e5 --- /dev/null +++ b/packages/ui/certd-server/src/modules/open/service/open-key-service.ts @@ -0,0 +1,97 @@ +import { Provide } from '@midwayjs/core'; +import { BaseService, Constants, CodeException, PageReq } from '@certd/lib-server'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Repository } from 'typeorm'; +import { OpenKeyEntity } from '../entity/open-key.js'; +import { utils } from '@certd/basic'; +import crypto from 'crypto'; + +export type OpenKey = { + userId: number; + keyId: string; + keySecret: string; + encrypt: boolean; +}; +@Provide() +export class OpenKeyService extends BaseService { + @InjectEntityModel(OpenKeyEntity) + repository: Repository; + + //@ts-ignore + getRepository() { + return this.repository; + } + + async page(pageReq: PageReq) { + return await super.page(pageReq); + } + + async getKey(userId: number) { + let entity = await this.getByUserId(userId); + if (entity) { + return { + keyId: entity.keyId, + keySecret: entity.keySecret, + }; + } + const keyId = utils.id.simpleNanoId(12) + '_key'; + const secretKey = crypto.randomBytes(32); + const keySecret = secretKey.toString('base64'); + entity = new OpenKeyEntity(); + entity.userId = userId; + entity.keyId = keyId; + entity.keySecret = keySecret; + await this.repository.save(entity); + return { + keyId: entity.keyId, + keySecret: entity.keySecret, + }; + } + + private getByUserId(userId: number) { + return this.repository.findOne({ where: { userId } }); + } + + async getByKeyId(keyId: string) { + return this.repository.findOne({ where: { keyId } }); + } + + async verifyOpenKey(openKey: string): Promise { + // openkey 组成,content = base64({keyId,t,encrypt,signType}) ,sign = md5({keyId,t,encrypt,signType}secret) , key = content.sign + const [content, sign] = openKey.split('.'); + const contentJson = Buffer.from(content, 'base64').toString(); + const { keyId, t, encrypt, signType } = JSON.parse(contentJson); + // 正负不超过3分钟 ,timestamps单位为秒 + if (Math.abs(Number(t) - Math.floor(Date.now() / 1000)) > 180) { + throw new CodeException(Constants.res.openKeyExpiresError); + } + + const entity = await this.getByKeyId(keyId); + if (!entity) { + throw new Error('openKey不存在'); + } + const secret = entity.keySecret; + let computedSign = ''; + if (signType === 'md5') { + computedSign = utils.hash.md5(contentJson + secret); + } else if (signType === 'sha256') { + computedSign = utils.hash.sha256(contentJson + secret); + } else { + throw new CodeException(Constants.res.openKeySignTypeError); + } + if (computedSign !== sign) { + throw new CodeException(Constants.res.openKeySignError); + } + + if (!entity.userId) { + throw new CodeException(Constants.res.openKeyError); + } + + return { + userId: entity.userId, + keyId: entity.keyId, + keySecret: entity.keySecret, + encrypt: encrypt, + }; + } +}