Введение
Каждый раз, когда запускается новый проект, будете ли вы, как и я, беспокоиться о том, использовать ли на этот раз сейссон или токен для серверной части? Если это токен, есть ли проблема с продлением? Это повторяющаяся совместная отладка, полная входов и выходов, эй 😔...
Текущий процесс:
- Серверная часть отправляет электронное письмо для подачи заявки на SSO и RBAC и предоставляет системный идентификатор.
- Внешний интерфейс заполняет информацию о проекте и автоматически завершает доступ SSO и RBAC.
- Создание ключа RSA, загрузка и разработка шаблона серверной бизнес-системы, совместная отладка инструмента совместной отладки RSA
Вы заметили, что фронтенд и бэкэнд упрощены до такой степени, что продукт может сделать это сам😋.
Боль действительно болезненная
В компании очень зрелая система SSO и RBAC, что очень удобно, но с увеличением проектов меня это не радует, каждый раз запуская новый проект, приходится повторять работу:
1. Функция сеансаПосле того, как пользователь входит в систему единого входа, возвращается только один токен.Бэкенд должен получить информацию о пользователе и разрешения на основе токена и установить сеанс в своей собственной системе, что приводит к серии разработок и совместной отладке интерфейсного и внутренние функции входа и выхода.
2. Проверка разрешенияRBAC предоставляет полные функции. Разрешения интерфейса необходимо ввести в контроллер и действие, а серверная часть должна проверить разрешения на основе информации RBAC. Его нельзя повторно использовать между одной и несколькими бизнес-системами. Этот код функции появится в каждой бизнес-системе.
Есть ли решениеПусть будет, пусть случится или найди другой способ, цитируя фразу из «Собора и базара»:
Чтобы быть хакером, вы должны действительно верить в это и хотеть автоматизировать как можно больше скучных вещей не только для себя, но и для других людей.
Группа внешнего интерфейса тесно сотрудничает с несколькими группами по развитию бизнеса и, естественно, должна брать на себя такие обязанности как за себя, так и за других.
Наша идея состоит в том, чтобы построить средний уровень Node, использовать JWT для входа в систему без сохранения состояния, все внешние интерфейсы перенаправляются в бизнес-систему с пользовательской информацией через Node, а перед переадресацией выполняется проверка разрешений, что является идеальным решением. так просто?
Реализовать идеи
сторона браузера куда мне идти
Первым шагом является отправка браузером данных интерфейса в Node, затем как определить, с какой системой браузер взаимодействует с Node, и как определить, какую среду (тестовую, производственную) перенаправить в систему до того, как Node перенаправит? Многодоменная обработка? Что делать, если бизнес-система не хочет использовать переадресацию?
Ответственность со стороны браузера ясна, то есть сообщать стороне Node, куда я иду, в какой среде, под какой системой я иду, и какие данные прикрепляются.
Входная информация
Большинство наших проектов имеют только тестовую и производственную среды, поэтому мы резервируем тестовые и онлайн-адреса и можем добавить несколько доменных имен, ввести их в систему, нажать «Сохранить», и файл конфигурации будет сгенерирован во внешнем каталоге git для последующие запросы.
В компоненте генерируется интерфейсный адрес всех запрошенных интерфейсов, первая половина — это идентификатор серверной службы, а вторая половина — адрес интерфейса.
Примечание. Наши внешние страницы создаются с использованием внутренней конфигурации системы, и идеи могут быть использованы для справки.
запрос на отправку
Это типичный прокси-режим, в котором все запросы передаются через прокси.request
В методе этот модуль отвечает за различение пересылающих и непересылающих интерфейсов, сопоставление с внутренним адресом в соответствии с fetchUrl и внешними переменными среды и отправку его на сервер Node с системным идентификатором.
Первая категория — это интерфейс, который напрямую взаимодействует с Node, например, логин, данные пользователя и т. д.
Второй тип заключается в переносе информации о пользователе и данных интерфейса на сторону Node и их пересылке бизнес-стороне.
Эти два типа интерфейсов приведут системный моменту, специфичный для формата интерфейса, который необходимо перенаправить.
Формат интерфейса следующий:
параметр | иллюстрировать | Типы |
---|---|---|
agentPath | адрес переадресации | String |
url | переменная среды | |
method | парадигма запроса интерфейса | String |
time | отметка времени | String |
random | случайное число | String |
data | сериализованные данные интерфейса | String |
sign | подпись данных md5 | String |
Например:
agentPath
Это адрес интерфейса, который Node отправит на конец службы в конце.systemNameNode
системный идентификатор, используемый для сопоставления закрытого ключа,data
Это данные, используемые конкретным бизнес-интерфейсом.sign
Просто сделайте простой md5 для проверки других полей на стороне узла.
Остальное остается Node подумать над философским вопросом, кто я такой, ха-ха, это часть нашего разговора.
Реализация сеанса узла кто я
Если интерфейс отправлен, Node получает адрес переадресации и данные интерфейса.
Ха-ха, конечно нет. Мы должны получить доступ к SSO, а затем использовать информацию о пользователе, предоставленную SSO, для установки сеанса уровня узла. Этот раздел в основном представляет собой узел и JWT. Давайте начнем наш первый шаг.
перекрестный домен
Мы используем AntPro для внешнего интерфейса, а инструмент запросаumi-request
, Следует отметить, что если вы отправляете запрос на получение и случайно передаете значение в тело, не забудьте удалить его, иначе не будет сообщено об ошибке и запрос не будет отправлен🙃.
const jsonMethod = ['POST','PUT','DELETE'];
if (jsonMethod.includes(newOptions.method)){
//...
} else {
// 序列化参数
if (newOptions.body) url += `?${stringify(newOptions.body)}
// 删除body
delete newOptions.body
}
Настройки на стороне узла позволяют различать простые и сложные запросы между доменами и доменами (запросы с предварительным обнаружением).Междоменное введение, так как мы используем настраиваемый атрибут «токен» в заголовке HTTP, это сложный запрос, который будет отправлять дополнительный запрос предварительного обнаружения.
app.all('*', function (req, res, next) {
res.header("Access-Control-Allow-Origin", req.headers.origin);
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, token');
res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
res.header("Access-Control-Allow-Credentials", "true");
res.header("Content-Type", "application/json;charset=utf-8");
if (req.method == 'OPTIONS') {
res.send(200); /*让options请求快速返回*/
}
else {
next();
}
});
две просьбы
Вспомогательные настройки для express-jwt
Мы используемjsonwebtoken
а такжеexpress-jwt
Эти два инструмента,jsonwebtoken
Очень взрослый, нечего сказать,express-jwt
Легко позволитьexpress
а такжеjwt
В сочетании с.
переменная среды
Мы используем строки и переменные среды для определенияsecret
, что позволяет избежать проблемы, связанной с тем, что токен не может отличить среду разработки.
Укажите источник токена
getToken указывает источник токена. Файлы cookie использовались и раньше, что позволяет не выполнять некоторую работу внешнего интерфейса по доступу к токенам. Например, чтобы выйти из системы, только сторона Node должна удалить файл cookie, но босс сказал, что, учитывая расширение других клиентов в будущем, таких как небольшие программы, мобильный терминал, мы зарезервировали query.token и headers.token.
интерфейс без входа в систему
Используйте, если не указываете интерфейс, который не проходит проверку токена.Если вы используете регулярку, поместите ее в последнюю цифру массива.Иначе, если регулярка не пройдет, она сразу вернет false 🙂 (указывая на то, что она посадили 🙋).
//签名加盐
const secret = 'salt' + process.env.ACTIVE;
//使用中间件验证token合法性
app.use(expressJwt({
secret: secret,
getToken: function fromHeaderOrQuerystring(req) {
if (req.query && req.query.token) { // 使用query.token
return req.query.token;
} else if (req.headers.token) { // 使用req.headers.token
return req.headers.token
}
return null;
}
}).unless({
path: ['/login','/agent/rsatool', /^\/proxy\/.*/,] //除了这些地址,其他的URL都需要验证
}));
перехват ошибок
Такие какtoken
Если проверка не удалась, вернитесь напрямую{ status: 41002 }
, я предпочитаю использовать код статуса http, но предложение не было одобрено 🙄, оно не было удаленоres.status
Код, а затем стремитесь к следующему 🤤.
//拦截器
app.use(function (err, req, res, next) {
//当token验证失败时会抛出如下错误
if (err.name === 'UnauthorizedError') {
//这个需要根据自己的业务逻辑来处理( 具体的err值 请看下面)
res.status(200).send({ code: -1, msg: '未登录', status: 41002 });
}
});
Разобрать
Для всех интерфейсов, прошедших проверку токена, мы помещаем разобранную информацию о пользователе в req.tokenDecode, что удобно для использования интерфейсом.
// 解析token
app.use((req, res, next) => {
// 获取token
let token = req.headers.token;
if (token) {
let decoded = jwt.decode(token, secret);
req.tokenDecode = decoded
}
next()
});
Процесс входа
Это просто вспомогательные, не генерируетtoken
Ничего хорошего 😜, тогда мы рассмотрим часть логина, как сгенерировать токен на основе информации SSO.
Войти
- браузер отправитьПолучить информацию о пользователепросить
- Анализ узлов
token
, пустой илиtoken
Неверный возврат без входа в систему - Браузер переходит на страницу входа SSO
- Войти успешно перейти кФронтальное доменное имя+
SSOtoken
- браузер отправить
SSOtoken
Запрос на вход в узел - узел будет
SSOtoken
Отправить в систему единого входа для получения информации о пользователе - Node использует информацию о пользователе для создания
token
, и воляtoken
и информация о пользователе отправляется на внешний интерфейс.
не путайSSOtoken
а такжеtoken
,SSOtoken
используется для отправки в систему единого входа для получения информации о пользователе; а такжеtoken
генерируется из пользовательской информации.
Проще говоря: пользователь должен быть пользователем системы единого входа, чтобы генерироватьSSOtoken
, а затем получите информацию о пользователе для созданияtoken
, без подробностей 🤔.
сгенерировать токен
использоватьjwt.sign
генерироватьtoken
, Содержит имя пользователя, информацию о разрешениях.
app.use('/login', async function (req, res) {
// 系统标识
let systemName = req.query.systemNameNode;
// 无系统标识 直接退出
if(!systemName) {
res.send({ code: -1, msg: '无系统标识' });
return
}
// 获取用户信息
let userInfo = await fetch(baseConfig.SSO + "/api/sso/verifyToken", {
method: "post",
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: encode({ token: req.query.token, system: systemName })
}).then((res) => res.json())
if (userInfo.code !== -1) {
let { masterName, masterFullName } = userInfo.data
// 根据用户信息获取菜单
const menu = await getRBAC({ masterName, system:systemName})
// 403
if(menu.code === -1 ) {
res.status(403).send(menu);
} else {
// 接口权限
const { powers } = menu.data;
//生成token
const token = jwt.sign({
systemName,
masterName,
masterFullName,
powers,
}, secret, {
expiresIn: 3600 * 2 //到期时间
});
res.send({ token: token, ...userInfo.data });
}
} else {
res.send(userInfo)
}
});
Exit — это просто интерфейс, который будет существовать в браузере.token
удалять и не рассматривать досрочный выход из системы иtoken
Продлить.
Зная, кто пользователь, следующим шагом будет пересылка.Уровень узла должен решить, следует ли пересылать его бизнес-стороне в соответствии с разрешениями, так как же общаться с бизнес-стороной? Есть ли какие-либо проблемы с безопасностью? 😋 Давайте посмотрим на ретвитнутую часть.
Часть реализации пересылки
Конечная цель-решить две проблемы дублирования труда.Первая решена,а вторая намного проще.Его нужно только судить по разрешениям пользователя перед пересылкой.Узел взаимодействует с бизнес-сторонойRSA
сделал простой знак,RSA
Приложение слишком широкое см.Принцип алгоритма Ruan Yifeng RSA (1)
.
Суждение власти
Требуется запись в RBACController
,Action
информация, соответствующая адресу интерфейса,
Если на рисунке ниже соответствующий адрес интерфейсаweb/delinter
.
Информация о разрешениях, полученная от RBAC, выглядит следующим образом, и эта часть информации существуетtoken
, непосредственно перед пересылкой, из req.tokenDecode
Результат возможен при проверке.
Для удобства чтения вставьте формат интерфейса.
// rbac接口权限判断
const { url } = req.body
// 环境变量、域名、接口地址
let [ env, host, apiUrl ] = url.split('||');
// host 去掉域名http和环境变量信息
host = hostWipeEnv(host);
// 获取控制器和行为
let [ , Controller, Action ] = apiUrl.split('/')
// key转小写,rabc中数据key为小写
Controller = Controller.toLowerCase();
Action = Action.toLowerCase();
// 不带域名 带斜杠与不带斜杠的权限
const noHostPowers = _.get(powers, `/${Controller}.${Action}`) || _.get(powers, `${Controller}.${Action}`);
// 带域名
const key = `${host}:/${Controller}`;
const hasHostPowers = _.get(powers[key], `${Action}`)
// 权限判断
if (!noHostPowers && !hasHostPowers ){
res.send({ code: -1, msg: '接口无权限' });
return
}
формат интерфейса
Должны быть установлены правила для общения.Бизнес-система должна знать информацию о пользователе и данные интерфейса.Другие только для увеличения сложности взлома.
добавить подпись
RSA-подпись
согласно сsystemNameNode
Получите файл с закрытым ключом и подпишите его с помощью закрытого ключа.
1. Создать объект параметра
Содержит имя пользователя, систему, данные, сгенерированное время и случайные свойства.
Код:
'use strict';
// 生成签名
const Encrypt = require('./encrypt');
/**
* 处理接口请求的参数,添加全局参数
* username: 当前登录的用户名
* system: 当前使用的系统名
* time: 当前时间戳
* random: 随机数
* data: 实际请求的参数
*/
module.exports = function (data, signStr, masterName, systemName) {
const newParam = {
username: masterName,
system: systemName,
time: Date.now(),
random: Math.random(),
data,
};
// 生成签名
const sign = new Encrypt(newParam, signStr).value;
newParam.sign = sign;
return newParam;
};
2. Сериализация объекта параметра
Используйте функцию sortAndQuery для сериализации параметров, сгенерированных на первом шаге. Формат сериализации выглядит следующим образом.
data={"page":"1","limit":"10"}&random=0.10105494318877817&system=op-log&time=1578997818828&username=qinshaowei
Код
'use strict';
/**
* 把Object的属性名从小到大排序,如果属性的值是对象则转成JSON字符串,并把所有属性转成query字符串
* obj: 要转换的对象
*/
module.exports = function (obj) {
const keys = Object.keys(obj).sort();
const query = keys.map(key => {
let value = obj[key];
if (typeof value === 'object') {
value = JSON.stringify(value);
}
return `${key}=${value}`;
}).join('&');
return query;
};
3. Создайте подпись, используя сериализованную строку
Используйте алгоритм RAS для создания подписи с использованием алгоритма RAS и возврата объекта записи.
'use strict';
/**
* 用密钥对文本做签名
*/
// 转换对象为query字符串
const sortAndQuery = require('./sortAndQuery');
const NodeRSA = require('node-rsa');
// 生成RSA密钥对象
let private_key;
class Encrypt {
constructor(params, signStr) {
// 秘钥赋值
private_key = new NodeRSA(signStr)
if (params) {
// 如果new对象时就有传入参数,则调用签名方法,返回签名结果,因为class构造函数只能返回对象,所以...
const signStr = this.sign(params);
return {
toString: () => signStr,
value: signStr,
};
}
}
// 对参数对象做签名,返回生成的签名
sign(params, signStr) {
// 先把对象转成query字符串,再做签名
const queryStr = sortAndQuery(params);
const sign = private_key.sign(queryStr, 'base64', 'utf8');
return sign;
}
}
module.exports = Encrypt;
Проверка подписи
Версия узла
// 序列化方法
const sortAndQuery = Param => {
// 排序
const keys = Object.keys(obj).sort();
// 对象转字符串
const query = keys.map(key => {
let value = obj[key];
if (typeof value === 'object') {
value = JSON.stringify(value);
}
return `${key}=${value}`;
}).join('&');
return query;
};
// 初始化
const init = req => {
const { sign, ...params } = req.body;
// 公钥字符串
const public_key_data = '-----BEGIN PUBLIC KEY-----\n' +
'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCD4EalJIz4YMGrj6oARl30Rji7\n' +
'cH9mzW2p2sNIUmNb48TeR7WN17z1lPxPuFkODmMyRuU7i+zZNbx2OZ/lAx8gGNQN\n' +
'BdeUWBUAJX0KfLc3y6bY/k81PE1V7SOV3Ordd8aJwQO/bQnNcDUvC6WP3feTnbu7\n' +
'Mja0HHTpXYcVn9uO+wIDAQAB\n' +
'-----END PUBLIC KEY-----';
const public_key = new NodeRSA(public_key_data);
const dataStr = sortAndQuery(params);
// 返回签名验证是否成功,true or false
return public_key.verify(dataStr, sign, 'utf8', 'base64');
}
Совместная отладка PHP
Во время совместной отладки с бизнес-системой PHP происходит сбой проверки подписи, и информация о подписи, сгенерированная с использованием одного и того же закрытого ключа и данных, несовместима.
Причина в том,node-rsa
и phpopenssl
Правила подписи по умолчанию отличаются, PHP указываетSHA256
Можно, как показано на рисунке:
GO совместная отладка
При совместной отладке с системой go сгенерированная подпись точно такая же, как напечатана в консоли, собственная подпись Go может пройти, а та, что выдана Node, может пройти по-другому.
РаспечататьNodeRSA
При генерации подписи обнаруживается, что если второй входной параметр не передан, выходом по умолчанию является тип буфера, а подпись, сгенерированная go, имеет видbety
Формат, который должен быть сгенерирован Nodebase64
превратиться вbety
Формат.
Действительно ли точка идеальна?
Ну наконец-то я избавился от двух упомянутых выше проблем, счастлив 🙂, подождите, неужели он идеален?
Еще одна цитата из "Собор и базар"
Когда вы решаете проблему в первый раз, вы часто не понимаете ее, и вы можете знать, как решить ее хорошо во второй раз. Итак, если вы хотите сделать это правильно, сделайте это хотя бы еще раз.
Мы находили и решали проблемы по-своему, только чтобы знать, что были созданы новые проблемы.
На совместном RSA бизнес-конце надеется всегда предоставить информацию подписи, генерируемую фиксированное значение, используется для координации, и если государственно-закрытый ключ является договор, необходимо заменить открытый ключ ключевым ключом.
Совместное расследование действительно заняло у меня очень много времени, и весь процесс был в сомнениях друг в друге и неуверенности в себе 😂, тогда повторитеУстранение нового дублирования работы.
Устранение нового дублирования работы
Вновь добавленного дублирования работы может быть не так много, но в будущем будет подключено больше проектов, и время, затрачиваемое на совместную настройку, скорее всего, возрастет в геометрической прогрессии.
решение
- Автоматически генерировать открытый и закрытый ключи и синхронизировать закрытый ключ с приложением Node.
- загружаемый
- Предоставляет интерфейс отладки.
сгенерировать ключ
Это относительно просто и может быть сгенерировано непосредственно с помощью RSA.
const key = new NodeRSA({ b: 512 }); // 生成512位的密钥
const publicDer = key.exportKey('pkcs1-public-pem'); // 公钥
const privateDer = key.exportKey('pkcs1-private-pem'); // 私钥
нажать на git
Все проекты используютgitlab
Хостинг, стандартная эксплуатация и техническое обслуживаниеgithook
+docker
Автоматическое развертывание, использование API gitlab для обновления кода, автоматическое развертывание, отлично 😘.
// 公钥私钥路径
const publicPath = `base/RSA/${proName}/pkcs1-public.pem`;
const privatePath = `base/RSA/${proName}/pkcs1-private.pem`;
// 获取文件返回详情 用于判断更新文件的更新方式
const publicInfo = await getGitLabInfo(publicPath);
const privateInfo = await getGitLabInfo(privatePath);
const getGitLabInfo = async path => {
const result = await getFlieContent(2660, path, 'develop').then(res => res).catch(err => false);
return result;
};
// 生成公钥
await setFlieContent({
projectID: 2660,
filePath: publicPath,
branch: 'develop',
content: publicDer,
method: getMethod(publicInfo),
message: `${userName} 生成公私钥`,
});
// 生成私钥
await setFlieContent({
projectID: 2660,
filePath: privatePath,
branch: 'develop',
content: privateDer,
method: getMethod(privateInfo),
message: `${userName} 生成公私钥`,
});
gitlab API
Коллеги инкапсулируют метод setFlieContent и вызывают его напрямую.
/**
* 修改文件内容
* @param { 项目ID } projectID
* @param { 文件路径 } filePath
* @param { 分支名 } branch
* @param { 文件内容 } content
* @param { commit信息 } message
* @param { 用户email } email
* @param { 用户名 } username
* @param { 请求类型 } method
*/
function setFlieContent({
projectID, filePath, branch, content, message, email, username, method,
}) {
const pathFile = encodeURIComponent(filePath);
return new Promise((resolve, reject) => {
request({
url: `https://git.xin.com/api/v4/projects/${projectID}/repository/files/${pathFile}`,
method,
headers: {
'content-type': 'application/json',
'PRIVATE-TOKEN': PersonalAccessTokens,
},
body: JSON.stringify({
branch,
content,
commit_message: message || '接口提交',
author_email: email || 'machine_xin_com@xin.com',
author_name: username || 'machine_xin_com',
}),
}, (error, response, body) => {
if (!error && (response.statusCode === 200 || response.statusCode === 201)) {
const data = JSON.parse(body);
resolve(data);
} else {
reject(error);
}
});
});
}
Есть сомнения? Для чего нужен метод get? Почему оценивается код возврата 200 или 201?
Я также узнал позже, что меня тронул проект верхнего уровня, потому что gitlab строго следует интерфейсу спецификации HTTP,method
изPUT
а такжеPOST
Обновлены, новый файл, успешный код статуса 200, 201 соответственно.
Скачать ключ
чтениемgitlab
содержимое файла вbuffer
,передачаexpress
изsend
Просто скачайте файл.
const { getFlieContent } = require('../git/fileLIst');
const getRSA = async (req, res) => {
const { proName, keyType } = req.query;
const keyTypes = ['public', 'private'];
// 类型判断
if (!keyTypes.includes(keyType)) {
res.send({code: -1,msg: '类型错误'})
return
}
try {
// 获取秘钥内容
const keyString = await getFlieContent(2660, `base/RSA/${proName}/pkcs1-${keyType}.pem`, 'develop');
// 转为buffer
const buff = Buffer.from(JSON.stringify(keyString, '\t', 2));
// 设置文件类型
res.set({'Content-Disposition': `attachment; filename=pkcs1-${keyType}.pem`});
// 发送文件
res.end(buff);
} catch (error) {
res.send({code: -1,msg: error})
}
};
module.exports = getRSA;
Подвеска инструмента
Многие инструменты просты, но предоставляют удобный способ предварительного просмотра.
веселье и рост
Просто в процессе совместной отладки РАН я набрал много очков знаний, типа встроенного Макаapache
,VScode
Вы можете напрямую запускать код go или php, различать простые и сложные запросы по доменам и т. д. 😇.
Я думаю, что это интересный процесс решения повторяющейся низкоуровневой работы с помощью технологий., Что более интересно, так это то, что вы можете получить ценность и рост в процессе; моя недавняя работа заставила меня осознать свои собственные недостатки,Многому научился и обнаружил, что не узнал больше, начал изучать шаблоны проектирования, сложность программного обеспечения, принципы проектирования, слушал выступления Джона Оустерхаута и Эриха Гаммы и начал изучать архитектуру исходного кода.
Сегодня последний день занятий по лунному календарю в 2019 г. В этом году я могу использовать Google для написания Node, React, vue, разработки инструментов для генерации страниц и выполнения интересующей меня работы.Я могу осознать свою несостоятельность и ценности на работе и все больше и больше понимать свое направление, что делает меня довольным моей нынешней работой., я думаю, что смогу нарисовать счастливый конец 2019 года.
Наконец, пусть чума рассеется после праздника Весны, и пусть вы чувствуете себя счастливыми на работе в ближайшие дни.