首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

使用 Play Framework 和 Scala 管理用户身份验证(2)建模用户和身份概况

使用 Play Framework 和 Scala 管理用户身份验证(2)建模用户和身份概况

建模用户和身份概况 该应用程序实现了帐户链接,所以用户与来自不同身份验证提供程序(在本应用程序中,包括凭据或 Twitter OAuth1)的多个身份概况相关联。我通过                Profile 类表示身份概况。一个 User 包含一个身份概况列表。 清单 4 显示了 app/models/User.scala 中的相关代码。
清单 4. 用户模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
case class Profile(
  loginInfooginInfo,
  confirmed: Boolean,
  email:Option[String],
  firstName: Option[String],
  lastName: Option[String],
  fullName: Option[String],
  passwordInfo:Option[PasswordInfo],
  oauth1Info: Option[OAuth1Info],
  avatarUrl: Option[String])

case class User(id: UUID, profiles: List[Profile]) extends Identity {
  def profileFor(loginInfooginInfo) = profiles.find(_.loginInfo == loginInfo)
  def fullName(loginInfooginInfo) = profileFor(loginInfo).flatMap(_.fullName)
}

object User {
  implicit val passwordInfoJsonFormat = Json.format[PasswordInfo]
  implicit val oauth1InfoJsonFormat = Json.format[OAuth1Info]
  implicit val profileJsonFormat = Json.format[Profile]
  implicit val userJsonFormat = Json.format[User]
}




                身份概况(以及用户)由                Silhouette 的 LoginInfo 类唯一标识 — 基本来讲是一个(用户 ID、提供商                ID)元组。一个概况可能已确认或正在等待确认。此特性对与凭据提供商关联的概况很方便,这些概况必须在注册流程的最后一步确认。概况也包含一些基本的身份信息(电子邮件地址、用户名和头像                URL),所有这些信息都是可选的,因为身份信息因提供商而异。一个与凭据提供商关联的概况存储一个 Silhouette                PasswordInfo 对象,该对象持有经过哈希运算的密码。OAuth1 Twitter 提供程序创建的概况在一个                Silhouette OAuth1Info                实例中存储身份验证令牌和机密数据。要支持其他身份验证提供程序,Profile 类必须使用额外的字段来扩展(例如一个针对                OAuth2 的 oauth2Info:OAuth2Info 属性)。
User 类是一个概况列表的包装器,它为与一个给定 LoginInfo                关联的概况和全名提供了两个便捷访问器。User 配套对象声明模型类与 JSON 之间的自动转换 —                这很有必要,因为 MongoDB 驱动程序适用于 JSON 对象。
模型持久性 该应用程序将持久性代码封装在 User、PasswordInfo 和                OAuth1Info 类的数据访问对象 (DAO) 中。在 app/daos/UserDao.scala 中,您将找到                UserDao 特征,如 清单 5 所示。
清单 5. UserDao                特征
1
2
3
4
5
6
7
8
trait UserDao {
  def save(user:User):Future[User]
  def find(loginInfooginInfo):Future[Option[User]]
  def find(userId:UUID):Future[Option[User]]
  def confirm(loginInfooginInfo):Future[User]
  def link(user:User, profilerofile):Future[User]
  def update(profilerofile):Future[User]
}




可按 ID 或 LoginInfo 持久化和查询用户。DAO                也实现了确认一个身份概况、将一个新身份概况链接到一个用户以及更新一个身份概况的操作。请注意 DAO 的异步性质:所有这些操作都返回一个                Future 实例,这是 Scala 建模最终将完成的计算的标准类。另外在 app/daos/UserDao.scala                中,您可以找到 UserDao 特征的 MongoDB 实现,如 清单 6 所示。
清单 6. MongoUserDao                    类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class MongoUserDao extends UserDao {
  lazy val reactiveMongoApi = current.injector.instanceOf[ReactiveMongoApi]
  val users = reactiveMongoApi.db.collection[JSONCollection]("users")

  def find(loginInfooginInfo):Future[Option[User]] =
    users.find(Json.obj("profiles.loginInfo" -> loginInfo)).one[User]

  def find(userId:UUID):Future[Option[User]] =
    users.find(Json.obj("id" -> userId)).one[User]
   
  def save(user:User):Future[User] =
    users.insert(user).map(_ => user)

  def confirm(loginInfooginInfo):Future[User] = for {
    _ <- users.update(Json.obj(
      "profiles.loginInfo" -> loginInfo
    ), Json.obj("$set" -> Json.obj("profiles.$.confirmed" -> true)))
    user <- find(loginInfo)
  } yield user.get

  def link(user:User, profilerofile) = for {
    _ <- users.update(Json.obj(
      "id" -> user.id
    ), Json.obj("$push" -> Json.obj("profiles" -> profile)))
    user <- find(user.id)
  } yield user.get

  def update(profilerofile) = for {
    _ <- users.update(Json.obj(
      "profiles.loginInfo" -> profile.loginInfo
    ), Json.obj("$set" -> Json.obj("profiles.$" -> profile)))
    user <- find(profile.loginInfo)
  } yield user.get
}




