import jwt from '../../common/jwt' import { ERROR } from '../../common/error' import { dbCmd, userCollection, roleCollection } from '../../common/constants' import { getDistinctArray, compareUniIdVersion } from '../../common/utils' import { version } from '../../../package.json' export default class TokenUtils { constructor ({ uniId } = {}) { /** * createToken、checkToken、refreshToken均有uid */ this.uid = null /** * createToken、refreshToken均有userRecord,checkToken在刷新token时有userRecord */ this.userRecord = null this.userPermission = null this.oldToken = null this.oldTokenPayload = null this.uniId = uniId this.config = this.uniId._getConfig() this.clientInfo = this.uniId._clientInfo this.checkConfig() } checkConfig () { const { tokenExpiresIn, tokenExpiresThreshold } = this.config if (tokenExpiresThreshold >= tokenExpiresIn) { throw new Error('Config error, tokenExpiresThreshold should be less than tokenExpiresIn') } if (tokenExpiresThreshold > tokenExpiresIn / 2) { console.warn(`Please check whether the tokenExpiresThreshold configuration is set too large, tokenExpiresThreshold: ${tokenExpiresThreshold}, tokenExpiresIn: ${tokenExpiresIn}`) } } get customToken () { return this.uniId.interceptorMap.get('customToken') } isTokenInDb (tokenVersion) { /** * uni-id-common 1.0.10以上版本需要在可能的情况下校验数据库内的token */ return compareUniIdVersion(tokenVersion, '1.0.10') >= 0 } async getUserRecord () { if (this.userRecord) { return this.userRecord } const getUserRes = await userCollection.doc(this.uid).get() this.userRecord = getUserRes.data[0] if (!this.userRecord) { throw { errCode: ERROR.ACCOUNT_NOT_EXISTS } } switch (this.userRecord.status) { case undefined: case 0: break case 1: throw { errCode: ERROR.ACCOUNT_BANNED } case 2: throw { errCode: ERROR.ACCOUNT_AUDITING } case 3: throw { errCode: ERROR.ACCOUNT_AUDIT_FAILED } case 4: throw { errCode: ERROR.ACCOUNT_CLOSED } default: break } // refreshToken、checkToken时如果用到userRecord就会走此逻辑 if (this.oldTokenPayload) { const isTokenInDb = this.isTokenInDb(this.oldTokenPayload.uniIdVersion) // uni-id-common 1.0.10起重新启用token存储于数据库 if (isTokenInDb) { const token = this.userRecord.token || [] if (token.indexOf(this.oldToken) === -1) { throw { errCode: ERROR.CHECK_TOKEN_FAILED } } } // valid_token_date再用户更新密码时会进行更新,目的是让所有token失效 if (this.userRecord.valid_token_date && this.userRecord.valid_token_date > this.oldTokenPayload.iat * 1000) { throw { errCode: ERROR.TOKEN_EXPIRED } } } return this.userRecord } async updateUserRecord (data) { await userCollection.doc(this.uid).update(data) } async getUserPermission () { if (this.userPermission) { return this.userPermission } const userRecord = await this.getUserRecord() const role = userRecord.role || [] if (role.length === 0) { this.userPermission = { role: [], permission: [] } return this.userPermission } if (role.includes('admin')) { this.userPermission = { role, permission: [] } return this.userPermission } const getRoleListRes = await roleCollection.where({ role_id: dbCmd.in(role) }).get() const permission = getDistinctArray( getRoleListRes.data.reduce((list, item) => { if (item.permission) { list.push(...item.permission) } return list }, []) ) this.userPermission = { role, permission } return this.userPermission } /** * 创建token * @param {Object} param * @param {String} param.uid 用户id,必填 * @param {Array} param.role 用户角色,非必填 * @param {Array} param.permission 用户权限,非必填 */ async _createToken ({ uid, role, permission } = {}) { if (!role || !permission) { const getUserPermissionResult = await this.getUserPermission() role = getUserPermissionResult.role permission = getUserPermissionResult.permission } let signContent = { uid, role, permission } if (this.uniId.interceptorMap.has('customToken')) { const customToken = this.uniId.interceptorMap.get('customToken') if (typeof customToken !== 'function') { throw new Error('Invalid custom token file') } signContent = await customToken({ uid, role, permission }) } const now = Date.now() const { tokenSecret, tokenExpiresIn, maxTokenLength = 10 } = this.config const token = jwt.sign({ ...signContent, uniIdVersion: version }, tokenSecret, { expiresIn: tokenExpiresIn }) const userRecord = await this.getUserRecord() const tokenList = (userRecord.token || []).filter(item => { try { const payload = this._checkToken(item) if (userRecord.valid_token_date && userRecord.valid_token_date > payload.iat * 1000) { return false } } catch (error) { if (error.errCode === ERROR.TOKEN_EXPIRED) { return false } } return true }) tokenList.push(token) if (tokenList.length > maxTokenLength) { tokenList.splice(0, tokenList.length - maxTokenLength) } await this.updateUserRecord({ last_login_ip: this.clientInfo.clientIP, last_login_date: now, token: tokenList }) return { token, tokenExpired: now + tokenExpiresIn * 1000 } } /** * 创建token * @param {Object} param * @param {String} param.uid 用户id,必填 * @param {Array} param.role 用户角色,非必填 * @param {Array} param.permission 用户权限,非必填 */ async createToken ({ uid, role, permission } = {}) { if (!uid) { throw { errCode: ERROR.PARAM_REQUIRED, errMsgValue: { param: 'uid' } } } this.uid = uid const { token, tokenExpired } = await this._createToken({ uid, role, permission }) return { errCode: 0, token, tokenExpired } } /** * 刷新token * @param {Object} param * @param {String} param.token 旧token */ async refreshToken ({ token } = {}) { if (!token) { throw { errCode: ERROR.PARAM_REQUIRED, errMsgValue: { param: 'token' } } } this.oldToken = token const payload = this._checkToken(token) this.uid = payload.uid this.oldTokenPayload = payload const { uid } = payload const { role, permission } = await this.getUserPermission() const { token: newToken, tokenExpired } = await this._createToken({ uid, role, permission }) return { errCode: 0, token: newToken, tokenExpired } } /** * 内部checkToken方法 * @param {String} token token内容 */ _checkToken (token) { const { tokenSecret } = this.config let payload try { payload = jwt.verify(token, tokenSecret) } catch (error) { if (error.name === 'TokenExpiredError') { throw { errCode: ERROR.TOKEN_EXPIRED } } throw { errCode: ERROR.CHECK_TOKEN_FAILED } } return payload } /** * 校验token * @param {String} token token * @param {Object} param * @param {Boolean} param.autoRefresh 是否自动刷新,默认自动刷新 */ async checkToken (token, { autoRefresh = true } = {}) { if (!token) { throw { errCode: ERROR.CHECK_TOKEN_FAILED } } this.oldToken = token const payload = this._checkToken(token) this.uid = payload.uid this.oldTokenPayload = payload const { tokenExpiresThreshold } = this.config const { uid, role, permission } = payload const rbacInfo = { role, permission } if (!role && !permission) { const { role: userRole, permission: userPermission } = await this.getUserPermission() rbacInfo.role = userRole rbacInfo.permission = userPermission } if (!tokenExpiresThreshold || !autoRefresh) { const result = { code: 0, errCode: 0, ...payload, ...rbacInfo } delete result.uniIdVersion return result } const now = Date.now() const needRefreshToken = payload.exp * 1000 - now < tokenExpiresThreshold * 1000 let newToken = {} if (needRefreshToken) { newToken = await this._createToken({ uid }) } const result = { code: 0, errCode: 0, ...payload, ...rbacInfo, ...newToken } delete result.uniIdVersion return result } }