跳转至

OAuth 2.0范围的细化授权

在许多应用中,操作有不同程度的访问控制。例如,一个用户可能需要特殊的权限来创建 "笔记",但每个用户都可以阅读笔记。在OAuth 2.0中,操作的权限是由访问令牌的范围决定的。操作可以被定义为需要特定的范围,只有当它的访问令牌被授予了这些范围时,请求才可以调用这些操作。

一个范围是一个字符串标识符,比如 "notes "或 "notes.readonly"。当客户端应用程序代表用户进行身份验证时,它请求将一个或多个这些作用域标识符授予访问令牌。有效的范围将与访问令牌一起存储,这样该范围就可以被后续使用的访问令牌所引用。

Aqueduct的范围用法

访问令牌的范围是在用户认证时确定的。在认证过程中,客户机应用会指示所请求的范围,Aqueduct应用会确定该范围是否允许客户机应用和用户使用。此范围信息附加在访问令牌上。

当使用访问令牌提出请求时,Authorizer会检索该令牌的范围。在请求被验证后,Authorizer将范围信息存储在 Request.authorization中。链接的控制器可以使用这些信息来决定如何处理请求。一般来说,当访问令牌的作用域不足时,控制器会拒绝请求,并发送一个403 Forbidden响应。

因此,向应用程序添加范围包括三个步骤。

  1. 增加业务范围限制。
  2. 为OAuth2客户端标识符(和可选的用户)添加允许的范围。
  3. 在认证时更新客户端应用请求范围。

增加业务范围限制

Authorizer处理请求时,它会创建一个 Authorization对象,并将其附加到请求中。Authorization对象有一个scopes属性,它包含了为访问令牌授予的每个范围。这个对象也有一个方便的方法来检查特定的范围是否对该范围列表有效。

class NoteController extends Controller {
  @override
  Future<RequestOrResponse> handle(Request request) async {
    if (!request.authorization.isAuthorizedForScope("notes")) {
      return Response.forbidden();
    }

    return Response.ok(await getAllNotes());
  }
}

使用 Authorizer

Requestauthorization属性只有在请求被Authorizer处理后才有效。否则为 null

Authorizer也可以验证请求的范围,然后再让它传递给它的链接控制器。

router
  .route('/notes')
  .link(() => Authorizer.bearer(authServer, scopes: ['notes']))
  .link(() => NoteController());

在上面,NoteController仅在请求的承载令牌具有'notes'范围时才可到达。 如果范围不足,则发送403禁止响应。 这适用于NoteController的所有操作。

在同一资源上为不同的操作设置单独的范围通常很有意义。 为此,可以将Scope注释添加到ResourceController操作方法中。

class NoteController extends ResourceController {
  @Scope(['notes.readonly'])
  @Operation.get()
  Future<Response> getNotes() async => ...;

  @Scope(['notes'])
  @Operation.post()
  Future<Response> createNote(@Bind.body() Note note) async => ...;
}

如果请求对于预期的操作方法没有足够的范围,则发送403禁止响应。 在使用 Scope注解时,必须在ResourceController之前链接Authorizer,但是不必指定Authorizer范围。