MongoUserDao 类通过 Play 的依赖注入器获取反应式 Mongo API 的                hook,并获取存储用户的集合的引用。从这里,该类使用   对 Play 的 JSON 对象执行操作。Silhouette 还需要                PasswordInfo 和 OAuth1Info 类的 DAO。它们的实现类似于                MongoUserDao 类。您可在 app/daos/PasswordInfoDao.scala 和                app/daos/OAuth1InfoDao.scala 中找到这些 DAO 的完整源代码。
测试 DAO                持久性代码是身份验证机制的基础,所以在继续之前确保它能正确地运行是个不错的主意。Play                提供了帮助器和存根,简化了测试的编写。为了测试持久性代码,我将使用 Play 的 FakeApplication                类。这个类将使用与实际应用程序相同的配置来运行,除了 mongodb.uri 属性,该属性指向一个测试数据库。 清单 7 显示了该代码,它位于 test/daos/DaoSpecResources.scala 中。
清单 7. 创建一个虚假的测试应用程序
1
2
3
4
5
6
7
8
def fakeApp = FakeApplication(additionalConfiguration =
     Map("mongodb.uri" -> "mongodb://localhost:27017/test"))

def withUserDao[T](t:MongoUserDao => T):T = running(fakeApp) {
  val userDao = new MongoUserDao
  Await.ready(userDao.users.drop(), timeout)
  t(userDao)
}




声明一个虚假应用程序后,该代码定义一个泛型 withUserDao 方法,该方法接受一个函数,而该函数接受一个                MongoUserDao 并执行实际测试。在清除虚假应用程序的测试数据库中的 users                集合后,该函数在虚假应用程序的上下文内运行。withUserDao 方法可用于运行一套                  测试,比如 test/daos/UserSpecDao.scala 中的那套测试,如 清单 8                所示。
清单 8. specs2                    示例用户 DAO                测试
1
2
3
4
5
6
7
8
9
"UserDao" should {
  "save users and find them by userId" in withUserDao { userDao =>
    val future = for {
      _ <- userDao.save(credentialsTestUser)
      maybeUser <- userDao.find(credentialsTestUser.id)
    } yield maybeUser.map(_ == credentialsTestUser)
    Await.result(future, timeout) must beSome(true)
  }
}




用户服务 Silhouette 需要一个 IdentityService 特征的实现来执行身份验证工作。清单 9 显示了                app/services/UserService.scala 中的该实现(围绕一个注入的 UserDao 的包装器)。
清单 9. 用户服务类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class UserService @Inject() (userDao:UserDao) extends IdentityService[User] {
  def retrieve(loginInfooginInfo) = userDao.find(loginInfo)
  def save(user:User) = userDao.save(user)
  def find(id:UUID) = userDao.find(id)
  def confirm(loginInfooginInfo) = userDao.confirm(loginInfo)
  def link(user:User, socialProfile:CommonSocialProfile) = {
    val profile = toProfile(socialProfile)
    if (user.profiles.exists(_.loginInfo == profile.loginInfo))
      Future.successful(user) else userDao.link(user, profile)
  }

  def save(socialProfile:CommonSocialProfile) = {
    val profile = toProfile(socialProfile)
    userDao.find(profile.loginInfo).flatMap {
      case None => userDao.save(User(UUID.randomUUID(), List(profile)))
      case Some(user) => userDao.update(profile)
    }
  }

  private def toProfile(p:CommonSocialProfile) = Profile(
    loginInfo = p.loginInfo,
    confirmed = true,
    email = p.email,
    firstName = p.firstName,
    lastName = p.lastName,
    fullName = p.fullName,
    passwordInfo = None,
    oauth1Info = None,
    avatarUrl = p.avatarURL
  )
}




save(user:User)                方法在注册流执行期间持久化一个用户。save(p:CommonSocialProfile)                方法处理用户通过社交服务提供商进行身份验证的情形。在此情况下,如果不存在具有指定概况的用户,该应用程序将创建一个新用户;否则,它会更新相应的身份概况。
用户令牌 作为注册和密码重置流的一部分,该应用程序会生成用户令牌。用户令牌通过电子邮件发送给用户,用户必须访问一个基于邮寄令牌 ID 的 URL                才能继续执行该流程。models/UserToken.scala 文件将令牌实现为一个类,该类存留用户和令牌 ID 及过期数据,如 清单 10 所示。
清单 10. 用户令牌
1
2
3
4
5
6
7
8
9
10
case class UserToken(id:UUID, userId:UUID, email:String, expirationTimeateTime, isSignUp:Boolean) {
  def isExpired = expirationTime.isBeforeNow
}

object UserToken {
  implicit val toJson = Json.format[UserToken]
   
  def create(userId:UUID, email:String, isSignUp:Boolean) =
    UserToken(UUID.randomUUID(), userId, email, new DateTime().plusHours(12), isSignUp)
}




用户令牌持久化到一个 MongoDB 集合中,所以配套的对象定义了需要的 JSON                格式。从这里,发生的事情都与用户相关。应用程序使用                UserTokenService 类(位于 services/UserTokenService.scala                中)处理令牌。这个服务类包装了一个注入的用户令牌 DAO,如 清单 11 所示。
清单 11. 用户令牌服务
1
2
3
4
5
class UserTokenService @Inject() (userTokenDao:UserTokenDao) {
  def find(id:UUID) = userTokenDao.find(id)
  def save(token:UserToken) = userTokenDao.save(token)
  def remove(id:UUID) = userTokenDao.remove(id)
}




UserTokenDao 是 MongoUserTokenDao                实现的一个特征。UserTokenDao 代码类似于用户 DAO,您可在 daos/UserTokenDao.scala                中找到它。
返回列表