授权

未匹配的标注

AdonisJS 附带了一个授权框架,可帮助你授权用户对给定资源的操作。例如,检查是否允许登录用户编辑给定的帖子。

@adonisjs/bouncer 包添加了对授权的支持,你必须单独安装它。

注:
@adonisjs/bouncer 包需要 @adonisjs/auth 包来查找当前登录的用户。确保首先配置 auth 包。

// title: Install
npm i @adonisjs/bouncer
// title: Configure
node ace configure @adonisjs/bouncer

# CREATE: start/bouncer.ts
# CREATE: contracts/bouncer.ts
# UPDATE: tsconfig.json { types += "@adonisjs/bouncer" }
# UPDATE: .adonisrc.json { commands += "@adonisjs/bouncer/build/commands" }
# UPDATE: .adonisrc.json { providers += "@adonisjs/bouncer" }
# UPDATE: .adonisrc.json { preloads += "./start/bouncer" }
# CREATE: ace-manifest.json file

基本示例

Bouncer 包的主要目标是帮助你将授权逻辑提取到操作或策略中,而不是将其写入代码库中的任何地方。

你可以在 start/bouncer.ts 文件中定义 Bouncer 操作。 Bouncer.define 方法接受动作名称和闭包来编写授权逻辑。

import Post from 'App/Models/Post'
import User from 'App/Models/User'

export const { actions } = Bouncer
  .define('viewPost', (user: User, post: Post) => {
    return post.userId === user.id
  })

你可以通过多次链接 .define 方法来定义多个操作。例如:

export const { actions } = Bouncer
  .define('viewPost', (user: User, post: Post) => {
    return post.userId === user.id
  })
  .define('editPost', (user: User, post: Post) => {
    return post.userId === user.id
  })
  .define('deletePost', (user: User, post: Post) => {
    return post.userId === user.id && post.status !== 'published'
  })

定义操作后,你可以使用 ctx.bouncer 对象在路由处理程序中访问它。

bouncer.authorize 方法接受动作名称和它接收的参数。 user从当前登录的用户推断出来的。因此,无需显式传递用户。

import Route from '@ioc:Adonis/Core/Route'
import Post from 'App/Models/Post'

Route.get('posts/:id', async ({ bouncer, request }) => {
  const post = await Post.findOrFail(request.param('id'))

  await bouncer.authorize('viewPost', post)
})

定义动作

你可以使用 Bouncer.define 方法定义内联操作。由于通常会针对用户检查权限,因此你的操作必须接受用户作为第一个参数,然后是表示授权逻辑所需的任何其他数据。

import Post from 'App/Models/Post'
import User from 'App/Models/User'

export const { actions } = Bouncer
  .define('viewPost', (
    user: User, // 用户应该始终是第一个参数
    post: Post
  ) => {
    return post.userId === user.id
  })

使用不同的用户模型

你不仅限于使用 User 模型,也可以定义需要不同用户模型的操作,Bouncer 将使用 TypeScript 类型推断来过滤适用于给定用户类型的操作。

访客用户

有时你可能想要编写在没有用户的情况下也可以工作的操作。例如,你希望允许访客访问者查看所有已发布的帖子。但是,未发布的帖子应该只对帖子作者可见。

在这种情况下,你必须将 options.allowGuest 属性设置为 true

备注:
如果 allowGuest !== true 并且没有登录用户,那么 Bouncer 甚至不会调用该操作并隐式拒绝该请求。

export const { actions } = Bouncer
  .define('viewPost', (user: User | null, post: Post) => {
    if (post.status === 'published') {
      return true
    }

    if (!user) {
      return false
    }

    return post.userId === user.id
  }, {
    allowGuest: true, // 👈
  })

拒绝访问

一个动作可以通过从动作闭包返回一个非真值来拒绝访问,并且 Bouncer 会将其转换为 403 状态码。

但是,你也可以使用Bouncer.deny 方法从操作本身返回自定义消息和状态代码。

export const { actions } = Bouncer
  .define('viewPost', (user: User, post: Post) => {
    if (post.userId === user.id || post.status === 'published') {
      return true
    }

    return Bouncer.deny('Post not found', 404)
  })

授权操作

你可以使用 bouncer.authorize 方法针对一组预定义的操作授权用户。它接受要授权的操作的名称,以及它接受的参数(不包括为用户保留的第一个参数)。

当操作拒绝访问时,authorize 方法会引发 AuthorizationException

Route.get('posts/:id', async ({ bouncer, request }) => {
  const post = await Post.findOrFail(request.param('id'))

  // 授权用户访问给定的帖子
  await bouncer.authorize('viewPost', post)
})

默认情况下,ctx.bouncer 对象授权针对当前登录用户的操作。但是,你可以使用 forUser 方法显式定义用户。

const admin = await Admin.findOrFail(1)

// 获取管理模型的子实例
const adminAuthorizer = bouncer.forUser(admin)

await adminAuthorizer.authorize('viewPost', post)

bouncer.allows

bouncer.allows 方法接受与 bouncer.authorize 方法相同的参数集。但是,它不是抛出异常,而是返回一个布尔值,指示是否允许某个操作。

if (await bouncer.allows('viewPost', post)) {
  // 其他逻辑
}

bouncer.denies

bouncer.allows 的反面是 bouncer.denies 方法。

if (await bouncer.denies('editPost', post)) {
  // 其他逻辑
}

