优化门禁、访客码、业主二维码加密方式&添加智能门禁物联网模块

This commit is contained in:
xuecong 2021-11-16 20:42:59 +08:00
parent f5bedbe367
commit 9708e1f673
32 changed files with 599 additions and 123 deletions

View File

@ -1,3 +1,20 @@
## 「e家宜业」物联网服务端
> 代码整理中,敬请期待
### 智能门禁
> 目前仅支持卡帕斯品牌
#### 接口地址
- 心跳检测 `域名/iot/entrance?id={{接口形参第一位}}&community_id={{接口形参第二位}}&method=status`
- 开门刷卡 `域名/iot/entrance?id={{接口形参第一位}}&community_id={{接口形参第二位}}&method=access`
#### 门禁配置
* 选择`启用AES128加密`,秘钥配置中台对应的秘钥
* 心跳间隔设置为`60`
* `本机信息-标识`配置为中台对应的标识名称
* `通讯协议`选择`启用Http通讯`
* `验证模式`选择`脱机验证优先`主要针对老旧小区用的RFID卡

View File

@ -72,4 +72,4 @@
"list": []
}
}
}
}

View File

@ -97,7 +97,7 @@ CwPage({
},
chooseContact() {
wx.chooseContact({
success: (res) => {
success: res => {
this.setData({
vistor_name: res.displayName,
vistor_phone: res.phoneNumber

View File

@ -10,7 +10,7 @@
is-detail
url="/pages/community/index"
/>
<cw-cell is-link clickable title="在通讯录中选择访客" bind:click="chooseContact"/>
<cw-cell is-link clickable title="在通讯录中选择访客" bind:click="chooseContact" />
<cw-field
label="访客称呼"
maxlength="{{ 8 }}"

View File

@ -32,13 +32,6 @@ CREATE TABLE `ejyy_ask_for_leave_flow` (
`finished_at` bigint(13) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `ejyy_building_access` (
`id` bigint(20) NOT NULL,
`building_id` bigint(20) NOT NULL,
`uid` varchar(128) DEFAULT NULL,
`created_at` bigint(13) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `ejyy_building_info` (
`id` int(11) NOT NULL,
`community_id` bigint(20) NOT NULL,
@ -1092,7 +1085,6 @@ CREATE TABLE `ejyy_vistor` (
`vistor_phone` varchar(11) NOT NULL,
`car_number` varchar(8) DEFAULT NULL,
`have_vistor_info` tinyint(1) NOT NULL DEFAULT 0,
`uid` varchar(128) DEFAULT NULL,
`expire` bigint(13) NOT NULL,
`used_at` bigint(13) DEFAULT NULL,
`scan_by` bigint(20) DEFAULT NULL,

View File

@ -33,6 +33,7 @@ import ModelMiddleware from '~/middleware/model';
import IpMiddleware from '~/middleware/ip';
import HeaderMiddleware from '~/middleware/header';
import WatcherMiddleware from '~/middleware/watcher';
import * as iot from '~/iot';
if (cluster.isMaster) {
cwlog.success(`main process ${process.pid}`);
@ -57,6 +58,15 @@ if (cluster.isMaster) {
NotifyModule(router);
OaModule(router);
// WebSocket
wss.init(server);
// for socket
redisService.subscribe();
// 物联网
iot.init(router);
app.use(KoaBodyMiddleware({ multipart: true }))
.use(
KoaLogMiddleware({
@ -80,12 +90,6 @@ if (cluster.isMaster) {
.use(router.routes())
.use(WatcherMiddleware());
// WebSocket
wss.init(server);
// for socket
redisService.subscribe();
const port = process.env.port ? parseInt(process.env.port, 10) : config.server.port;
server.listen(port, '0.0.0.0', () => {

View File

@ -148,3 +148,19 @@ export const PARK_NAME_EXIST = -171;
export const PARK_BLACKLIST_EXIST = -172;
// 预警中控名称存在
export const WARNING_NAME_EXIST = -173;
// 门禁非法
export const IOT_ENTRANCE_ILLEGAL = -174;
// 梯控非法
export const IOT_ELEVATOR_ILLEGAL = -175;
// 灯控非法
export const IOT_LAMP_ILLEGAL = -176;
// 中继器非法
export const IOT_REPEATER_ILLEGAL = -177;
// 仪表非法
export const IOT_METER_ILLEGAL = -178;
// 停车差非法
export const IOT_PARK_ILLEGAL = -179;
// 预警中控非法
export const IOT_WARNING_ILLEGAL = -180;
// 物联网设备秘钥错误
export const IOT_SECRET_ERROR = -181;

View File

@ -0,0 +1,124 @@
/**
* +----------------------------------------------------------------------
* | e家宜业
* +----------------------------------------------------------------------
* | Copyright (c) 2020~2021 https://www.chowa.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed e家宜业
* +----------------------------------------------------------------------
* | Author: jixuecong@chowa.cn
* +----------------------------------------------------------------------
*/
import moment from 'moment';
import Knex from 'Knex';
import { EjyyIotEntrance } from '~/types/model';
import { SELF_ACCESS_QRCODE, VISTOR_ACCESS_QRCODE } from '~/constant/open_access';
import utils from '~/utils';
import { IOT_METHOD_QRCODE, IOT_METHOD_NFC, IOT_METHOD_ICCARD } from '~/constant/iot';
const TYPE_CARD = '0';
const TYPE_SERIAL = '1';
const TYPE_PASSWORD = '2';
const TYPE_BUTTON = '3';
const TYPE_ID_CARD = '6';
const TYPE_QRCODE = '9';
const TYPE_FINGERPRINT = '10';
const TYPE_PULSE = '11';
const TYPE_RFID = '12';
const TYPE_FACE = '13';
const OPEN = '1';
const CLOSE = '3';
const ENTER = '0';
const LEAVE = '1';
interface AccessParams {
type:
| typeof TYPE_CARD
| typeof TYPE_SERIAL
| typeof TYPE_PASSWORD
| typeof TYPE_BUTTON
| typeof TYPE_ID_CARD
| typeof TYPE_QRCODE
| typeof TYPE_FINGERPRINT
| typeof TYPE_PULSE
| typeof TYPE_RFID
| typeof TYPE_FACE;
Reader: string;
DataLen: string;
Index: string;
Serial: string;
Status: string;
Input: string;
Ver: string;
ID: string;
IP: string;
MAC: string;
Card: string;
}
interface AccessResponse {
AcsRes: typeof OPEN | typeof CLOSE;
ActIndex?: typeof ENTER | typeof LEAVE;
Time?: string;
}
async function accessMethod(params: AccessParams, entranceInfo: EjyyIotEntrance, model: Knex): Promise<AccessResponse> {
const mom = moment();
await utils.redis.set(`entrance${entranceInfo.id}`, mom.valueOf().toString());
// 卡 二维码
if (
params.type === TYPE_CARD ||
params.type === TYPE_RFID ||
params.type === TYPE_SERIAL ||
params.type === TYPE_QRCODE
) {
const reuslt =
params.type === TYPE_SERIAL || params.type === TYPE_QRCODE
? utils.access.decrypt(Buffer.from(params.Card, 'base64').toString())
: utils.access.decrypt(params.Card);
if (!reuslt.success) {
return { AcsRes: CLOSE };
}
const existBuilding = await model
.from('ejyy_building_info')
.where('id', reuslt.building_id)
.andWhere('community_id', entranceInfo.community_id)
.first();
if (!existBuilding) {
return { AcsRes: CLOSE };
}
let method: typeof IOT_METHOD_NFC | typeof IOT_METHOD_ICCARD | typeof IOT_METHOD_QRCODE = IOT_METHOD_QRCODE;
if (params.type === TYPE_CARD) {
method = IOT_METHOD_NFC;
} else if (params.type === TYPE_RFID) {
method = IOT_METHOD_ICCARD;
}
await model.from('ejyy_iot_entrance_log').insert({
wechat_mp_user_id: reuslt.type === VISTOR_ACCESS_QRCODE ? null : reuslt.id,
vistor_id: reuslt.type === SELF_ACCESS_QRCODE ? null : reuslt.id,
entrance_id: entranceInfo.id,
method,
created_at: mom.valueOf()
});
return { AcsRes: OPEN, ActIndex: ENTER, Time: '5' };
}
// 按钮
else if (params.type === TYPE_BUTTON) {
return { AcsRes: OPEN, ActIndex: LEAVE };
} else {
return { AcsRes: CLOSE };
}
}
export default accessMethod;

View File

@ -0,0 +1,197 @@
/**
* +----------------------------------------------------------------------
* | e家宜业
* +----------------------------------------------------------------------
* | Copyright (c) 2020~2021 https://www.chowa.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed e家宜业
* +----------------------------------------------------------------------
* | Author: jixuecong@chowa.cn
* +----------------------------------------------------------------------
*/
import path from 'path';
import crypto from 'crypto';
import RawBody from 'raw-body';
import KoaRouter from 'koa-router';
import { Context } from 'koa';
import { Action } from '~/types/action';
import { IOT_ENTRANCE_ILLEGAL, PARAMS_ERROR, IOT_SECRET_ERROR } from '~/constant/code';
import validatorService from '~/service/validator';
import statusMethod from './status';
import accessMethod from './access';
import config from '~/config';
import cwlog from 'chowa-log';
function zeroPad(str: string, length = 8): string {
str = Buffer.from(str, 'utf8').toString('hex');
const bitLength = str.length * length;
if (bitLength < 256) {
for (let i = bitLength; i < 256; i += length) {
str += 0;
}
} else if (bitLength > 256) {
while ((str.length * length) % 256 != 0) {
str += 0;
}
}
return Buffer.from(str, 'hex').toString('utf8');
}
function encrypt(str: string, secret: string): string {
const cipher = crypto.createCipheriv('aes-128-ecb', secret, '');
cipher.setAutoPadding(false);
let crypted = cipher.update(zeroPad(str), 'utf8', 'hex');
crypted += cipher.final('hex');
return crypted;
}
function decrypt(str: string, secret: string): string {
const cipher = crypto.createDecipheriv('aes-128-ecb', secret, '');
const isBase64 = Buffer.from(str, 'base64').toString('base64') === str;
cipher.setAutoPadding(false);
let decrypted = cipher.update(zeroPad(str), isBase64 ? 'base64' : 'hex', 'utf8');
decrypted = decrypted + cipher.final('utf8');
return decrypted.replace(/[\u0000-\u0019]+/g, '');
}
const IotEntranceAction = <Action>{
router: {
path: '/entrance',
method: 'post',
authRequired: false
},
validator: {
query: [
{
name: 'community_id',
required: true,
regex: /^\d+$/
},
{
name: 'id',
required: true,
regex: /^\d+$/
},
{
name: 'method',
required: true,
regex: /^status|access$/
}
]
},
response: async ctx => {
const { community_id, id, method } = ctx.request.query;
const entranceInfo = await ctx.model
.from('ejyy_iot_entrance')
.where('id', id)
.andWhere('community_id', community_id)
.first();
if (!entranceInfo) {
return (ctx.body = {
code: IOT_ENTRANCE_ILLEGAL,
message: '非法的物联网门禁'
});
}
const raw = await RawBody(ctx.req, {
length: ctx.request.length,
limit: '1mb',
encoding: ctx.request.charset || 'utf-8'
});
const datas = raw.replace('DATAS=', '');
if (!datas) {
return (ctx.body = {
code: PARAMS_ERROR,
message: '物联网门禁受控参数错误'
});
}
let decrypted = null;
try {
decrypted = decrypt(datas, entranceInfo.secret);
} catch (e) {
return (ctx.bod = {
code: IOT_SECRET_ERROR,
message: '物联网门禁秘钥错误'
});
}
let params = null;
try {
params = JSON.parse(decrypted);
} catch (e) {
return (ctx.body = {
code: IOT_SECRET_ERROR,
message: '物联网门禁秘钥错误'
});
}
if (!('Serial' in params) || !('ID' in params)) {
return (ctx.body = {
code: PARAMS_ERROR,
message: '物联网门禁控制参数错误'
});
}
if (params.ID !== entranceInfo.sign) {
return (ctx.body = {
code: PARAMS_ERROR,
message: '物联网门禁自定义标识符配置错误'
});
}
let res = {};
console.log(params);
switch (method) {
// 心跳
case 'status':
res = await statusMethod(params, entranceInfo);
break;
// 开门
case 'access':
res = await accessMethod(params, entranceInfo, ctx.model);
break;
default:
}
ctx.res.setHeader('Content-Type', 'text/html');
ctx.body = `DATAS={${encrypt(JSON.stringify(res), entranceInfo.secret)}}`;
}
};
export default (appRouter: KoaRouter) => {
const { router, validator, response } = IotEntranceAction;
appRouter[router.method](path.posix.join('/iot', router.path), async (ctx: Context, next) => {
const vs = validatorService(ctx, validator);
if (!vs.success) {
return (ctx.body = {
code: PARAMS_ERROR,
message: vs.message
});
}
await response.apply(this, [ctx, next]);
});
if (config.debug) {
cwlog.info(`IotEntrance mounted and request from ${path.posix.join('/iot', router.path)} by ${router.method}`);
}
};

View File

@ -0,0 +1,51 @@
/**
* +----------------------------------------------------------------------
* | e家宜业
* +----------------------------------------------------------------------
* | Copyright (c) 2020~2021 https://www.chowa.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed e家宜业
* +----------------------------------------------------------------------
* | Author: jixuecong@chowa.cn
* +----------------------------------------------------------------------
*/
import moment from 'moment';
import { EjyyIotEntrance } from '~/types/model';
import utils from '~/utils';
interface StatusParams {
Serial: string;
Status: string;
Input: string;
Ver: string;
ID: string;
IP: string;
MAC: string;
Now: string;
Crc: string;
T1: string;
H1: string;
T2: string;
H2: string;
NextNum: string;
Key: string;
}
interface StatusResponse {
Key: string;
Now: string;
}
async function statusMethod(params: StatusParams, entranceInfo: EjyyIotEntrance): Promise<StatusResponse> {
const mom = moment();
await utils.redis.set(`entrance${entranceInfo.id}`, mom.valueOf().toString());
return {
Key: params.Key,
Now: `${mom.format('YYYYMMDDHHmmss')}${mom.day()}`
};
}
export default statusMethod;

18
server/src/iot/index.ts Normal file
View File

@ -0,0 +1,18 @@
/**
* +----------------------------------------------------------------------
* | e家宜业
* +----------------------------------------------------------------------
* | Copyright (c) 2020~2021 https://www.chowa.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed e家宜业
* +----------------------------------------------------------------------
* | Author: jixuecong@chowa.cn
* +----------------------------------------------------------------------
*/
import KoaRouter from 'koa-router';
import IotEntrance from './entrance';
export function init(appRouter: KoaRouter) {
IotEntrance(appRouter);
}

View File

@ -12,6 +12,8 @@
import { Action } from '~/types/action';
import { SUCCESS } from '~/constant/code';
import utils from '~/utils';
import { SELF_ACCESS_QRCODE } from '~/constant/open_access';
interface RequestBody {
building_ids: number[];
@ -43,11 +45,22 @@ const MpAccessListAction = <Action>{
response: async ctx => {
const { building_ids, community_id } = <RequestBody>ctx.request.body;
const cardList = await ctx.model
.from('ejyy_building_access')
.whereIn('building_id', building_ids)
.select('building_id', 'uid')
.orderBy('id', 'desc');
const buildings = await ctx.model
.from('ejyy_user_building')
.leftJoin('ejyy_building_info', 'ejyy_building_info.id', 'ejyy_user_building.building_id')
.where('ejyy_building_info.community_id', community_id)
.whereIn('ejyy_user_building.building_id', building_ids)
.select('ejyy_user_building.building_id')
.orderBy('ejyy_user_building.id', 'desc');
const cardList = [];
buildings.forEach(({ building_id }) => {
cardList.push({
building_id,
uid: utils.access.encrypt(ctx.mpUserInfo.id, building_id, SELF_ACCESS_QRCODE)
});
});
const entranceList = await ctx.model
.from('ejyy_iot_entrance')

View File

@ -41,7 +41,7 @@ const MpCommunityBindingByFamliyAction = <Action>{
response: async ctx => {
const { qrcontent } = <RequestBody>ctx.request.body;
const qrInfo = utils.community.decode(qrcontent);
const qrInfo = utils.community.decrypt(qrcontent);
if (
!qrInfo.success ||

View File

@ -39,7 +39,7 @@ const MpCommunityBindingByPropertyAction = <Action>{
},
response: async ctx => {
const { qrcontent } = <RequestBody>ctx.request.body;
const qrInfo = utils.community.decode(qrcontent);
const qrInfo = utils.community.decrypt(qrcontent);
if (!qrInfo.success || qrInfo.authenticated_type !== AUTHENTICTED_BY_PROPERTY_COMPANY) {
return (ctx.body = {

View File

@ -41,7 +41,7 @@ const MpCommunityFamliyCodeAction = <Action>{
response: async ctx => {
const { building_ids } = <RequestBody>ctx.request.body;
const content = utils.community.encode(building_ids, AUTHENTICTED_BY_FAMILY, ctx.mpUserInfo.id);
const content = utils.community.encrypt(building_ids, AUTHENTICTED_BY_FAMILY, ctx.mpUserInfo.id);
ctx.body = {
code: SUCCESS,

View File

@ -12,8 +12,7 @@
import { Action } from '~/types/action';
import { SUCCESS } from '~/constant/code';
import config from '~/config';
import crypto from 'crypto';
import utils from '~/utils';
const MpUserCardAction = <Action>{
router: {
@ -24,15 +23,10 @@ const MpUserCardAction = <Action>{
},
response: async ctx => {
const cipher = crypto.createCipheriv('aes-256-cbc', config.crypto.key, config.crypto.iv);
let crypted = cipher.update(`${ctx.mpUserInfo.id}-${Date.now()}`, 'utf8', 'hex');
crypted += cipher.final('hex');
ctx.body = {
code: SUCCESS,
data: {
uid: crypted
uid: utils.crypto.encrypt(`${ctx.mpUserInfo.id}-${Date.now()}`)
}
};
}

View File

@ -13,7 +13,6 @@
import { Action } from '~/types/action';
import { SUCCESS, STATUS_ERROR } from '~/constant/code';
import { BINDING_BUILDING, TRUE, FALSE } from '~/constant/status';
import { VISTOR_ACCESS_QRCODE } from '~/constant/open_access';
import utils from '~/utils';
import * as vistorService from '~/service/vistor';
import moment from 'moment';
@ -117,13 +116,6 @@ const MpVistorCreateAction = <Action>{
created_at: Date.now()
});
const uid = utils.openAccess.encode(id, building_id, VISTOR_ACCESS_QRCODE);
await ctx.model
.from('ejyy_vistor')
.update({ uid })
.where('id', id);
if (vistorInfo) {
vistorService.pushAccessToVistor(ctx.model, vistorInfo, id, expire);
}

View File

@ -12,6 +12,7 @@
import { Action } from '~/types/action';
import { SUCCESS } from '~/constant/code';
import { VISTOR_ACCESS_QRCODE } from '~/constant/open_access';
import utils from '~/utils';
interface RequestParams {
@ -51,6 +52,7 @@ const MpVistorDetailAction = <Action>{
'ejyy_vistor.expire',
'ejyy_vistor.used_at',
'ejyy_vistor.created_at',
'ejyy_vistor.building_id',
'ejyy_building_info.type',
'ejyy_building_info.area',
'ejyy_building_info.building',
@ -65,10 +67,13 @@ const MpVistorDetailAction = <Action>{
detail.vistor_phone = utils.phone.hide(detail.vistor_phone);
const uid = utils.access.encrypt(detail.id, detail.building_id, VISTOR_ACCESS_QRCODE);
ctx.body = {
code: SUCCESS,
data: {
...detail
...detail,
uid
}
};
}

View File

@ -12,6 +12,8 @@
import { Action } from '~/types/action';
import { SUCCESS } from '~/constant/code';
import { VISTOR_ACCESS_QRCODE } from '~/constant/open_access';
import utils from '~/utils';
interface RequestParams {
id: number;
@ -47,6 +49,7 @@ const MpVistorUseAction = <Action>{
'ejyy_vistor.expire',
'ejyy_vistor.used_at',
'ejyy_vistor.created_at',
'ejyy_vistor.building_id',
'ejyy_building_info.type',
'ejyy_building_info.area',
'ejyy_building_info.building',
@ -59,10 +62,13 @@ const MpVistorUseAction = <Action>{
.where('ejyy_vistor.id', id)
.first();
const uid = utils.access.encrypt(detail.id, detail.building_id, VISTOR_ACCESS_QRCODE);
ctx.body = {
code: SUCCESS,
data: {
...detail
...detail,
uid
}
};
}

View File

@ -15,7 +15,6 @@ import { SUCCESS } from '~/constant/code';
import * as ROLE from '~/constant/role_access';
import utils from '~/utils';
import { HOUSE, CARPORT, WAREHOUSE, MERCHANT, GARAGE } from '~/constant/building';
import { SELF_ACCESS_QRCODE } from '~/constant/open_access';
interface Building {
type: typeof HOUSE | typeof CARPORT | typeof WAREHOUSE;
@ -127,18 +126,6 @@ const PcBuildingImportAction = <Action>{
});
}
if (type === HOUSE) {
const [accessId] = await ctx.model.from('ejyy_building_access').insert({
building_id: insertId,
created_at
});
const uid = utils.openAccess.encode(accessId, insertId, SELF_ACCESS_QRCODE);
await ctx.model
.from('ejyy_building_access')
.update({ uid })
.where('id', accessId);
}
ids.push(insertId);
}

View File

@ -17,8 +17,6 @@ import { NORMAL_STATUS, TRUE, BINDING_BUILDING } from '~/constant/status';
import { HOUSE, CARPORT, WAREHOUSE, MERCHANT, GARAGE } from '~/constant/building';
import * as ROLE from '~/constant/role_access';
import utils from '~/utils';
import config from '~/config';
import crypto from 'crypto';
interface RequestBody {
uid: string;
@ -57,21 +55,17 @@ const PcOptionCardAction = <Action>{
},
response: async ctx => {
const { uid, community_id } = <RequestBody>ctx.request.body;
let userId = null;
let stamp = null;
const origin = utils.crypto.decrypt(uid);
try {
const cipher = crypto.createDecipheriv('aes-256-cbc', config.crypto.key, config.crypto.iv);
const decrypted = cipher.update(uid, 'hex', 'utf8');
const origin = decrypted + cipher.final('utf8');
[userId, stamp] = origin.split('-');
} catch (e) {
if (!origin) {
return (ctx.body = {
code: QUERY_ILLEFAL,
message: '非法业主名片'
});
}
const [userId, stamp] = origin.split('-');
if (!/^\d+$/.test(userId) || !/^\d{13}$/.test(stamp)) {
return (ctx.body = {
code: QUERY_ILLEFAL,

View File

@ -61,7 +61,7 @@ const PcOwerApproveAction = <Action>{
});
}
const res = utils.community.encode(building_ids, AUTHENTICTED_BY_PROPERTY_COMPANY, ctx.pcUserInfo.id);
const res = utils.community.encrypt(building_ids, AUTHENTICTED_BY_PROPERTY_COMPANY, ctx.pcUserInfo.id);
ctx.body = {
code: SUCCESS,

View File

@ -110,17 +110,12 @@ const PcVistorCreateAction = <Action>{
created_at: Date.now()
});
const uid = utils.openAccess.encode(id, building_id, VISTOR_ACCESS_QRCODE);
await ctx.model
.from('ejyy_vistor')
.update({ uid })
.where('id', id);
if (vistorInfo) {
vistorService.pushAccessToVistor(ctx.model, vistorInfo, id, expire);
}
const uid = utils.access.encrypt(id, building_id, VISTOR_ACCESS_QRCODE);
ctx.body = {
code: SUCCESS,
data: {

View File

@ -13,6 +13,8 @@
import { Action } from '~/types/action';
import { SUCCESS, QUERY_ILLEFAL } from '~/constant/code';
import * as ROLE from '~/constant/role_access';
import { VISTOR_ACCESS_QRCODE } from '~/constant/open_access';
import utils from '~/utils';
interface RequestBody {
id: number;
@ -94,10 +96,13 @@ const PcVistorDetailAction = <Action>{
delete info.property_company_user_id;
const uid = utils.access.encrypt(info.id, info.building_id, VISTOR_ACCESS_QRCODE);
ctx.body = {
code: SUCCESS,
data: {
info,
uid,
registrant
}
};

View File

@ -45,7 +45,7 @@ const PcVistorScanAction = <Action>{
},
response: async ctx => {
const { uid, community_id } = <RequestBody>ctx.request.body;
const { id, building_id, type, success } = utils.openAccess.decode(uid);
const { id, building_id, type, success } = utils.access.decrypt(uid);
if (!success || type !== VISTOR_ACCESS_QRCODE) {
return (ctx.body = {

View File

@ -415,13 +415,6 @@ declare namespace EjyyModel {
replyed_at?: number;
}
interface EjyyBuildingAccess {
id?: number;
building_id: number;
uid?: string;
created_at: number;
}
interface EjyyRepair {
id?: number;
wechat_mp_user_id?: number;
@ -656,7 +649,6 @@ declare namespace EjyyModel {
vistor_phone: string;
car_number?: string;
have_vistor_info: typeof TRUE | typeof FALSE;
uid?: string;
expire: number;
used_at?: number;
scan_by?: number;
@ -1355,7 +1347,6 @@ declare module 'knex/types/tables' {
ejyy_notice_to_user_readed: EjyyModel.EjyyNoticeToUserReaded;
ejyy_notice_tpl: EjyyModel.EjyyNoticeTpl;
ejyy_feedback: EjyyModel.EjyyFeedback;
ejyy_building_access: EjyyModel.EjyyBuildingAccess;
ejyy_repair: EjyyModel.EjyyRepair;
ejyy_repair_urge: EjyyModel.EjyyRepairUrge;
ejyy_complain: EjyyModel.EjyyComplain;

View File

@ -10,39 +10,32 @@
* +----------------------------------------------------------------------
*/
import crypto from 'crypto';
import config from '~/config';
import * as crypto from './crypto';
import { SELF_ACCESS_QRCODE, VISTOR_ACCESS_QRCODE } from '~/constant/open_access';
interface DecodeResult {
interface DecryptResult {
id?: number;
building_id?: number;
type?: typeof SELF_ACCESS_QRCODE | typeof VISTOR_ACCESS_QRCODE;
stamp?: number;
success: boolean;
}
// 区分访客二维码和自己的二维码
export function encode(
// 访客二维码 id 为 vistor_id 自己的二维码为user_id
export function encrypt(
id: number,
building_id: number,
type: typeof SELF_ACCESS_QRCODE | typeof VISTOR_ACCESS_QRCODE
): string {
const cipher = crypto.createCipheriv('aes-256-cbc', config.crypto.key, config.crypto.iv);
let crypted = cipher.update(`${id}#${building_id}#${type}`, 'utf8', 'hex');
crypted += cipher.final('hex');
return crypted;
return crypto.encrypt(`${id}#${building_id}#${type}${Date.now()}`);
}
export function decode(uid: string): DecodeResult {
let origin = null;
export function decrypt(uid: string): DecryptResult {
const origin = crypto.decrypt(uid);
try {
const cipher = crypto.createDecipheriv('aes-256-cbc', config.crypto.key, config.crypto.iv);
const decrypted = cipher.update(uid, 'hex', 'utf8');
origin = decrypted + cipher.final('utf8');
} catch (e) {
if (!origin) {
return { success: false };
}
@ -50,9 +43,15 @@ export function decode(uid: string): DecodeResult {
const id = parseInt(arr[0], 10);
const building_id = parseInt(arr[1], 10);
const type = <typeof SELF_ACCESS_QRCODE | typeof VISTOR_ACCESS_QRCODE>parseInt(arr[2], 10);
const stamp = parseInt(arr[3], 10);
let success = true;
if ((type !== SELF_ACCESS_QRCODE && type !== VISTOR_ACCESS_QRCODE) || building_id === NaN || id === NaN) {
if (
(type !== SELF_ACCESS_QRCODE && type !== VISTOR_ACCESS_QRCODE) ||
building_id === NaN ||
id === NaN ||
/^\d{13}$/.test(stamp.toString())
) {
success = false;
}
@ -60,6 +59,7 @@ export function decode(uid: string): DecodeResult {
id,
building_id,
type,
stamp,
success
};
}

View File

@ -10,8 +10,7 @@
* +----------------------------------------------------------------------
*/
import crypto from 'crypto';
import config from '~/config';
import * as crypto from './crypto';
import { AUTHENTICTED_BY_PROPERTY_COMPANY, AUTHENTICTED_BY_FAMILY } from '~/constant/authenticated_type';
interface DecodeResult {
@ -106,7 +105,7 @@ function fake(len: number): string {
return ret.join('');
}
export function encode(
export function encrypt(
building_ids: number[],
authenticated_type: typeof AUTHENTICTED_BY_PROPERTY_COMPANY | typeof AUTHENTICTED_BY_FAMILY,
user_id: number
@ -120,25 +119,16 @@ export function encode(
ret.push(fake(8));
const cipher = crypto.createCipheriv('aes-256-cbc', config.crypto.key, config.crypto.iv);
let crypted = cipher.update(ret.join(''), 'utf8', 'base64');
crypted += cipher.final('base64');
return {
text: crypted,
text: crypto.encrypt(ret.join('')),
stamp
};
}
export function decode(text: string): DecodeResult {
let str = null;
export function decrypt(text: string): DecodeResult {
let str = crypto.decrypt(text);
try {
const cipher = crypto.createDecipheriv('aes-256-cbc', config.crypto.key, config.crypto.iv);
let decrypted = cipher.update(text, 'base64', 'utf8');
str = decrypted + cipher.final('utf8');
} catch (e) {
if (!str) {
return { success: false };
}

View File

@ -11,6 +11,7 @@
*/
import crypto from 'crypto';
import config from '~/config';
export function md5(str: string, upperCase = false): string {
const hash = crypto.createHash('md5');
@ -21,3 +22,26 @@ export function md5(str: string, upperCase = false): string {
return upperCase ? ret.toUpperCase() : ret;
}
export function encrypt(str: string): string {
const cipher = crypto.createCipheriv('aes-256-cbc', config.crypto.key, config.crypto.iv);
let crypted = cipher.update(str, 'utf8', 'hex');
crypted += cipher.final('hex');
return crypted;
}
export function decrypt(str: string): string {
let result = null;
try {
const cipher = crypto.createDecipheriv('aes-256-cbc', config.crypto.key, config.crypto.iv);
const decrypted = cipher.update(str, 'hex', 'utf8');
result = decrypted + cipher.final('utf8');
} catch (e) {
return result;
}
return result;
}

View File

@ -11,7 +11,7 @@
*/
import * as crypto from './crypto';
import * as openAccess from './open_access';
import * as access from './access';
import * as phone from './phone';
import * as community from './community';
import * as building from './building';
@ -20,10 +20,11 @@ import * as order from './order';
import * as text from './text';
import * as idcard from './idcard';
import * as mail from './mail';
import * as redis from './redis';
const utils = {
crypto,
openAccess,
access,
phone,
community,
building,
@ -31,7 +32,8 @@ const utils = {
order,
text,
idcard,
mail
mail,
redis
};
export default utils;

58
server/src/utils/redis.ts Normal file
View File

@ -0,0 +1,58 @@
/**
* +----------------------------------------------------------------------
* | e家宜业
* +----------------------------------------------------------------------
* | Copyright (c) 2020~2021 https://www.chowa.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed e家宜业
* +----------------------------------------------------------------------
* | Author: jixuecong@chowa.cn
* +----------------------------------------------------------------------
*/
import redis from 'redis';
import config from '~/config';
const client = config.debug ? null : redis.createClient(config.redis);
const ERROR = 'REDIS_ERROR';
const SUCCESS = 'OK';
export type ERROR_TYPE = typeof ERROR;
export type SUCCESS_TYPE = typeof SUCCESS;
const storage = {};
export async function set(key: string, value: string): Promise<ERROR_TYPE | SUCCESS_TYPE> {
if (config.debug) {
storage[key] = value;
return SUCCESS;
}
return new Promise(resolve => {
client.set(key, value, (err, reply) => {
if (err) {
resolve(ERROR);
} else {
resolve(reply);
}
});
});
}
export async function get(key: string): Promise<ERROR_TYPE | string> {
if (config.debug) {
return storage[key];
}
return new Promise(resolve => {
client.get(key, (err, reply) => {
if (err) {
resolve(ERROR);
} else {
resolve(reply);
}
});
});
}

View File

@ -151,7 +151,8 @@ export default {
fetching: true,
detail: {
info: {},
registrant: {}
registrant: {},
uid: ''
},
now: Date.now()
};
@ -175,7 +176,7 @@ export default {
this.detail = res.data;
this.$nextTick(() => {
qrcode.toCanvas(this.$refs.canvas, res.data.info.uid, {
qrcode.toCanvas(this.$refs.canvas, res.data.uid, {
width: 220,
height: 220,
margin: 2