mirror of https://github.com/certd/certd
feat: 支持open api接口,根据域名获取证书
parent
c6c269f9e4
commit
52a4fd3318
|
@ -12,6 +12,8 @@ export const Constants = {
|
||||||
authOnly: '_authOnly_',
|
authOnly: '_authOnly_',
|
||||||
//仅需要登录
|
//仅需要登录
|
||||||
loginOnly: '_authOnly_',
|
loginOnly: '_authOnly_',
|
||||||
|
|
||||||
|
open: '_open_',
|
||||||
},
|
},
|
||||||
res: {
|
res: {
|
||||||
serverError(message: string) {
|
serverError(message: string) {
|
||||||
|
@ -68,5 +70,29 @@ export const Constants = {
|
||||||
code: 10001,
|
code: 10001,
|
||||||
message: '对不起,预览环境不允许修改此数据',
|
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: '证书不存在',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,10 +5,6 @@ import { BaseException } from './base-exception.js';
|
||||||
*/
|
*/
|
||||||
export class AuthException extends BaseException {
|
export class AuthException extends BaseException {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
super(
|
super('AuthException', Constants.res.auth.code, message ? message : Constants.res.auth.message);
|
||||||
'AuthException',
|
|
||||||
Constants.res.auth.code,
|
|
||||||
message ? message : Constants.res.auth.message
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,3 +8,9 @@ export class CommonException extends BaseException {
|
||||||
super('CommonException', Constants.res.error.code, message ? message : Constants.res.error.message);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export * from './service/plus-service.js';
|
export * from './service/plus-service.js';
|
||||||
export * from './service/file-service.js';
|
export * from './service/file-service.js';
|
||||||
|
export * from './service/encryptor.js';
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
import { Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
import { Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||||
import crypto from 'crypto';
|
import { Encryptor, SysSecret, SysSettingsService } from '../../../system/index.js';
|
||||||
import { SysSecret, SysSettingsService } from '../../../system/index.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 授权
|
* 授权
|
||||||
|
@ -8,7 +7,7 @@ import { SysSecret, SysSettingsService } from '../../../system/index.js';
|
||||||
@Provide()
|
@Provide()
|
||||||
@Scope(ScopeEnum.Singleton)
|
@Scope(ScopeEnum.Singleton)
|
||||||
export class EncryptService {
|
export class EncryptService {
|
||||||
secretKey: Buffer;
|
encryptor: Encryptor;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
sysSettingService: SysSettingsService;
|
sysSettingService: SysSettingsService;
|
||||||
|
@ -16,28 +15,16 @@ export class EncryptService {
|
||||||
@Init()
|
@Init()
|
||||||
async init() {
|
async init() {
|
||||||
const secret: SysSecret = await this.sysSettingService.getSecret();
|
const secret: SysSecret = await this.sysSettingService.getSecret();
|
||||||
this.secretKey = Buffer.from(secret.encryptSecret, 'base64');
|
this.encryptor = new Encryptor(secret.encryptSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加密函数
|
// 加密函数
|
||||||
encrypt(text: string) {
|
encrypt(text: string) {
|
||||||
const iv = crypto.randomBytes(16); // 初始化向量
|
return this.encryptor.encrypt(text);
|
||||||
// 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) {
|
decrypt(encryptedText: string) {
|
||||||
const textParts = encryptedText.split(':');
|
return this.encryptor.decrypt(encryptedText);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
import { Init, Inject, MidwayWebRouterService, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
import { Init, Inject, MidwayWebRouterService, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||||
import { IMidwayKoaContext, IWebMiddleware, NextFunction } from '@midwayjs/koa';
|
import { IMidwayKoaContext, IWebMiddleware, NextFunction } from '@midwayjs/koa';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { Constants } from '@certd/lib-server';
|
import { Constants, SysPrivateSettings, SysSettingsService } from '@certd/lib-server';
|
||||||
import { logger } from '@certd/basic';
|
import { logger } from '@certd/basic';
|
||||||
import { AuthService } from '../modules/sys/authority/service/auth-service.js';
|
import { AuthService } from '../modules/sys/authority/service/auth-service.js';
|
||||||
import { SysSettingsService } from '@certd/lib-server';
|
import { Next } from 'koa';
|
||||||
import { SysPrivateSettings } from '@certd/lib-server';
|
import { OpenKeyService } from '../modules/open/service/open-key-service.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 权限校验
|
* 权限校验
|
||||||
|
@ -18,6 +18,8 @@ export class AuthorityMiddleware implements IWebMiddleware {
|
||||||
@Inject()
|
@Inject()
|
||||||
authService: AuthService;
|
authService: AuthService;
|
||||||
@Inject()
|
@Inject()
|
||||||
|
openKeyService: OpenKeyService;
|
||||||
|
@Inject()
|
||||||
sysSettingsService: SysSettingsService;
|
sysSettingsService: SysSettingsService;
|
||||||
|
|
||||||
secret: string;
|
secret: string;
|
||||||
|
@ -48,6 +50,10 @@ export class AuthorityMiddleware implements IWebMiddleware {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (permission === Constants.per.open) {
|
||||||
|
return this.doOpenHandler(ctx, next);
|
||||||
|
}
|
||||||
|
|
||||||
let token = ctx.get('Authorization') || '';
|
let token = ctx.get('Authorization') || '';
|
||||||
token = token.replace('Bearer ', '').trim();
|
token = token.replace('Bearer ', '').trim();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
@ -79,4 +85,21 @@ export class AuthorityMiddleware implements IWebMiddleware {
|
||||||
await next();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { Provide } from '@midwayjs/core';
|
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 { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { CertInfoEntity } from '../entity/cert-info.js';
|
import { CertInfoEntity } from '../entity/cert-info.js';
|
||||||
|
import { utils } from '@certd/basic';
|
||||||
|
import { CertInfo, CertReader } from '@certd/plugin-cert';
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
export class CertInfoService extends BaseService<CertInfoEntity> {
|
export class CertInfoService extends BaseService<CertInfoEntity> {
|
||||||
|
@ -68,4 +70,25 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
|
||||||
pipelineId: id,
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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<OpenKeyEntity> {
|
||||||
|
@InjectEntityModel(OpenKeyEntity)
|
||||||
|
repository: Repository<OpenKeyEntity>;
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
getRepository() {
|
||||||
|
return this.repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
async page(pageReq: PageReq<OpenKeyEntity>) {
|
||||||
|
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> {
|
||||||
|
// 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue