【问题标题】:How to access the body of a Request[_] as byte array如何以字节数组的形式访问 Request[_] 的主体
【发布时间】:2023-04-03 21:15:01
【问题描述】:

访问请求正文的字节数组很简单,只要在定义 Action 时使用适当的正文解析器,例如 request.body.asRaw...

但是,我现在正在为 HMAC-secured Actions 构建一个ActionBuilder,在这种情况下,访问主体是不可避免的。问题是 ActionBuilders 的定义在请求类型方面是通用的,因此对于正文解析器也是:

def invokeBlock[A](request: Request[A], block: HmacRequest[A] => Future[SimpleResult])

由于A 没有任何类型限制,似乎没有任何方法可以从Request[_] 访问请求正文。

在我的具体情况下,它可以执行以下操作:

request.body.asInstanceOf[AnyContentAsJson].json.toString()...

但这对我来说不是一个可接受的解决方案。

我还尝试定义自定义正文解析器并将其应用于Request[_],但结果为空。

如何访问Request[_] 的主体(字节数组表示就足够了)?


更新:如果我可以访问 ActionBuilder 中的请求正文,这也是一个可接受的解决方案,例如通过将整个处理包装在另一个执行自定义解析的操作中。但我不明白这将如何工作......该解决方案应该是可重用的,因为可以将任意用户定义的操作与 HMAC 功能一起使用,而不会干扰任何用户逻辑。

