lib/Subject.js

const AuthenticationToken = require('./AuthenticationToken')
const Realm = require('./Realm')
const { TokenInvalidException, UnsupportedTokenException } = require('./AuthException')

/**
 * @class
 * @classdesc 主体类,实例指向当前会话的具体用户,通过它来提供认证和授权信息的存取
 */
class Subject {
  /**
   * @constructor
   * @param {object} ctx egg's ctx
   * @param {SessionManager} 会话管理器的实例
   */
  constructor (ctx, sessionManager) {
    /**
     * egg 的 ctx 对象,通过它来与 session 或其他 cache 打交道,实现数据的存取
     * @public
     */
    this.ctx = ctx
    /**
     * 会话管理器对象,内部通过它来找到会话 id,进而通过 id 去 storage 中找到 token
     * @public
     */
    this.sessionManager = sessionManager
    /**
     * 是否登录
     * @public
     * @type {boolean}
     */
    this.isLogined = false
    /**
     * 主体所属的角色名称
     * @public
     * @type {array<string>}
     */
    this.roleNames = []
    /**
     * 主体所拥有的权限
     * @public
     * @type {object}
     */
    this.permissions = {}
  }
  /**
   * 获取数据源实例
   * @returns {Realm}
   */
  async getRealm () {
    if (!this.__realmInstance) {
      let token = await this.getToken()
      let targetRealm = this.ctx.app.realms.find(item => item.supports(token))
      if (!targetRealm) {
        throw new UnsupportedTokenException()
      }
      this.__realmInstance = new targetRealm(this.ctx)
    }
    return this.__realmInstance
  }
  /**
   * 设置数据源实例
   * @param {Realm} realm 数据源
   */
  setRealm (realm) {
    this.__realmInstance = realm
  }
  /**
   * 初始化方法,在构造完成后会调用,函数内拿到会话对应的 token 异步完成角色和权限数据的获取与设置
   */
  async init () {
    let token = await this.getToken()
    if (token) {
      this.isLogined = true
      let realm = await this.getRealm()
      const roles = await realm.getRoles(token)
      if (roles && roles.length) {
        this.setRoles(roles.map(item => item.name))
        let roleIds = roles.map(item => item.id)
        let permissions = await realm.getPermissions(roleIds)
        this.setPermissions(permissions)
      }
    }
  }
  /**
   * 主体登录
   * @param {AuthenticationToken} token 认证 token 的实例
   * @returns {object} 认证成功后拿到的用户信息
   * @see {@link Realm#authenticate} 内部调用可能抛出异常
   * @throws {TokenInvalidException}
   * @throws {UnsupportedTokenException}
   */
  async login (token) {
    if (token instanceof AuthenticationToken) {
      let targetRealm = this.ctx.app.realms.find(item => item.supports(token))
      if (!targetRealm) {
        throw new UnsupportedTokenException()
      }
      let realm = new targetRealm(this.ctx)
      this.setRealm(realm)
      const user = await realm.authenticate(token)
      this.isLogined = true
      token.uuid = user.id
      token.className = token.constructor.name
      this.setToken(token)
      return user
    } else {
      throw new TokenInvalidException()
    }
  }
  /**
   * 登出
   */
  async logout () {
    const id = await this.sessionManager.getSession()
    this.sessionManager.destroy()
    this.ctx.app.storage.deleteKey(this.ctx, id)
  }
  /**
   * 设置主体的角色
   * @param {array<string>} roleNames  角色名称
   */
  setRoles (roleNames) {
    this.roleNames = [...roleNames]
  }
  /**
   * 设置主体的权限列表
   * @param {array<string>} permissions 权限数组
   */
  setPermissions (permissions) {
    let obj = {}
    permissions.forEach(p => {
      obj[p] = true
    })
    this.permissions = obj
  }
  /**
   * 是否拥有指定权限
   * @param {string} permission 权限名称
   */
  hasPermission (permission) {
    return this.permissions[permission]
  }
  /**
   * 是否拥有指定权限集
   * @param {array<string>} permissions 权限名称集合
   */
  hasPermissions (permissions) {
    return permissions.every(p => {
      return this.permissions[p] !== undefined
    })
  }
  /**
   * 主体是否拥有指定角色
   * @param {string} roleName 角色名称
   */
  hasRole (roleName) {
    return this.roleNames.includes(roleName)
  }
  /**
   * 主体是否拥有指定多个角色
   * @param {array<string>} roleNames 角色名称集合
   */
  hasRoles (roleNames) {
    return roleNames.every(r => {
      return this.roleNames.includes(r)
    })
  }
  /**
   * 设置认证token,内部完成 token 的存储,默认存储在 session 中
   * @param {AuthenticationToken} token 
   */
  async setToken (token) {
    await this.sessionManager.setSession(token.uuid)
    await this.ctx.app.storage.setKey(this.ctx, token.uuid, token)
  }
  /**
   * 获取认证 token,默认从 session 中拿到并构造一个 AuthenticationToken 的实例返回
   * @returns {AuthenticationToken} 认证 token 的实例,或者 null
   */ 
  async getToken () {
    if (this.__token) {
      return this.__token
    }
    const id = await this.sessionManager.getSession()
    if (!id) {
      return null
    }
    const token = await this.ctx.app.storage.getKey(this.ctx, id)
    if (!token) {
      return null
    }
    let TokenClass = this.ctx.app.authTokens.find(item => item.name === token.className)
    if (!TokenClass) {
      throw null
    }
    this.__token = new TokenClass(token)
    return this.__token
  }
}

module.exports = Subject