如果一个 Scope注解或Authorizer包含多个范围条目,则访问令牌必须使每个条目都具有范围。 例如,注解@Scope(['notes','user'])需要访问令牌同时具有`notes'和'user'范围。

界定允许的范围

当客户端应用程序代表用户进行身份验证时,它包含了一个访问令牌的请求范围列表。如果身份验证客户端标识符和身份验证用户都允许请求的范围,那么Aqueduct应用程序将授予令牌请求的范围。

要向验证客户端添加允许的范围,您可以使用aqueduct auth命令行工具。当创建一个新的客户端标识符时,请包含--allowed-scopes 选项。

aqueduct auth add-client \
  --id com.app.mobile \
  --secret myspecialsecret \
  --allowed-scopes 'notes users' \
  --connect postgres://user:password@dbhost:5432/db_name

修改现有的客户机标识符时,使用命令aqueduct auth set-scope

aqueduct auth set-scope \
  --id com.app.mobile \
  --scopes 'notes users' \
  --connect postgres://user:password@dbhost:5432/db_name

每个范围都是一个以空格分隔的字符串;以上示例允许使用客户端IDcom.app.mobile进行身份验证的客户端授予具有'notes'和'users' 范围的访问令牌。如果一个客户端应用请求的范围对该客户端应用来说是不可用的,授予的访问令牌将不包含该范围。如果没有任何请求范围可用于客户端标识符,则不会授予访问令牌。在向应用程序添加范围限制时,必须确保所有有权访问这些操作的客户端应用程序都可以授予该范围。

范围也可能受到应用程序'user'概念的某些属性的限制。通过覆盖AuthServerDelegate中的getAllowedScopes来完成用户级过滤。默认情况下,此方法返回AuthScope.Any,这意味着没有限制。如果客户端应用程序允许该范围,则使用该应用程序登录的任何用户都可以请求该范围。

这个方法可以返回一个对认证用户有效的 AuthScope的列表。以下示例显示了一个ManagedAuthDelegate <T>子类,该子类允许使用@stablekernel.com用户名的任何范围,不允许使用@hotmail.com地址的范围,并且可以使用其他所有人的有限范围:

class DomainBasedAuthDelegate extends ManagedAuthDelegate<User> {
  DomainBasedAuthDelegate(ManagedContext context, {int tokenLimit: 40}) :
        super(context, tokenLimit: tokenLimit);

  @override
  List<AuthScope> getAllowedScopes(covariant User user) {
    if (user.username.endsWith("@stablekernel.com")) {
      return AuthScope.Any;
    } else if (user.username.endsWith("@hotmail.com")) {
      return [];
    } else {
      return [AuthScope("user")];
    }
  }      
}

传给getAllowedScopes的 "user "是被验证的用户。它之前已经由AuthServer获取了。AuthServer通过调用AuthDelegate.getResourceOwner获取这个对象。ManagedAuthDelegate<T>这个方法的默认实现只获取用户的idusernamesalthashedPassword

当使用应用程序的用户对象的某些其他属性来限制允许的范围时,你还必须重写getResourceOwner来获取这些属性。 例如,如果你的应用程序的用户有一个role属性,则必须获取它以及其他四个必需的属性。 这是一个示例实现:

class RoleBasedAuthDelegate extends ManagedAuthDelegate<User> {
  RoleBasedAuthDelegate(ManagedContext context, {int tokenLimit: 40}) :
        super(context, tokenLimit: tokenLimit);

  @override
  Future<User> getResourceOwner(
      AuthServer server, String username) {
    final query = Query<User>(context)
      ..where((u) => u.username).equalTo(username)
      ..returningProperties((t) =>
        [t.id, t.username, t.hashedPassword, t.salt, t.role]);

    return query.fetchOne();
  }

  @override
  List<AuthScope> getAllowedScopes(covariant User user) {
    var scopeStrings = [];
    if (user.role == "admin") {
      scopeStrings = ["admin", "user"];
    } else if (user.role == "user") {
      scopeStrings = ["user:email"];
    }

    return scopeStrings.map((str) => AuthScope(str)).toList();
  }
}

客户端应用集成

与带范围 Aqueduct 应用程序集成的客户端应用程序在执行身份验证时必须包含一个请求的Scope列表。当通过AuthController进行身份验证时,必须在表单数据正文中添加一个 scope参数。该参数的值必须是一个空格分隔的URL编码的请求范围列表。

username=bob&password=foo&grant_type=password&scope=notes%20users

通过AuthCodeController进行身份验证时,会将相同的查询参数添加到初始的GET请求中以呈现登录表单。

身份验证完成后,将在JSON响应正文中以空格分隔的字符串形式提供已授予范围的列表。

{
  "access_token": "...",
  "refresh_token": "...",
  "token_type": "bearer",
  "expires_in": 3600,
  "scopes": "notes users"
}

范围格式和层次结构

关于范围字符串应该是什么样子,除了限于字母数字字符和一些符号之外,没有明确的指导。然而,Aqueduct提供了一个简单的范围结构,有两个特殊的符号,:.

:字符指定了层次结构。例如,下面是一个与用户及其子资源相关的范围的层次结构。

  • user (可以读/写用户拥有的所有内容)
  • user:email (可以读/写用户的电子邮件)
  • user:documents (可以读/写用户的文档)
  • user:documents:spreadsheets (可以读/写用户的电子表格文档)

注意这些范围是如何形成一个层次结构的。每个段都使范围的限制性更强。例如,如果一个访问令牌有user:email范围,它只允许访问一个用户的电子邮件。但是,如果访问令牌有 user范围,它允许访问用户的所有内容,包括他们的电子邮件。

作为另一个例子,具有user:document范围的访问令牌可以访问用户的所有文档,但范围user:document:spreadsheets仅限于电子表格文档。

Scope经常被用来表示读访问和写访问。乍一看,使用层次结构操作符听起来可能是个好主意,例如user:email:readuser:email:write。然而,一个带有user:email:write的访问令牌并没有读取邮件的权限,这很可能是无意的。

这就是范围修饰符的作用所在。范围修饰符是在范围字符串结尾的.之后添加的。例如, user:email.readonly只允许对用户的电子邮件进行只读访问,而user:email允许读写访问。

没有修饰符的访问令牌具有any修饰符的权限。因此,useruser:email可以访问user:email.readonly受保护的资源和操作,但user:email.readonly不能访问受user:email保护的资源。

范围修饰符仅对范围字符串的最后一段有效。也就是说,user:document.readonly:spreadsheets是无效的,但user:document:spreadsheets.readonly是有效的。