巴林闲侠
2 years ago
12 changed files with 325 additions and 22 deletions
@ -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 |
||||
|
} |
@ -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); |
||||
|
}; |
@ -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; |
@ -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, |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
}; |
@ -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 |
||||
|
} |
||||
|
} |
@ -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, ''); |
|
||||
} |
|
||||
}))); |
|
||||
} |
|
||||
}; |
|
Loading…
Reference in new issue