使用 Play Framework 和 Scala 管理用户身份验证(2)建模用户和身份概况
- UID
- 1066743
|
使用 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 中找到它。 |
|
|
|
|
|