Filters trong Play Framework

Play cũng cấp một API Filter đơn giản cho việc apply global filter cho mỗi request.

Filters so với các action composition

API filter dành cho các mối lo ngại cho việc apply một cách không phân biệt vào tất cả các routes. Cho ví dụ, đây là một vào use cases thông thường cho filter:

  • Logging/metríc collection
  • GZIP encoding
  • Security headers Ngược lại, action composition dành cho các mối lo ngại về các route xác định, chẳng hạn như authentication và authorization, caching,... Nếu bạn không chỉ muốn filter cho một mà bạn muốn aplly cho mọi route, hãy cân nhắc việc sử dụng action composition thay thế, nó có quyền lực hơn rất nhiều. Và đừng quên rằng bạn có thể tự tạp action buiders riêng của mình bằng cách tự biên soạn bộ các định nghĩa tùy chọn của action cho mỗi route, để giảm thiểu các khuôn mẫu có sẵn.

Một logging filter đơn giản

Sau đây là một filer đơn giản ghi lại thời gian một request thực hiện trong Play Framework, class này sẽ implements Filter

import javax.inject.Inject
import akka.stream.Materializer
import play.api.Logger
import play.api.mvc._
import scala.concurrent.{ExecutionContext, Future}

class LoggingFilter @Inject() (implicit val mat: Materializer, ec: ExecutionContext) extends Filter {

  def apply(nextFilter: RequestHeader => Future[Result])
           (requestHeader: RequestHeader): Future[Result] = {

    val startTime = System.currentTimeMillis

    nextFilter(requestHeader).map { result =>

      val endTime = System.currentTimeMillis
      val requestTime = endTime - startTime

      Logger.info(s"${requestHeader.method} ${requestHeader.uri} took ${requestTime}ms and returned ${result.header.status}")

      result.withHeaders("Request-Time" -> requestTime.toString)
    }
  }
}

Ta sẽ cùng hiểu điều gì xảy ra ở đây. Đầu tiên cần chú ý đến signature của apply method. Đó là một function canh chừng, với parameter đầu tiên (nextFilter) là một function có header request và tạo ra một kết quả, và parameter thứ hai (requestHeader) là header request thực tế của request đến. Parameter nextFilter đại diện cho hành động tiếp theo trong chuỗi filter. Invoking nó sẽ tạo ra hành động được onvoking. Phần lớn các cases, bạn có lẽ sẽ muốn invoke nó tại một vài điểm trong tương lai. Bạn có thể quyết định không invoke nó nếu có một vài nguyên nhân làm bạn muốn block một request. Chúng ta lưu lại một timestamp trước khi invoke next filter trong một chuỗi filter. Invoke nextFilter sẽ return một Future[Result]. Hãy tìm hiểu về "Handling asynchronous results" để biết chi tiết hơn về kết quả không đồng bộ. Sau đó, chúng ta thao tác với Result trong Future bằng cách gọi method map với kết thúc là một Result. Chúng ta tính toán thời gian cho một request, log nó và gửi nó trở lại một client trong response header bằng cách gọi result.withHeaders("Request-Time" -> requestTime.toString).

Sử dụng Filters

Cách đơn giản nhất để sử dụng một filter là cung cấp một implementation của HttpFilters trong root package. Nếu bạn sử dụng** runtime dependency injection support** của Play (chẳng hạn như Guice) bạn có thể extend class DefaultHttpFilters và thông qua filters để varatgs constructor:

import javax.inject.Inject
import play.api.http.DefaultHttpFilters
import play.api.http.EnabledFilters
import play.filters.gzip.GzipFilter

class Filters @Inject() (
  defaultFilters: EnabledFilters,
  gzip: GzipFilter,
  log: LoggingFilter
) extends DefaultHttpFilters(defaultFilters.filters :+ gzip :+ log: _*)

Nếu bạn muốn có các filters khác nhau trong các môi trường khác nhau, hoặc ko muốn put chúng trong class trong root package, bạn có thể config tại nơi mà Play nên tìm class bằng setting** lay.http.filters** trong file application.conf để đủ điều kiên tên của một class. Ví dụ:

play.http.filters=com.example.MyFilters

Nếu bạn sử dụng BuiltInComponents cho compile-time dependency injection, bạn có thể override một cách đơn giản httpFilters lazy val:

import play.api._
import play.filters.gzip._
import play.filters.HttpFiltersComponents
import router.Routes