保镖钩子(Bouncer hooks)

Bouncer 挂钩允许你定义 beforeafter 生命周期挂钩。你可以使用这些生命周期挂钩向管理员或超级用户授予特殊权限。

生命周期前的钩子(Before hook)

在以下示例中,超级用户被授予 before 生命周期挂钩内的所有访问权限。

// 文件名: start/bouncer.ts
Bouncer.before((user: User | null) => {
  if (user && user.isSuperUser) {
    return true
  }
})
  • 当 before 钩子返回 truefalse 值时,永远不会执行实际的操作回调。
  • 如果你希望 Bouncer 执行下一个挂钩或操作回调,请确保返回 undefined
  • before 钩子总是被执行,即使没有登录用户。确保在钩子回调中处理丢失用户的用例。
  • before 钩子接收 action name 作为第二个参数。

生命周期之后的钩子(After hook)

after 钩子在执行操作回调后执行。如果 after 钩子返回 truefalse 值,它将被视为最终响应,我们将丢弃该操作的响应。

Bouncer.after((user: User | null, action, actionResult) => {
  if (actionResult.authorized) {
    console.log(`${action} was authorized`)
  } else {
    console.log(`${action} denied with "${actionResult.errorResponse}" message`)
  }
})

使用策略

将所有应用程序权限表示为单个文件中的操作是不切实际的,因此保镖允许你将权限提取到专用策略文件。

通常,你将为给定资源创建一个策略。例如,管理 Post 资源 权限的策略,管理 Comment 资源 权限的另一个策略,等等。

创建策略文件

你可以通过运行以下 Ace 命令来创建策略。策略存储在 app/Policies 目录中。但是,可以通过更新 .adonisrc.json 文件 中的 namespaces.policies 属性来自定义位置。

node ace make:policy Post

# CREATE: app/Policies/PostPolicy.ts

每个策略类都扩展了 BasePolicy,并且类公共方法被视为策略操作

策略操作的工作方式与独立保镖操作类似。第一个参数是为用户保留的,动作可以接受任意数量的附加参数。

import User from 'App/Models/User'
import Post from 'App/Models/Post'
import { BasePolicy } from '@ioc:Adonis/Addons/Bouncer'

export default class PostPolicy extends BasePolicy {
  public async view(user: User, post: Post) {
    return post.userId === user.id
  }
}

向 Bouncer 注册策略

此外,请确保在 start/bouncer.ts 文件中注册新创建的策略。registerPolicies 方法接受一个键值对。 key 为策略名称,value 为延迟导入 Policy 文件的函数。

export const { policies } = Bouncer.registerPolicies({
  PostPolicy: () => import('App/Policies/PostPolicy')
})

使用策略

注册策略后,你可以使用 bouncer.with 方法访问它。

import Route from '@ioc:Adonis/Core/Route'
import Post from 'App/Models/Post'

Route.get('posts/:id', async ({ bouncer }) => {
  const post = await Post.findOrFail(1)
  await bouncer
    .with('PostPolicy')
    .authorize('view', post)
})

策略挂钩

策略还可以通过实现 beforeafter 方法来定义挂钩。同样,挂钩遵循与 独立保镖挂钩 相同的生命周期。

import User from 'App/Models/User'
import Post from 'App/Models/Post'
import { BasePolicy } from '@ioc:Adonis/Addons/Bouncer'

export default class PostPolicy extends BasePolicy {
  public async before(user: User | null) {
    if (user && user.isSuperUser) {
      return true
    }
  }

  public async view(user: User, post: Post) {
    return post.userId === user.id
  }
}

访客用户

要授权来宾用户的请求,你必须使用 @action 装饰器标记策略操作并设置 options.allowGuests = true

export default class PostPolicy extends BasePolicy {

  @action({ allowGuest: true })
  public async view(user: User | null, post: Post) {
    if (post.status === 'published') {
      return true
    }

    if (!user) {
      return false
    }

    return post.userId === user.id
  }
}

Edge 模板内的用法

你可以使用 @can@cannot 标记有条件地显示标记的特定部分。例如:当用户无法执行这些操作时,隐藏链接以删除和编辑帖子。

@can('editPost', post)
  <a href="{{ route('posts.edit', [post.id]) }}"> Edit </a>
@end

@can('deletePost', post)
  <a href="{{ route('posts.delete', [post.id]) }}"> Delete </a>
@end

你还可以使用点符号引用策略的操作。第一部分是策略名称,第二部分是策略操作。

备注:
策略名称位于 start/bouncer.ts 文件中。 registerPolicies 对象的键是策略名称。

@can('PostPolicy.edit', post)
  <a href="{{ route('posts.edit', [post.id]) }}"> Edit </a>
@end

此外,你可以使用 @cannot 标记编写逆条件。

@cannot('PostPolicy.edit')
  <!-- Markup -->
@end

@can@cannot 标签授权针对当前登录用户的操作。如果底层保镖/策略操作需要不同的用户,你将必须传递一个明确的授权实例。

@can('PostPolicy.edit', bouncer.forUser(admin), post)
@end

在上面的示例中,第二个参数 bouncer.forUser(admin) 是特定用户的保镖的子实例,后跟操作参数。

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/adonisjs/5.x/di...

译文地址:https://learnku.com/docs/adonisjs/5.x/di...

上一篇 下一篇
贡献者:2
讨论数量: 0
发起讨论 只看当前版本


暂无话题~