How to integrate Play (web framework), Deadbolt (authorization) and Slick (database access)

时间:2016-04-04 17:28:55

标签: scala playframework slick playframework-2.5 deadbolt-2

Briefly: my application uses the Play web framework version 2.5.1. I want to use the Deadbolt authorization system, and Slick to access the user-authorization information in my database. How can I do this? Deadbolt is made specifically for Play, and Play comes with Slick integrated out-of-the-box, so it ought to be possible if not very easy.

Based on "Integrating Deadbolt" from the Deadbolt documentation, I extended the DeadboltHandler trait. Its abstract getSubject() method seems like the place to do the database query (so says the documentation but without any example). That method receives as an argument an AuthenticatedRequest and returns the Subject, basically the user-id that was authenticated, along with roles and permissions (authorizations).

I am stuck, because while Play comes with Slick integration, the documentation describes only how to use it from within a Play controller. (Note I am wanting to do this using dependency injection since using global lookups is deprecated and error-prone)

I am successfully using Deadbolt in my controller to restrict access to certain resources, but the controller seems like the wrong place for Deadbolt to be doing database queries for authorization details (if it were, then the DeadboltHandler would be purposeless). The controller constructor signature definition looks something like (note the controller accesses the default database that stores web content rather than the authorization database):

class Application @Inject()(
  dbConfigProvider: DatabaseConfigProvider,
  playConfig: play.api.Configuration,
  deadbolt: DeadboltActions
) extends Controller {

That works. However, similarly annotating the DeadboltHandler extension with @Inject fails to provide Slick access to the database:

class AuthHandler @Inject()(@play.db.NamedDatabase("auth") dbConfigProvider: DatabaseConfigProvider)
  extends DeadboltHandler {

the result being

not enough arguments for constructor AuthHandler: (dbConfigProvider: play.api.db.slick.DatabaseConfigProvider)services.AuthHandler.
Unspecified value parameter dbConfigProvider.

Obviously, Play does something special for controllers so that the @Inject annotation works, something of which I lack understanding. I presume it is in the nature of constructing controllers using an injector rather than the new keyword, but my search through the Play source code has failed to show me what exactly is happening. If I could find that, perhaps I could mimic that technique to construct a DeadboltHandler.

I see that play comes with classes such as GuiceInjector and GuiceInjectorBuilder, which sound as if they might be part of the solution, but my experimentation has yet failed to show me how, and if there is any documentation on how to use them in the specific context of a DeadboldHandler extension, I am missing it.

I found this previous question: Scala (Play 2.4.x) How to call a class with @inject() annotation, which seems very much on point. Unfortunately, despite a half-dozen follow-up comments from the original poster, it is yet unanswered. I feel if I had the answer to that question I would have the answer to this question, though my question is very specific: how to use Play and Deadbolt and Slick with each other (in Scala).

What baffles me most is that this seems like something that ought to be common enough that it would be either mentioned in the documentation or have been asked about already on SO. My failure to find any such references typically means I am doing something so uniquely wrong that nobody else has ever had occasion to talk about it. It certainly seems as if it ought to be simple enough that I am optimistically hoping that I am missing something very basic, and I look forward to some kind soul informing me of that knowledge.

1 个答案:

答案 0 :(得分:2)

正如您在问题中所述,检索用户的位置在DeadboltHandler.getSubject。实际上,您可以将特定于数据库的代码移动到自己的类中,因此在此示例中,这就是我所做的。

这是DeadboltHandler的通用实现;你应该能够将它放入你的代码并按原样使用它,因为持久性细节将在稍后处理。

import javax.inject.{Inject, Singleton}

import be.objectify.deadbolt.scala.models.Subject
import be.objectify.deadbolt.scala.{AuthenticatedRequest, DeadboltHandler, DynamicResourceHandler}
import models.{LogInForm, User}
import play.api.mvc.{Request, Result, Results}
import play.twirl.api.HtmlFormat
import views.html.security.denied

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

@Singleton
class MyDeadboltHandler @Inject() (authSupport: AuthSupport) extends DeadboltHandler {

  override def beforeAuthCheck[A](request: Request[A]): Future[Option[Result]] = Future {None}

  override def getDynamicResourceHandler[A](request: Request[A]): Future[Option[DynamicResourceHandler]] = Future {None}

  /**
    * Get the current user.
    *
    * @param request the HTTP request
    * @return a future for an option maybe containing the subject
    */
  override def getSubject[A](request: AuthenticatedRequest[A]): Future[Option[Subject]] = 
    Future {
      request.subject.orElse {
        // replace request.session.get("userId") with how you identify the user
        request.session.get("userId") match {
          case Some(userId) => authSupport.getUser(userId)
          case _ => None
        }
      }}

  /**
    * Handle instances of authorization failure.
    *
    * @param request the HTTP request
    * @return either a 401 or 403 response, depending on the situation
    */
  override def onAuthFailure[A](request: AuthenticatedRequest[A]): Future[Result] = {
    def toContent(maybeSubject: Option[Subject]): (Boolean, HtmlFormat.Appendable) =
      maybeSubject.map(subject => subject.asInstanceOf[User])
      .map(user => (true, denied(Some(user))))
      .getOrElse {(false, views.html.security.logIn(LogInForm.logInForm))}

    getSubject(request).map(maybeSubject => toContent(maybeSubject))
    .map(subjectPresentAndContent =>
      if (subjectPresentAndContent._1) Results.Forbidden(subjectPresentAndContent._2)
      else Results.Unauthorized(subjectPresentAndContent._2))
  }
}

现在,转到数据库的需求已减少到尚未将主题放入请求的情况。请注意有关替换request.session.get("userId")的注释,但要识别用户。

AuthSupport类提供对主题持久性的访问。这将DB访问与DeadboltHandler隔离开来。它非常简单,主要是因为您将使用Slick查询填充此内容。

@Singleton
class AuthSupport @Inject()(dbConfigProvider: DatabaseConfigProvider) {
    // set up your usual Slick support

    // use Slick to get the subject from the database
    def getUser(userId: String): Option[User] = ???
}

要公开此功能,您需要创建一个模块并在application.conf中注册。

import be.objectify.deadbolt.scala.DeadboltHandler
import be.objectify.deadbolt.scala.cache.HandlerCache
import security.{AuthSupport, MyDeadboltHandler, MyHandlerCache}
import play.api.inject.{Binding, Module}
import play.api.{Configuration, Environment}

class CustomBindings extends Module  {
  override def bindings(environment: Environment,
                        configuration: Configuration): Seq[Binding[_]] =
    Seq(
         bind[DeadboltHandler].to[MyDeadboltHandler],
         bind[AuthSupport].toSelf,
         // other bindings, such as HandlerCache
       )
}

application.conf中声明它是使用play.modules.enabled的常见问题:

play {
  modules {
    enabled += be.objectify.deadbolt.scala.DeadboltModule
    enabled += modules.CustomBindings
  }
}