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