mirror of
https://github.com/certd/certd.git
synced 2026-04-23 19:57:27 +08:00
perf: 授权配置支持加密
原本已经添加的授权配置,再次编辑保存即变成加密配置
This commit is contained in:
@@ -12,13 +12,11 @@ import { PipelineEntity } from '../modules/pipeline/entity/pipeline.js';
|
||||
//import { logger } from '../utils/logger';
|
||||
// load .env file in process.cwd
|
||||
import { mergeConfig } from './loader.js';
|
||||
import { Keys } from './keys.js';
|
||||
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
|
||||
const keys = Keys.load();
|
||||
const development = {
|
||||
keys: keys.cookieKeys,
|
||||
keys: 'certd',
|
||||
koa: {
|
||||
port: 7001,
|
||||
},
|
||||
@@ -78,7 +76,6 @@ const development = {
|
||||
|
||||
auth: {
|
||||
jwt: {
|
||||
secret: keys.jwtKey,
|
||||
expire: 7 * 24 * 60 * 60, //单位秒
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import yaml from 'js-yaml';
|
||||
import * as _ from 'lodash-es';
|
||||
import { nanoid } from 'nanoid';
|
||||
import path from 'path';
|
||||
const KEYS_FILE = './data/keys.yaml';
|
||||
export class Keys {
|
||||
jwtKey: string = nanoid();
|
||||
cookieKeys: string[] = [nanoid()];
|
||||
|
||||
static load(): Keys {
|
||||
const keys = new Keys();
|
||||
if (fs.existsSync(KEYS_FILE)) {
|
||||
const content = fs.readFileSync(KEYS_FILE, 'utf8');
|
||||
const json = yaml.load(content);
|
||||
_.merge(keys, json);
|
||||
}
|
||||
keys.save();
|
||||
return keys;
|
||||
}
|
||||
|
||||
save() {
|
||||
const parent = path.dirname(KEYS_FILE);
|
||||
if (!fs.existsSync(parent)) {
|
||||
fs.mkdirSync(parent, {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
fs.writeFileSync(KEYS_FILE, yaml.dump(this));
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,31 @@
|
||||
import { Config, Inject, MidwayWebRouterService, Provide } from '@midwayjs/core';
|
||||
import { Init, Inject, MidwayWebRouterService, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { IMidwayKoaContext, IWebMiddleware, NextFunction } from '@midwayjs/koa';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { Constants } from '../basic/constants.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { AuthService } from '../modules/authority/service/auth-service.js';
|
||||
import { SysSettingsService } from '../modules/system/service/sys-settings-service.js';
|
||||
import { SysPrivateSettings } from '../modules/system/service/models.js';
|
||||
|
||||
/**
|
||||
* 权限校验
|
||||
*/
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Singleton)
|
||||
export class AuthorityMiddleware implements IWebMiddleware {
|
||||
@Config('auth.jwt.secret')
|
||||
private secret: string;
|
||||
@Inject()
|
||||
webRouterService: MidwayWebRouterService;
|
||||
@Inject()
|
||||
authService: AuthService;
|
||||
@Inject()
|
||||
sysSettingsService: SysSettingsService;
|
||||
|
||||
secret: string;
|
||||
@Init()
|
||||
async init() {
|
||||
const setting: SysPrivateSettings = await this.sysSettingsService.getSetting(SysPrivateSettings);
|
||||
this.secret = setting.jwtKey;
|
||||
}
|
||||
|
||||
resolve() {
|
||||
return async (ctx: IMidwayKoaContext, next: NextFunction) => {
|
||||
|
||||
@@ -3,9 +3,9 @@ import { logger } from '../../utils/logger.js';
|
||||
import { UserService } from '../authority/service/user-service.js';
|
||||
import { SysSettingsService } from '../system/service/sys-settings-service.js';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { SysInstallInfo, SysLicenseInfo } from '../system/service/models.js';
|
||||
import { SysInstallInfo, SysLicenseInfo, SysPrivateSettings } from '../system/service/models.js';
|
||||
import { verify } from '@certd/pipeline';
|
||||
|
||||
import crypto from 'crypto';
|
||||
export type InstallInfo = {
|
||||
installTime: number;
|
||||
instanceId?: string;
|
||||
@@ -23,6 +23,7 @@ export class AutoInitSite {
|
||||
@Init()
|
||||
async init() {
|
||||
logger.info('初始化站点开始');
|
||||
//安装信息
|
||||
const installInfo: SysInstallInfo = await this.sysSettingsService.getSetting(SysInstallInfo);
|
||||
if (!installInfo.siteId) {
|
||||
installInfo.siteId = nanoid();
|
||||
@@ -33,6 +34,19 @@ export class AutoInitSite {
|
||||
await this.sysSettingsService.saveSetting(installInfo);
|
||||
}
|
||||
|
||||
//private信息
|
||||
const privateInfo = await this.sysSettingsService.getSetting<SysPrivateSettings>(SysPrivateSettings);
|
||||
if (!privateInfo.jwtKey) {
|
||||
privateInfo.jwtKey = nanoid();
|
||||
await this.sysSettingsService.saveSetting(privateInfo);
|
||||
}
|
||||
|
||||
if (!privateInfo.encryptSecret) {
|
||||
const secretKey = crypto.randomBytes(32);
|
||||
privateInfo.encryptSecret = secretKey.toString('base64');
|
||||
await this.sysSettingsService.saveSetting(privateInfo);
|
||||
}
|
||||
|
||||
// 授权许可
|
||||
const licenseInfo: SysLicenseInfo = await this.sysSettingsService.getSetting(SysLicenseInfo);
|
||||
const req = {
|
||||
|
||||
@@ -4,6 +4,8 @@ import jwt from 'jsonwebtoken';
|
||||
import { CommonException } from '../../../basic/exception/common-exception.js';
|
||||
import { RoleService } from '../../authority/service/role-service.js';
|
||||
import { UserEntity } from '../../authority/entity/user.js';
|
||||
import { SysSettingsService } from '../../system/service/sys-settings-service.js';
|
||||
import { SysPrivateSettings } from '../../system/service/models.js';
|
||||
|
||||
/**
|
||||
* 系统用户
|
||||
@@ -17,6 +19,9 @@ export class LoginService {
|
||||
@Config('auth.jwt')
|
||||
private jwt: any;
|
||||
|
||||
@Inject()
|
||||
sysSettingsService: SysSettingsService;
|
||||
|
||||
/**
|
||||
* login
|
||||
*/
|
||||
@@ -47,7 +52,11 @@ export class LoginService {
|
||||
roles: roleIds,
|
||||
};
|
||||
const expire = this.jwt.expire;
|
||||
const token = jwt.sign(tokenInfo, this.jwt.secret, {
|
||||
|
||||
const setting = await this.sysSettingsService.getSetting<SysPrivateSettings>(SysPrivateSettings);
|
||||
const jwtSecret = setting.jwtKey;
|
||||
|
||||
const token = jwt.sign(tokenInfo, jwtSecret, {
|
||||
expiresIn: expire,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import {
|
||||
ALL,
|
||||
Body,
|
||||
Controller,
|
||||
Inject,
|
||||
Post,
|
||||
Provide,
|
||||
Query,
|
||||
} from '@midwayjs/core';
|
||||
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
|
||||
import { CrudController } from '../../../basic/crud-controller.js';
|
||||
import { AccessService } from '../service/access-service.js';
|
||||
import { Constants } from '../../../basic/constants.js';
|
||||
@@ -28,7 +20,7 @@ export class AccessController extends CrudController<AccessService> {
|
||||
async page(@Body(ALL) body) {
|
||||
body.query = body.query ?? {};
|
||||
body.query.userId = this.ctx.user.id;
|
||||
return super.page(body);
|
||||
return await super.page(body);
|
||||
}
|
||||
|
||||
@Post('/list', { summary: Constants.per.authOnly })
|
||||
|
||||
@@ -15,9 +15,12 @@ export class AccessEntity {
|
||||
@Column({ comment: '类型', length: 100 })
|
||||
type: string;
|
||||
|
||||
@Column({ name: 'setting', comment: '设置', length: 1024, nullable: true })
|
||||
@Column({ name: 'setting', comment: '设置', length: 10240, nullable: true })
|
||||
setting: string;
|
||||
|
||||
@Column({ name: 'encrypt_setting', comment: '已加密设置', length: 10240, nullable: true })
|
||||
encryptSetting: string;
|
||||
|
||||
@Column({
|
||||
name: 'create_time',
|
||||
comment: '创建时间',
|
||||
|
||||
@@ -1,33 +1,127 @@
|
||||
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { BaseService } from '../../../basic/base-service.js';
|
||||
import { AccessEntity } from '../entity/access.js';
|
||||
import { accessRegistry, IAccessService } from '@certd/pipeline';
|
||||
import { AccessDefine, accessRegistry, IAccessService } from '@certd/pipeline';
|
||||
import { EncryptService } from './encrypt-service.js';
|
||||
import { ValidateException } from '../../../basic/exception/validation-exception.js';
|
||||
|
||||
/**
|
||||
* 授权
|
||||
*/
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Singleton)
|
||||
export class AccessService
|
||||
extends BaseService<AccessEntity>
|
||||
implements IAccessService
|
||||
{
|
||||
export class AccessService extends BaseService<AccessEntity> implements IAccessService {
|
||||
@InjectEntityModel(AccessEntity)
|
||||
repository: Repository<AccessEntity>;
|
||||
|
||||
@Inject()
|
||||
encryptService: EncryptService;
|
||||
|
||||
getRepository() {
|
||||
return this.repository;
|
||||
}
|
||||
|
||||
async page(query, page = { offset: 0, limit: 20 }, order, buildQuery) {
|
||||
const res = await super.page(query, page, order, buildQuery);
|
||||
res.records = res.records.map(item => {
|
||||
delete item.encryptSetting;
|
||||
return item;
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
async add(param) {
|
||||
this.encryptSetting(param, null);
|
||||
return await super.add(param);
|
||||
}
|
||||
|
||||
encryptSetting(param: any, oldSettingEntity?: AccessEntity) {
|
||||
const accessType = param.type;
|
||||
const accessDefine: AccessDefine = accessRegistry.getDefine(accessType);
|
||||
if (!accessDefine) {
|
||||
throw new ValidateException(`授权类型${accessType}不存在`);
|
||||
}
|
||||
const setting = param.setting;
|
||||
if (!setting) {
|
||||
return;
|
||||
}
|
||||
const json = JSON.parse(setting);
|
||||
let oldSetting = {};
|
||||
let encryptSetting = {};
|
||||
const firstEncrypt = !oldSettingEntity.encryptSetting || oldSettingEntity.encryptSetting === '{}';
|
||||
if (oldSettingEntity) {
|
||||
oldSetting = JSON.parse(oldSettingEntity.setting || '{}');
|
||||
encryptSetting = JSON.parse(oldSettingEntity.encryptSetting || '{}');
|
||||
}
|
||||
for (const key in json) {
|
||||
//加密
|
||||
const value = json[key];
|
||||
const accessInputDefine = accessDefine.input[key];
|
||||
if (!accessInputDefine) {
|
||||
throw new ValidateException(`授权类型${accessType}不存在字段${key}`);
|
||||
}
|
||||
if (!accessInputDefine.encrypt || !value || typeof value !== 'string') {
|
||||
//定义无需加密、value为空、不是字符串 这些不需要加密
|
||||
encryptSetting[key] = {
|
||||
value: value,
|
||||
encrypt: false,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (firstEncrypt || oldSetting[key] !== value) {
|
||||
//星号保护
|
||||
const length = value.length;
|
||||
const subIndex = Math.min(2, length);
|
||||
const starLength = length - subIndex * 2;
|
||||
const starString = '*'.repeat(starLength);
|
||||
json[key] = value.substring(0, subIndex) + starString + value.substring(value.length - subIndex);
|
||||
encryptSetting[key] = {
|
||||
value: this.encryptService.encrypt(value),
|
||||
encrypt: true,
|
||||
};
|
||||
}
|
||||
//未改变情况下,不做修改
|
||||
}
|
||||
param.encryptSetting = JSON.stringify(encryptSetting);
|
||||
param.setting = JSON.stringify(json);
|
||||
}
|
||||
/**
|
||||
* 修改
|
||||
* @param param 数据
|
||||
*/
|
||||
async update(param) {
|
||||
const oldEntity = await this.info(param.id);
|
||||
if (oldEntity == null) {
|
||||
throw new ValidateException('该授权配置不存在,请确认是否已被删除');
|
||||
}
|
||||
this.encryptSetting(param, oldEntity);
|
||||
return await super.update(param);
|
||||
}
|
||||
|
||||
async getById(id: any): Promise<any> {
|
||||
const entity = await this.info(id);
|
||||
if (entity == null) {
|
||||
throw new Error(`该授权配置不存在,请确认是否已被删除:id=${id}`);
|
||||
}
|
||||
// const access = accessRegistry.get(entity.type);
|
||||
const setting = JSON.parse(entity.setting);
|
||||
let setting = {};
|
||||
if (entity.encryptSetting && entity.encryptSetting !== '{}') {
|
||||
setting = JSON.parse(entity.encryptSetting);
|
||||
for (const key in setting) {
|
||||
//解密
|
||||
const encryptValue = setting[key];
|
||||
let value = encryptValue.value;
|
||||
if (encryptValue.encrypt) {
|
||||
value = this.encryptService.decrypt(value);
|
||||
}
|
||||
setting[key] = value;
|
||||
}
|
||||
} else if (entity.setting) {
|
||||
setting = JSON.parse(entity.setting);
|
||||
}
|
||||
return {
|
||||
id: entity.id,
|
||||
...setting,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { Provide } from '@midwayjs/core';
|
||||
import { dnsProviderRegistry } from '@certd/plugin-cert';
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Singleton)
|
||||
export class DnsProviderService {
|
||||
getList() {
|
||||
return dnsProviderRegistry.getDefineList();
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import crypto from 'crypto';
|
||||
import { SysSettingsService } from '../../system/service/sys-settings-service.js';
|
||||
import { SysPrivateSettings } from '../../system/service/models.js';
|
||||
|
||||
/**
|
||||
* 授权
|
||||
*/
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Singleton)
|
||||
export class EncryptService {
|
||||
secretKey: Buffer;
|
||||
|
||||
@Inject()
|
||||
sysSettingService: SysSettingsService;
|
||||
|
||||
@Init()
|
||||
async init() {
|
||||
const privateInfo: SysPrivateSettings = await this.sysSettingService.getSetting(SysPrivateSettings);
|
||||
this.secretKey = Buffer.from(privateInfo.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();
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ export class SysPrivateSettings extends BaseSettings {
|
||||
static __title__ = '系统私有设置';
|
||||
static __access__ = 'private';
|
||||
static __key__ = 'sys.private';
|
||||
jwtKey?: string;
|
||||
encryptSecret?: string;
|
||||
}
|
||||
|
||||
export class SysInstallInfo extends BaseSettings {
|
||||
|
||||
@@ -21,6 +21,7 @@ export class AliyunAccess {
|
||||
placeholder: 'accessKeySecret',
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
accessKeySecret = '';
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export class CloudflareAccess {
|
||||
},
|
||||
helper: '前往 https://dash.cloudflare.com/profile/api-tokens 获取API令牌, token权限必须包含:[Zone区域-Zone区域-Edit编辑], [Zone区域-DNS-Edit编辑]',
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
apiToken = '';
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export class DemoAccess implements IAccess {
|
||||
},
|
||||
//是否必填
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
//属性名称
|
||||
demoKeySecret = '';
|
||||
|
||||
@@ -38,6 +38,7 @@ export class SshAccess implements IAccess, ConnectConfig {
|
||||
name: 'a-input-password',
|
||||
vModel: 'value',
|
||||
},
|
||||
encrypt: true,
|
||||
helper: '登录密码或密钥必填一项',
|
||||
})
|
||||
password!: string;
|
||||
@@ -48,6 +49,7 @@ export class SshAccess implements IAccess, ConnectConfig {
|
||||
name: 'a-textarea',
|
||||
vModel: 'value',
|
||||
},
|
||||
encrypt: true,
|
||||
})
|
||||
privateKey!: string;
|
||||
|
||||
@@ -58,6 +60,7 @@ export class SshAccess implements IAccess, ConnectConfig {
|
||||
name: 'a-input-password',
|
||||
vModel: 'value',
|
||||
},
|
||||
encrypt: true,
|
||||
})
|
||||
passphrase!: string;
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export class HuaweiAccess {
|
||||
placeholder: 'accessKeySecret',
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
accessKeySecret = '';
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export class K8sAccess {
|
||||
placeholder: 'kubeconfig',
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
kubeconfig = '';
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export class DnspodAccess {
|
||||
component: {
|
||||
placeholder: '开放接口token',
|
||||
},
|
||||
encrypt: true,
|
||||
rules: [{ required: true, message: '该项必填' }],
|
||||
})
|
||||
token = '';
|
||||
|
||||
@@ -19,6 +19,7 @@ export class TencentAccess {
|
||||
component: {
|
||||
placeholder: 'secretKey',
|
||||
},
|
||||
encrypt: true,
|
||||
rules: [{ required: true, message: '该项必填' }],
|
||||
})
|
||||
secretKey = '';
|
||||
|
||||
Reference in New Issue
Block a user