提交 3a96266d 编写于 作者: 雪洛's avatar 雪洛

refactor: save token to db

上级 cfd6c490
...@@ -10,3 +10,44 @@ export function getType (val) { ...@@ -10,3 +10,44 @@ export function getType (val) {
export function isPromise (obj) { export function isPromise (obj) {
return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function' return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'
} }
function parseVersion (version) {
if (!version) {
return
}
const matches = version.match(/^(\d+).(\d+).(\d+)/)
if (!matches) {
return
}
return matches.slice(1, 4).map(item => parseInt(item))
}
function compareArray (a, b) {
const minLength = Math.max(a.length, b.length)
const result = 0
for (let i = 0; i < minLength; i++) {
const valA = a[i]
const valB = b[i]
if (valA > valB) {
return 1
} else if (valA < valB) {
return -1
}
}
return result
}
export function compareUniIdVersion (a, b) {
const arrA = parseVersion(a)
const arrB = parseVersion(b)
if (!arrA) {
if (!arrB) {
return 0
}
return -1
}
if (!arrB) {
return 1
}
return compareArray(arrA, arrB)
}
...@@ -21,6 +21,7 @@ function parseErrMsg (err) { ...@@ -21,6 +21,7 @@ function parseErrMsg (err) {
if (errCode in codeMap) { if (errCode in codeMap) {
err.code = codeMap[errCode] err.code = codeMap[errCode]
} }
delete err.errMsgValue
} }
function isUniIDResponse (res) { function isUniIDResponse (res) {
......
import {
checkTokenInternal,
refreshTokenInternal,
getUserPermission
} from './utils'
import {
userCollection
} from '../../common/constants'
export default async function checkToken (token, {
autoRefresh = true
} = {}) {
const payload = await checkTokenInternal.call(this, token)
const {
tokenExpiresThreshold
} = this._getConfig()
const {
uid,
role,
permission
} = payload
const rbacInfo = {
role,
permission
}
if (!role && !permission) {
const getUserRes = await userCollection.doc(uid).get()
const userRecord = getUserRes.data[0]
const {
role: userRole,
permission: userPermission
} = await getUserPermission(userRecord)
rbacInfo.role = userRole
rbacInfo.permission = userPermission
}
if (!tokenExpiresThreshold || !autoRefresh) {
return {
code: 0,
errCode: 0,
...payload,
...rbacInfo
}
}
const now = Date.now()
const needRefreshToken = payload.exp * 1000 - now < tokenExpiresThreshold * 1000
let newToken = {}
if (needRefreshToken) {
newToken = await refreshTokenInternal.call(this, payload)
}
return {
code: 0,
errCode: 0,
...payload,
...rbacInfo,
...newToken
}
}
import {
createTokenInternal
} from './utils'
export default async function createToken ({
uid,
role,
permission
} = {}) {
return createTokenInternal.call(this, {
uid,
role,
permission
})
}
import checkToken from './check-token' import TokenUtils from './token-utils'
import createToken from './create-token'
import refreshToken from './refresh-token'
export { export async function checkToken (token, {
checkToken, autoRefresh = true
createToken, } = {}) {
refreshToken const tokenUtils = new TokenUtils({
uniId: this
})
return tokenUtils.checkToken(token, {
autoRefresh
})
}
export async function createToken ({
uid,
role,
permission
} = {}) {
const tokenUtils = new TokenUtils({
uniId: this
})
return tokenUtils.createToken({
uid,
role,
permission
})
}
export async function refreshToken ({
token
} = {}) {
const tokenUtils = new TokenUtils({
uniId: this
})
return tokenUtils.refreshToken({
token
})
} }
import {
checkTokenInternal,
refreshTokenInternal
} from './utils'
export default async function refreshToken ({
token
} = {}) {
const payload = await checkTokenInternal.call(this, token)
const newToken = await refreshTokenInternal.call(this, payload)
return {
errCode: 0,
...newToken
}
}
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')
}
}
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: ['admin'],
permission: []
}
return this.userPermission
}
const getRoleListRes = await roleCollection.where({
role_id: dbCmd.in(role)
})
const permission = getDistinctArray(
getRoleListRes.data.reduce((list, item) => {
if (item.permission) {
list.push(...item.permission)
}
}, [])
)
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
} = 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)
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.PARAM_REQUIRED,
errMsgValue: {
param: 'token'
}
}
}
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
}
}
import jwt from '../../common/jwt'
import {
ERROR
} from '../../common/error'
import {
dbCmd,
userCollection,
roleCollection
} from '../../common/constants'
import {
getDistinctArray
} from '../../common/utils'
export async function checkTokenInternal (token) {
const {
tokenSecret,
tokenExpiresIn,
tokenExpiresThreshold
} = this._getConfig()
if (tokenExpiresThreshold > tokenExpiresIn) {
throw new Error('Config error, tokenExpiresThreshold should be less than tokenExpiresIn')
}
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
}
export async function refreshTokenInternal (payload) {
// refresh时查库校验对比token创建时间和用户表的valid_token_date
const uid = payload.uid
const getUserRes = await userCollection.doc(uid).get()
const userRecord = getUserRes.data[0]
if (!userRecord) {
throw {
errCode: ERROR.CHECK_TOKEN_FAILED
}
}
if (userRecord.valid_token_date && userRecord.valid_token_date > payload.iat * 1000) {
throw {
errCode: ERROR.TOKEN_EXPIRED
}
}
const {
role,
permission
} = await getUserPermission(userRecord)
return createTokenInternal.call(this, {
uid,
role,
permission
})
}
export async function getUserPermission (userRecord) {
// const getUserRes = await userCollection.doc(uid).get()
// const userRecord = getUserRes.data[0]
if (!userRecord) {
throw {
errCode: ERROR.ACCOUNT_NOT_EXISTS
}
}
checkUserStatus(userRecord)
const role = userRecord.role || []
if (role.length === 0) {
return {
role: [],
permission: []
}
}
if (role.includes('admin')) {
return {
role: ['admin'],
permission: []
}
}
const getRoleListRes = await roleCollection.where({
role_id: dbCmd.in(role)
})
const permission = getDistinctArray(
getRoleListRes.data.reduce((list, item) => {
if (item.permission) {
list.push(...item.permission)
}
}, [])
)
return {
role,
permission
}
}
function checkUserStatus (userRecord) {
switch (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
}
}
async function postCreateToken ({
uid,
signContent,
tokenSecret,
tokenExpiresIn
} = {}) {
const now = Date.now()
const token = jwt.sign(signContent, tokenSecret, {
expiresIn: tokenExpiresIn
})
await userCollection.doc(uid).update({
last_login_ip: this._clientInfo.clientIP,
last_login_date: now
})
return {
token,
tokenExpired: now + tokenExpiresIn * 1000
}
}
export async function createTokenInternal ({
uid,
role,
permission
} = {}) {
const {
tokenSecret,
tokenExpiresIn,
tokenExpiresThreshold
} = this._getConfig()
if (!uid) {
throw {
errCode: ERROR.PARAM_REQUIRED,
errMsgValue: {
param: 'uid'
}
}
}
if (tokenExpiresThreshold > tokenExpiresIn) {
throw new Error('Config error, tokenExpiresThreshold should be less than tokenExpiresIn')
}
if (!role || !permission) {
const getUserRes = await userCollection.doc(uid).get()
const userRecord = getUserRes.data[0]
const getUserPermissionResult = await getUserPermission(userRecord)
role = getUserPermissionResult.role
permission = getUserPermissionResult.permission
}
if (!this.interceptorMap.has('customToken')) {
return postCreateToken.call(this, {
uid,
signContent: {
uid,
role,
permission
},
tokenSecret,
tokenExpiresIn
})
}
const customToken = this.interceptorMap.get('customToken')
if (typeof customToken !== 'function') {
throw new Error('Invalid custom token file')
}
const customTokenRes = await customToken({
uid,
role,
permission
})
return postCreateToken.call(this, {
uid,
signContent: customTokenRes,
tokenSecret,
tokenExpiresIn
})
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册