Browse Source

OAuth2.0

master
巴林闲侠 2 years ago
parent
commit
c4e2f4a61f
  1. 5
      code/api/.vscode/launch.json
  2. 135
      code/api/app/lib/controllers/auth/app.js
  3. 4
      code/api/app/lib/index.js
  4. 1
      code/api/app/lib/middlewares/authenticator.js
  5. 14
      code/api/app/lib/routes/auth/app.js
  6. 68
      code/api/app/lib/service/paasRequest.js
  7. 2
      code/api/app/lib/service/redis.js
  8. 16
      code/api/app/lib/utils/index.js
  9. 62
      code/api/app/lib/utils/oauth2.js
  10. 21
      code/api/config.js
  11. 15
      code/api/utils/forward-api.js
  12. 2
      code/web/package.json

5
code/api/.vscode/launch.json

@ -16,8 +16,9 @@
"-p 4200",
"-f http://localhost:4200",
"-g postgres://postgres:123@10.8.30.32:5432/iot_auth",
"--redisHost 127.0.0.1",
"--redisPort 6379"
"--redisHost 10.8.30.112",
"--redisPort 6379",
"--iotVcmpApi http://127.0.0.1:4000",
]
},
{

135
code/api/app/lib/controllers/auth/app.js

@ -0,0 +1,135 @@
'use strict';
// ! OAUTH2.0 模式
const uuid = require('uuid');
const moment = require('moment');
// 暂时定制予 vcmp 视频平台 app
async function apply (ctx) {
try {
const { models } = ctx.fs.dc;
const { body } = ctx.request;
const { authorization } = ctx.headers;
const { utils: { oauthParseAuthHeader, oauthParseBody } } = ctx.app.fs
await oauthParseBody(body, 'apply');
const keySplit = await oauthParseAuthHeader(authorization);
// TODO 删除此应用下的其他 token
// 记录token
const userInfo = {
appKey: keySplit[0],
appSecret: keySplit[1]
}
const token = uuid.v4();
const expired = moment().add(1, 'year');
await models.UserToken.create({
token: token,
userInfo: userInfo,
expired: expired.format('YYYY-MM-DD HH:mm:ss')
});
ctx.status = 200;
ctx.body = {
token: token,
expires: expired.toISOString()
};
} catch (e) {
ctx.status = 400;
ctx.body = {
name: 'RequestError',
message: e.message
};
}
}
async function refresh (ctx) {
const transaction = await ctx.fs.dc.orm.transaction();
try {
const { authorization } = ctx.headers;
const { body } = ctx.request;
const { models } = ctx.fs.dc;
const { utils: { oauthParseAuthHeader, oauthParseBody } } = ctx.app.fs
const keySplit = await oauthParseAuthHeader(authorization);
const $token = await oauthParseBody(body, 'refresh');
const oldToken = await models.UserToken.findOne({
where: {
token: $token,
expired: { $gte: moment().format('YYYY-MM-DD HH:mm:ss') }
}
});
if (
!oldToken
|| oldToken.userInfo.appKey != keySplit[0]
|| oldToken.userInfo.appSecret != keySplit[1]
) {
throw new Error('参数无效:正文token无效或已过期,请重新申请');
}
const token = uuid.v4();
const expired = moment().add(1, 'year');
// 记录token
const tokenMsg = {
token: token,
userInfo: oldToken.userInfo,
expired: expired.format('YYYY-MM-DD HH:mm:ss')
}
await models.UserToken.create(tokenMsg, { transaction });
await ctx.redis.hmset(token, tokenMsg);
// 移除旧的token
await models.UserToken.destroy({ where: { token: $token }, transaction });
await ctx.redisTools.hdelall($token);
await transaction.commit();
ctx.status = 200;
ctx.body = {
token: token,
expires: expired.toISOString()
};
} catch (e) {
await transaction.rollback();
ctx.status = 400;
ctx.body = {
name: 'RequestError',
message: e.message
};
}
}
async function invalidate (ctx) {
const transaction = await ctx.fs.dc.orm.transaction();
try {
const { models } = ctx.fs.dc;
const { body } = ctx.request;
const { authorization } = ctx.headers;
const { utils: { oauthParseAuthHeader, oauthParseBody } } = ctx.app.fs
const keySplit = await oauthParseAuthHeader(authorization);
const $token = await oauthParseBody(body, 'invalidate');
// 删除token
await models.UserToken.destroy({ where: { token: $token } });
await ctx.redisTools.hdelall($token);
await transaction.commit();
ctx.status = 204;
} catch (e) {
await transaction.rollback();
ctx.status = 400;
ctx.body = {
name: 'RequestError',
message: e.message
};
}
}
module.exports = {
apply,
refresh,
invalidate
}

4
code/api/app/lib/index.js

@ -5,6 +5,8 @@ const authenticator = require('./middlewares/authenticator');
// const apiLog = require('./middlewares/api-log');
const redisConnect = require('./service/redis')
const schedule = require('./schedule')
const utils = require('./utils')
const paasRequest = require('./service/paasRequest');
module.exports.entry = function (app, router, opts) {
app.fs.logger.log('info', '[FS-AUTH]', 'Inject auth and api mv into router.');
@ -15,6 +17,8 @@ module.exports.entry = function (app, router, opts) {
redisConnect(app, opts)
schedule(app, opts)
utils(app, opts)
paasRequest(app, opts)
router.use(authenticator(app, opts));
// router.use(apiLog(app, opts));

1
code/api/app/lib/middlewares/authenticator.js

@ -65,6 +65,7 @@ let isPathExcluded = function (opts, path, method) {
return excluded;
};
// TODO OAuth2 authorization
let authorizeToken = async function (ctx, token) {
let rslt = null;
const tokenFormatRegexp = /^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$/g;

14
code/api/app/lib/routes/auth/app.js

@ -0,0 +1,14 @@
'use strict';
const OAuth = require('../../controllers/auth/app');
module.exports = function (app, router, opts) {
app.fs.api.logAttr['POST/oauth2/token'] = { content: '获取访问令牌', visible: true };
router.post('/oauth2/token', OAuth.apply);
app.fs.api.logAttr['POST/oauth2/token/refresh'] = { content: '刷新访问令牌', visible: false };
router.post('/oauth2/token/refresh', OAuth.refresh);
app.fs.api.logAttr['POST/oauth2/token/invalidate'] = { content: '作废访问令牌', visible: false };
router.post('/oauth2/token/invalidate', OAuth.invalidate);
};

68
code/api/app/lib/service/paasRequest.js

@ -0,0 +1,68 @@
'use strict';
const request = require('superagent')
class paasRequest {
constructor(root, { query = {} } = {}, option) {
this.root = root;
this.query = query
this.option = option
}
#buildUrl = (url) => {
return `${this.root}/${url}`;
}
#resultHandler = (resolve, reject) => {
return (err, res) => {
if (err) {
reject(err);
} else {
resolve(res[this.option.dataWord]);
}
};
}
get = (url, { query = {}, header = {} } = {}) => {
return new Promise((resolve, reject) => {
request.get(this.#buildUrl(url)).set(header).query(Object.assign(query, this.query)).end(this.#resultHandler(resolve, reject));
})
}
post = (url, { data = {}, query = {}, header = {} } = {}) => {
return new Promise((resolve, reject) => {
request.post(this.#buildUrl(url)).set(header).query(Object.assign(query, this.query)).send(data).end(this.#resultHandler(resolve, reject));
})
}
put = (url, { data = {}, header = {}, query = {}, } = {}) => {
return new Promise((resolve, reject) => {
request.put(this.#buildUrl(url)).set(header).query(Object.assign(query, this.query)).send(data).end(this.#resultHandler(resolve, reject));
})
}
delete = (url, { header = {}, query = {} } = {}) => {
return new Promise((resolve, reject) => {
request.delete(this.#buildUrl(url)).set(header).query(Object.assign(query, this.query)).end(this.#resultHandler(resolve, reject));
})
}
}
function factory (app, opts) {
if (opts.pssaRequest) {
try {
for (let r of opts.pssaRequest) {
if (r.name && r.root) {
console.log(`${r.name} request factory created.`);
app.fs[r.name] = new paasRequest(r.root, { ...(r.params || {}) }, { dataWord: r.dataWord || 'body' })
} else {
throw 'opts.pssaRequest 参数错误!'
}
}
} catch (error) {
console.error(error)
process.exit(-1);
}
}
}
module.exports = factory;

2
code/api/app/lib/service/redis.js

@ -47,8 +47,10 @@ module.exports = async function factory (app, opts) {
async function hdelall (key) {
const obj = await client.hgetall(key);
const hkeys = Object.keys(obj)
if (hkeys.length > 0) {
await client.hdel(key, hkeys)
}
}
app.redis = client
app.redisTools = {

16
code/api/app/lib/utils/index.js

@ -0,0 +1,16 @@
'use strict';
const path = require('path');
const fs = require('fs');
module.exports = async function (app, opts) {
fs.readdirSync(__dirname).forEach((filename) => {
if (!['index.js'].some(f => filename == f)) {
const utils = require(`./${filename}`)(app, opts)
app.fs.utils = {
...app.fs.utils,
...utils,
}
}
});
};

62
code/api/app/lib/utils/oauth2.js

@ -0,0 +1,62 @@
module.exports = function (app, opts) {
async function oauthParseAuthHeader (auth) {
if ('isVcmp') {
// 去 vcmp 检查 appkey 和 appsecret 是否正确
try {
const existRes = await app.fs.vcmpRequest.get(`application/check`, { header: { authorization: auth } })
} catch (error) {
throw new Error('应用已禁用或不存在!');
}
}
if (!auth) {
throw new Error('参数无效: 未包含Authorization头');
}
const authSplit = auth.split('Basic');
if (authSplit.length != 2) {
throw new Error('参数无效: Authorization头格式无效,请检查是否包含了"Basic "');
}
const authCode = authSplit[1];
const apikey = Buffer.from(authCode, 'base64').toString();
const keySplit = apikey.split(':');
if (keySplit.length != 2) {
throw new Error('参数无效:请检查Authorization头内容是否经过正确Base64编码');
}
return keySplit;
}
async function oauthParseBody (body, type) {
let checked = true, token = '';
if (type == 'apply' && body['grant_type'] != 'client_credentials') {
checked = false;
} else if (type == 'refresh') {
if (body['grant_type'] != 'refresh_token' || body['token'] == null) {
checked = false;
} else {
token = body['token'];
}
} else if (type == 'invalidate') {
if (body['token'] == null) {
checked = false;
} else {
token = body['token'];
}
}
if (!checked) {
throw new Error('参数无效:请求正文中未包含正确的信息');
}
return token;
}
return {
oauthParseAuthHeader,
oauthParseBody
}
}

21
code/api/config.js

@ -14,17 +14,23 @@ args.option(['f', 'fileHost'], '文件中心本地化存储: WebApi 服务器地
args.option('redisHost', 'redisHost');
args.option('redisPort', 'redisPort');
args.option('redisPswd', 'redisPassword');
args.option('iotVcmpApi', 'IOT视频接入平台api');
const flags = args.parse(process.argv);
const IOT_AUTH_DB = process.env.IOT_AUTH_DB || flags.pg;
const IOT_AUTH_LOCAL_SVR_ORIGIN = process.env.IOT_AUTH_LOCAL_SVR_ORIGIN || flags.fileHost;
// 视频平台api
const IOT_VCMP_API = process.env.IOT_VCMP_API || flags.iotVcmpApi;
// redis
const IOTA_REDIS_SERVER_HOST = process.env.IOTA_REDIS_SERVER_HOST || flags.redisHost || "localhost";//redis IP
const IOTA_REDIS_SERVER_PORT = process.env.IOTA_REDIS_SERVER_PORT || flags.redisPort || "6379";//redis 端口
const IOTA_REDIS_SERVER_PWD = process.env.IOTA_REDIS_SERVER_PWD || flags.redisPswd || "";//redis 密码
if (!IOT_AUTH_DB || !IOTA_REDIS_SERVER_HOST || !IOTA_REDIS_SERVER_PORT) {
if (!IOT_AUTH_DB || !IOTA_REDIS_SERVER_HOST || !IOTA_REDIS_SERVER_PORT ||
!IOT_VCMP_API) {
console.log('缺少启动参数,异常退出');
args.showHelp();
process.exit(-1);
@ -49,14 +55,23 @@ const product = {
opts: {
exclude: [// 不做认证的路由,也可以使用 exclude: ["*"] 跳过所有路由
{ p: '/cross_token/check', o: 'POST' },
{ p: '/user/:userId/message', o: 'GET' }
{ p: '/user/:userId/message', o: 'GET' }, // 自有系统怎么鉴权捏
{ p: '/oauth2/token', o: 'POST' },
{ p: '/oauth2/token/refresh', o: 'POST' },
{ p: '/oauth2/token/invalidate', o: 'POST' },
],
redis: {
host: IOTA_REDIS_SERVER_HOST,
port: IOTA_REDIS_SERVER_PORT,
pwd: IOTA_REDIS_SERVER_PWD
},
}
pssaRequest: [
{// name 会作为一个 request 出现在 ctx.app.fs
name: 'vcmpRequest',
root: IOT_VCMP_API
},
]
},
}
],
dc: {

15
code/api/utils/forward-api.js

@ -1,15 +0,0 @@
'use strict';
const proxy = require('koa-proxy');
const convert = require('koa-convert');
module.exports = {
entry: function (app, router, opts) {
app.use(convert(proxy({
host: opts.host,
match: opts.match,
map: function (path) {
return path.replace(opts.match, '');
}
})));
}
};

2
code/web/package.json

@ -7,7 +7,7 @@
"test": "mocha",
"start-vite": "cross-env NODE_ENV=developmentVite npm run start-params",
"start": "cross-env NODE_ENV=development npm run start-params",
"start-params": "node server -p 5200 -u http://10.8.30.34:4200",
"start-params": "node server -p 5200 -u http://10.8.30.112:4200",
"deploy": "export NODE_ENV=production&& npm run build && node server",
"build-dev": "cross-env NODE_ENV=development&&webpack --config webpack.config.js",
"build": "export NODE_ENV=production&&webpack --config webpack.config.prod.js"

Loading…
Cancel
Save