在上一篇筆記中,提到使用Node.js作了中間層,對接SSO和RBAC系統,這篇就詳細的來介紹一下具體實現的流程吧。前端
先說明一下技術棧,咱們前端使用的是Ant Design Pro,後端用的express + jsonwebtoken + express-jwt
。node
原本想直接扔一張時序圖,可是怕本身說不清楚,就把步驟一個一個畫了下來了(比較累贅,能夠直接跳過看下方的實現細節)。web
http://sso.xxx.com/login?callback=http://ABIS.xxx.com
;
http://ABIS.xxx.com?token=asdfikj123ijajfjlkajdf
,能夠開看到地址上是有帶過來token的。expressJwt
解析token是否過時來判斷是否要把接口內容轉發至後端的業務系統。Encrypt
將要轉發的信息加密爲簽名發送給後端。redis
,並加上事務,到期自動刪除,咱們也沒作token續簽的功能,能夠在每次token解析後判斷用戶的過時時間,若是小於五分鐘,則生成新的token寫入瀏覽器的cookie中。思路和上邊的同樣,不過還有好多細節須要補充,咱們但願集羣部署這個Node應用,而且前端和Node是分開部署的,須要對跨域作處理。redis
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();
}
});
複製代碼
使用expressJwt
很方便,這也是咱們用它的緣由,而不是用jwt
在實現一遍這樣的功能。算法
const expressJwt = require('express-jwt');
//定義簽名
const secret = 'salt';
//使用中間件驗證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'] //除了這些地址,其餘的URL都須要驗證
}));
複製代碼
爲了方便後續接口的操做,咱們須要把token中的用戶信息提供給轉發的方法用, 咱們把解開的token掛在了req.tokenDecode
上,這一步很重要,若是驗證不經過也要返回給瀏覽器端提示。數據庫
//攔截器
app.use(function (err, req, res, next) {
//當token驗證失敗時會拋出以下錯誤
if (err.name === 'UnauthorizedError') {
//這個須要根據本身的業務邏輯來處理( 具體的err值 請看下面)
res.status(200).send({ code: -1, msg: '未登陸', status: 41002 });
}
});
// 解析token中間件
app.use((req, res, next) => {
// 獲取token
let token = req.headers.token;
if (token) {
let decoded = jwt.decode(token, secret);
req.tokenDecode = decoded
}
next()
});
複製代碼
若是你還記得上邊的流程:express
這是一個大方法,在二、3的步驟中咱們要帶着系統標識發送給SSO,也就說SSO不是隨便一個服務器帶着token請求過來就會返回用戶信息的,每個系統對應的都有一個系統標識(字符串),由於這個Node應用是多項目的,因此咱們把每一個前端域名對應的系統標識存到了庫裏,瀏覽器端的請求代碼都是經過工具生成出來的,都帶有項目Id過來,咱們須要根據id把系統標識查詢出來,若是你不太理解這段什麼意思,能夠忽略掉註釋中獲取server詳情
和得到系統標識
的部分。json
由於要先獲取系統標識再獲取用戶詳情再獲取用戶權限,因此咱們用了async await
串行操做。後端
//定義一個接口,返回token給客戶端. 寫入cookie
app.use('/login', async function (req, res) {
// 獲取server詳情
var getProInfo = require('./model/getProInfo.js');
// 得到系統標識
let systemName = await getProInfo.find({ _id: req.query.id }).then(rs => {
if (rs.length === 0) {
res.send({ code: -1, msg: 'token錯誤' });
}
return rs[0].projectName
})
let systemName = req.query.system;
// 判斷用戶是否登陸 SSO
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
//生成token
const token = jwt.sign({
systemName,
masterName,
masterFullName
}, secret, {
expiresIn: 3600 * 2 //秒到期時間
});
// 寫入cookie
res.cookie('token', token, {
maxAge: 1000 * 60 * 60 * 24 * 30,
domain:'xxx.com', //設置domain 共享當前域下面登陸狀態
httpOnly: true
});
res.send({ token: token, ...userInfo.data });
} else {
res.send(userInfo)
}
});
複製代碼
Node端和業務系統的通信就是經過RSA加簽來通信了,不過這一步性能不好,每一個請求都要加密和解密,居所這個簽名算法很耗費性能,咱們本着先上了再說的原則,先走通,哈哈,若是有好的方法也但願分享一下,一塊兒進步。api
另外,咱們在瀏覽器端和Node端的通信上,也加了一下md5
的驗證,其實沒什麼用,不過咱們仍是加上了,算是加了個讓人疑惑
的功能吧,咱們給每一個請求都加入了sign
字段,這個字段只是把要發送的內容生成了一個字符串,Node端再驗證一下sign
是否一致。
md5加密 在瀏覽器端把發送的內容生成一個字符串,並做爲sign自動發送出去
// 前端項目的request.js中 生成 sign
import hash from 'hash.js';
const fingerprint = url + (options.body ? JSON.stringify(options.body) : '');
const sign = hash
.sha256()
.update(fingerprint)
.digest('hex');
複製代碼
Node的md5驗證與RSA加密 這一段代碼的上半部分是md5
的對比驗證,把數據從新生成md5
字符串,並與請求中的sign
字段對比。
下半部分是RSA加密,其中一個生成的工具方法./../utils/param
在這段代碼的下方貼出來。
var express = require('express');
var router = express.Router();
var hash = require('hash.js');
var request = require('request');
// 封裝參數 加密
const Param = require('./../utils/param');
/* GET home page. */
router.post('/', async function(req, res, next) {
if(req.body.sign){
let { sign, ...body } = req.body
const signCode = hash
.sha256()
.update(body,'utf8')
.digest('hex');
// md5加密對比
if(signCode === sign){
let { data, url } = body
console.log(data,typeof data,'data--------');
// 統一封裝數據
let postData = Param(data);
// 地址正則匹配
url = url.replace(/\$\{(\w+)\}/, ($0, $1) => {
<!--環境變量判斷 可忽略-->
if (process.env === 'prod') {
return 'http://ABIS.xxx.com'
} else if (process.env === 'fat') {
return 'http://ABIS.fat.xxx.com'
} else {
return 'http://ABIS.uat.xxx.com'
}
});
// 請求接口
request({
url: url,
method: "POST",
json: true,
headers: {
"content-type": "application/json",
},
body: postData
}, (error, response, body) => {
if (!error && response.statusCode == 200) {
res.send(response.body)
}else{
res.status(response.statusCode).send({code: -1, msg: '非法請求'})
}
});
}else{
res.send({ code:-1,msg:'非法參數' });
}
}else{
res.send({ code:-1,msg:'非法參數' });
}
});
module.exports = router;
複製代碼
加密方法
Param.js爲封裝與後端通信數據格式,這塊中的用戶名應爲token中解析的用戶名,系統標識數據庫中查詢的系統標識,現用****
代替。
// 生成簽名
const Encrypt = require('./encrypt');
module.exports = function (data) {
const newParam = {
username: '*****',
system: '****',
time: Date.now(),
random: Math.random(),
data,
};
// 生成簽名
const sign = new Encrypt(newParam).value;
newParam.sign = sign;
return newParam;
};
複製代碼
encrypt.js 爲加密方法
'use strict';
/** * 用密鑰對文本作簽名 */
// 轉換對象爲query字符串
const sortAndQuery = require('./sortAndQuery');
const NodeRSA = require('node-rsa');
// 簽名用的密鑰
const private_key_data = '-----BEGIN RSA PRIVATE KEY-----\n' +
'MIICWwIBAAKBgQCD4EalJIz4YMGrj6oARl30Rji7cH9mzW2p2sNIUmNb48TeR7WN\n' +
'IkkUf61VzVxk/K6taQJAc74f49zfD62u0sCcODS3UVUs7c/wEMZE7lmRlx/RQgSE\n' +
'XZYS/Rq+kbkjfb8DWZLVguU1+owiwogUsdwmD4WaMw==\n' +
'-----END RSA PRIVATE KEY-----';
// 生成RSA密鑰對象
const private_key = new NodeRSA(private_key_data);
class Encrypt {
constructor(params) {
if (params) {
// 若是new對象時就有傳入參數,則調用簽名方法,返回簽名結果,由於class構造函數只能返回對象,因此...
const signStr = this.sign(params);
return {
toString: () => signStr,
value: signStr,
};
}
}
// 對參數對象作簽名,返回生成的簽名
sign(params) {
// 先把對象轉成query字符串,再作簽名
const queryStr = sortAndQuery(params);
const sign = private_key.sign(queryStr, 'base64', 'utf8');
return sign;
}
}
module.exports = Encrypt;
複製代碼
基本上算是淌水了一遍JWT
和RSA
,JWT
要相對簡單一些,RSA
本身理解的還不是很好,後期有實踐的話繼續深刻吧,若是最近一週有實踐,但願可以把上篇筆記的拖拽實現和模塊劃分寫一下,見上篇開發一個前端系統生成工具的實現思路 。但願多多督促多多交流哈,給個贊吧😘。