class MyComponents(context: ApplicationLoader.Context)
    extends BuiltInComponentsFromContext(context)
    with HttpFiltersComponents
    with GzipFilterComponents {

  // implicit executionContext and materializer are defined in BuiltInComponents
  lazy val loggingFilter: LoggingFilter = new LoggingFilter()

  // gzipFilter is defined in GzipFilterComponents
  override lazy val httpFilters = Seq(gzipFilter, loggingFilter)

  lazy val router = new Routes(/* ... */)

Các filters cung cấp bởi Play cung cấp các tính trạng hoạt động với BuiltInComponents:

  • GzipFilterComponents
  • CSRFComponents
  • CORSComponents
  • SecurityHeadersComponents
  • AllowedHostsComponents

Filters phù hợp ở đâu?

Filters wrap action sau khi action bị khóa by router. Nó có nghĩa là bạn không thể sử dụng filter để biến đổi một path, method hoặc query parameter để tác động đến router. Tuy nhiên, bạn có thể direct request đến một action khác bằng cash invoke action đó thẳng đến filter, mặc dù lưu ý rằng điều này sẽ bỏ qua phần còn lại của chuỗi filter. Nếu bạn cần modify request trước khi router được invoke, cách tốt hơn để làm là thay thế logic của bạn trong HttpRequestHandler. Kể từ khi filters được apply sau khi routing hoàn thành, có thể truy cập đến thông tin routing từ request, qua attrs map trong RequestHeader. Ví dụ, bạn có thể muốn log time chống lại action method. Trong case này, bạn có thể update filter như sau:

import javax.inject.Inject
import akka.stream.Materializer
import play.api.mvc.{Result, RequestHeader, Filter}
import play.api.Logger
import play.api.routing.{HandlerDef, Router}
import scala.concurrent.{Future, ExecutionContext}

class LoggingFilter @Inject() (implicit val mat: Materializer, ec: ExecutionContext) extends Filter {
  def apply(nextFilter: RequestHeader => Future[Result])
           (requestHeader: RequestHeader): Future[Result] = {

    val startTime = System.currentTimeMillis

    nextFilter(requestHeader).map { result =>
      val handlerDef: HandlerDef = requestHeader.attrs(Router.Attrs.HandlerDef)
      val action = handlerDef.controller + "." + handlerDef.method
      val endTime = System.currentTimeMillis
      val requestTime = endTime - startTime

      Logger.info(s"${action} took ${requestTime}ms and returned ${result.header.status}")

      result.withHeaders("Request-Time" -> requestTime.toString)
    }
  }
}

Các Filters mạnh mẽ hơn

Play cung cấp một API filter level thấp hơn gọi là EssentialFilter đưa bạn full quyền truy cập body của request. API này cho phếp bạn wrap EssentialAction with action khác. Ví dụ Filter dưới đây viết lại một EssentialFilter:

import javax.inject.Inject
import akka.util.ByteString
import play.api.Logger
import play.api.libs.streams.Accumulator
import play.api.mvc._
import scala.concurrent.ExecutionContext

class LoggingFilter @Inject() (implicit ec: ExecutionContext) extends EssentialFilter {
  def apply(nextFilter: EssentialAction) = new EssentialAction {
    def apply(requestHeader: RequestHeader) = {

      val startTime = System.currentTimeMillis

      val accumulator: Accumulator[ByteString, Result] = nextFilter(requestHeader)

      accumulator.map { result =>

        val endTime = System.currentTimeMillis
        val requestTime = endTime - startTime

        Logger.info(s"${requestHeader.method} ${requestHeader.uri} took ${requestTime}ms and returned ${result.header.status}")
        result.withHeaders("Request-Time" -> requestTime.toString)

      }
    }
  }
}

Sự khác nhau chính ở đây, ngoài việc tạo một EssentialAction mới wrap thông qua next action, là khi chúng ta invoke tiếp theo, chúng ta gọi một Accumulator. Bạn có thể tạp ra Accumulator với Akka Streams Flow sử dụng thông qua method với một vài transformations đến stream nếu bạn mong muốn. Sau đó, chúng ta map kết quả của iteratee và xử lý nó.

class AccumulatorFlowFilter @Inject()(actorSystem: ActorSystem)(implicit ec: ExecutionContext) extends EssentialFilter {

  private val logger = org.slf4j.LoggerFactory.getLogger("application.AccumulatorFlowFilter")

  private implicit val logging = Logging(actorSystem.eventStream, logger.getName)

  override def apply(next: EssentialAction): EssentialAction = new EssentialAction {
    override def apply(request: RequestHeader): Accumulator[ByteString, Result] = {
      val accumulator: Accumulator[ByteString, Result] = next(request)

      val flow: Flow[ByteString, ByteString, NotUsed] = Flow[ByteString].log("byteflow")
      val accumulatorWithResult = accumulator.through(flow).map { result =>
        logger.info(s"The flow has completed and the result is $result")
        result
      }

      accumulatorWithResult
    }
  }
}

Tài liệu tham khảo

https://www.playframework.com/documentation/2.6.x/ScalaHttpFilters


All Rights Reserved