【问题讨论】:

    标签: scala playframework playframework-2.2


    【解决方案1】:

    我们解决这个问题的方法(在 Play 2.3 中)是构建一个 BodyParser,它并行运行 2 个 BodyParsers。使用它,您可以运行 BodyParsers.parse.raw 或除主程序之外的任何其他内容。将原始解析器与验证相结合(此处未显示),并生成一个 Left[Result] 以及您喜欢的任何错误消息和状态,以实现您想要的结果。

    import scala.concurrent.ExecutionContext    
    
    import play.api.libs.concurrent.Execution.defaultContext
    import play.api.libs.iteratee.Enumeratee
    import play.api.libs.iteratee.Iteratee
    import play.api.mvc.BodyParser
    import play.api.mvc.RequestHeader
    import play.api.mvc.Result  
    
    /**
     * A BodyParser which executes any two provided BodyParsers in parallel.
     *
     * The results are combined in the following way:
     * If any wrapped parser's Iteratee encounters an Error, that's the result.
     * Else if the first parser's Iteratee finally yields a Left, this is used as the result.
     * Else if the second parser's Iteratee yields a Left, this is used as the result.
     * Else both Right results are combined in a Right[(A, B)].
     *
     * This can be used to e.g. provide the request's body both as a RawBuffer and a json-parsed
     * custom model class, or to feed the body through a HMAC module in addition to parsing it.
     *
     * @author Jürgen Strobel <juergen@strobel.info>
     */
    final case class DualBodyParser[+A, +B](
        a: BodyParser[A],
        b: BodyParser[B]
    )(
        implicit ec: ExecutionContext = defaultContext
    )
    extends BodyParser[(A, B)]
    {
        def apply(v1: RequestHeader): Iteratee[Array[Byte], Either[Result, (A, B)]] =
            Enumeratee.zipWith(a(v1), b(v1)) {
                case (Left(va), _) => Left(va)
                case (_, Left(vb)) => Left(vb)
                case (Right(va), Right(vb)) => Right((va, vb))
            }
    }
    

    我们还创建了 ActionBuilder 的自定义变体,它使用 DualBodyParser 和我们自己的验证逻辑包装任何用户提供的 BodyParser,并且只有在身份验证成功后才将第二个的结果再次拼接到用户提供的代码块。请注意,ActionBuilder 的这种变体复制并粘贴了大部分原始 ActionBuilder 代码,但需要一个参数来注入我们的身份验证逻辑,因此它是一个类而不是对象。将有不同的方法来解决这个问题。在 BodyParser 中封装完整的身份验证逻辑在 TODO 列表中,并且可能更容易和更好地组合。

    /**
     * An FooAuthenticatedAction does Foo authentication before invoking its action block.
     *
     * This replicates ActionBuilder and Action almost fully.
     * It splices a parser.tolerantText BodyParser in, does Foo authentication with the
     * body text, and then continues to call the provided block with the result of the
     * provided body parser (if any).
     *
     * @param fooHelper An FooHelper configured to handle the incoming requests.
     *
     * @author Jürgen Strobel <juergen@strobel.info>
     */
    case class FooAuthenticatedAction(fooHelper: FooHelper) extends ActionFunction[Request, Request] {
      self =>
    
        final def apply[A](bodyParser: BodyParser[A])(block: Request[A] => Result): Action[(String, A)] =
            async(bodyParser) { req: Request[A] =>
                Future.successful(block(req))
            }
    
        final def apply(block: Request[AnyContent] => Result): Action[(String, AnyContent)] =
            apply(BodyParsers.parse.anyContent)(block)
    
        final def apply(block: => Result): Action[(String, AnyContent)] =
            apply(_ => block)
    
        final def async(block: => Future[Result]): Action[(String, AnyContent)] =
            async(_ => block)
    
        final def async(block: Request[AnyContent] => Future[Result]): Action[(String, AnyContent)] =
            async(BodyParsers.parse.anyContent)(block)
    
        final def async[A](bodyParser: BodyParser[A])(block: Request[A] => Future[Result]): Action[(String, A)] =
            composeAction(
                new Action[(String, A)] {
                    def parser = DualBodyParser(parse.tolerantText, composeParser(bodyParser))
                    def apply(request: Request[(String, A)]) = try {
                        fooHelper.authenticate(request map (_._1)) match {
                            case Left(error) => failUnauthorized(error)
                            case Right(_key) => invokeBlock(request map (_._2), block)
                        }
                    } catch {
                        case e: NotImplementedError => throw new RuntimeException(e)
                        case e: LinkageError => throw new RuntimeException(e)
                    }
                    override def executionContext = self.executionContext
                }
            )
    
        /**
         * This produces the Result if authentication fails.
         */
        def failUnauthorized(error: String): Future[Result] =
            Future.successful( Unauthorized(error) )
    
        protected def composeParser[A](bodyParser: BodyParser[A]): BodyParser[A] = bodyParser
    
        protected def composeAction[A](action: Action[A]): Action[A] = action
    
        // we don't use/support this atm
        /**
          override def andThen[Q[_]](other: ActionFunction[R, Q]): ActionBuilder[Q] = new ActionBuilder[Q] 
        **/
    
        def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) =
            block(request)
    }
    

    【讨论】:

      【解决方案2】:

      请求类只有一个body字段,当body解析器成功解析请求体时,会创建一个Request[A]的实例。通常,将原始字节与 A 实例一起使用并不有趣,因为这将占用每个请求的两倍内存量。

      主体解析器可以继续消费或提前返回它所提供的每个字节块。 也许您可以将 Hmac 验证内容实现为包装体解析器?

      对于每个输入块 (Array[Byte]),您将使用包装 iteratee/enumeratee 收集字节。当输入结束时,您会触发对这些字节的 hmac 签名计算/验证,如果它无效,则可以返回 BadRequest 或将整个正文推送到实际的正文解析器。

      【讨论】:

      • 如果我不得不写Action(parse.hmacParser) { ...,这对我来说也不是一个可接受的解决方案,因为这显然限制了可重用性。如果有可能有一个ActionBuilder 在内部使用它自己的BodyParser 并以某种方式公开解析的数据,但使用原始正文解析器调用包装的Action,那将是可以接受的。但我不明白这怎么可能。
      • 如果你有一个 hmacParser(parse.actualParser) 我认为你可以创建一个动作构建器或辅助方法来获得类似 HmacProtected(parse.actualParser) { ...
      • 另外,您可以在创建 ActionBuilder 时覆盖 composeParser 并用您的 hmac 解析器包装实际的解析器。
      • 如何确切地?我只能返回一个 BodyParser/Iteratee...我应该如何在不“破坏”用户定义的解析器的情况下解析内容?几乎没有关于这个主题的文档。
      • 好吧,我没有时间为你做这件事,你需要阅读 iteratees api,但你应该能够创建一个 Enumeratee/Iteratee 来收集字节并传递他们到另一个迭代。在 play doc 和各种在线博客中都有很多关于迭代的文档,即使没有人为您的确切问题编写解决方案。
      猜你喜欢
      • 1970-01-01
      • 2020-09-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多