使用 Play Framework 和 Scala 管理用户身份验证(4)用户管理和身份验证
- UID
- 1066743
|
使用 Play Framework 和 Scala 管理用户身份验证(4)用户管理和身份验证
用户管理和身份验证 了解用户模型和 Silhouette 的配置后,您就可以理解身份验证代码了。我们将详细介绍注册和身份验证。本节中的所有代码段都来自 controllers/Auth.scala 文件中的 Auth 控制器。Auth 控制器与 “ ” 一节中介绍的所有 Silhouette 组件交互,还会与用户和用户令牌服务交互。所有这些组件必须注入到控制器的构造函数中。该控制器实现安全请求处理函数,所以它混合在 Silhouette 控制器特征中(参阅 “ ” 小节)。清单 16 显示了 Auth 代码。
清单 16. Auth 控制器类声明1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class Auth @Inject() (
val messagesApi: MessagesApi,
val env:Environment[User,CookieAuthenticator],
socialProviderRegistry: SocialProviderRegistry,
authInfoRepository: AuthInfoRepository,
credentialsProvider: CredentialsProvider,
userService: UserService,
userTokenService: UserTokenService,
avatarService: AvatarService,
passwordHasher: PasswordHasher,
configuration: Configuration,
mailer: Mailer) extends Silhouette[User,CookieAuthenticator] {
// ... auth controller code ...
}
|
用户注册 注册流从 startSignUp 方法开始。如 清单 17 所示,startSignUp 是一个用户感知的匿名请求处理函数。
清单 17. startSignUp 方法1
2
3
4
5
6
| def startSignUp = UserAwareAction.async { implicit request =>
Future.successful(request.identity match {
case Some(user) => Redirect(routes.Application.index)
case None => Ok(views.html.auth.startSignUp(signUpForm))
})
}
|
如果一个用户与该请求关联,该方法会重定向到索引页面。否则,它将提供注册页面,如 图 1 所示。
图 1. 注册页面 注册页面包含一个要求输入用户电子邮件、姓名和密码(出于验证用途,需要输入两次)的表单。提交时,该表单会由 handleStartSignUp 方法处理,如 清单 18 所示。
清单 18. handleStartSignUp 方法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
| def handleStartSignUp = Action.async { implicit request =>
signUpForm.bindFromRequest.fold(
bogusForm => Future.successful(BadRequest(views.html.auth.startSignUp(bogusForm))),
signUpData => {
val loginInfo = LoginInfo(CredentialsProvider.ID, signUpData.email)
userService.retrieve(loginInfo).flatMap {
case Some(_) =>
Future.successful(Redirect(routes.Auth.startSignUp()).flashing(
"error" -> Messages("error.userExists", signUpData.email)))
case None =>
val profile = Profile(
loginInfo = loginInfo, confirmed=false, email=Some(signUpData.email),
firstName=Some(signUpData.firstName), lastName=Some(signUpData.lastName),
fullName=Some(s"${signUpData.firstName} ${signUpData.lastName}"),
passwordInfo = None, oauth1Info = None, avatarUrl = None)
for {
avatarUrl <- avatarService.retrieveURL(signUpData.email)
user <- userService.save(User(id = UUID.randomUUID(),
profiles = List(profile.copy(avatarUrl = avatarUrl))))
_ <- authInfoRepository.add(loginInfo, passwordHasher.hash(signUpData.password))
token <- userTokenService.save(UserToken.create(user.id, signUpData.email, true))
} yield {
mailer.welcome(profile, link = routes.Auth.signUp(token.id.toString).absoluteURL())
Ok(views.html.auth.finishSignUp(profile))
}
}
}
)
}
|
清单 18 中的代码首先将请求表单绑定到一个 signUpForm 类。如果由于该表单无效(电子邮件地址无效、姓名为空或密码不匹配)而绑定失败,该方法会再次转到注册页面,显示验证错误。否则,该方法首先检查系统是否已有一个使用收到的电子邮件注册的用户。如果是,再次将用户重定向到注册页面并显示一条错误消息。
用户通过所有检查后,该方法使用表单的注册数据实例化一个身份概况,并通过调用 userService.save 来持久化一个具有该概况的用户。然后该方法调用 authInfoRepository.add(它委派给 PasswordInfoDao.save)来持久化凭据并创建一个令牌。在该过程的最后,发送一封包含该令牌 ID 的欢迎电子邮件并重定向到完成注册页面,该页面告诉用户检查收到的电子邮件。该电子邮件链接到 /auth/signup/:token 路由。该路由映射到 signUp 方法,如 清单 19 所示,注册操作到此就完成了。
清单 19. signUp 方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| def signUp(tokenId:String) = Action.async { implicit request =>
val id = UUID.fromString(tokenId)
userTokenService.find(id).flatMap {
case None =>
Future.successful(NotFound(views.html.errors.notFound(request)))
case Some(token) if token.isSignUp && !token.isExpired =>
userService.find(token.userId).flatMap {
case None => Future.failed(new IdentityNotFoundException(Messages("error.noUser")))
case Some(user) =>
val loginInfo = LoginInfo(CredentialsProvider.ID, token.email)
for {
authenticator <- env.authenticatorService.create(loginInfo)
value <- env.authenticatorService.init(authenticator)
_ <- userService.confirm(loginInfo)
_ <- userTokenService.remove(id)
result <- env.authenticatorService.embed(value, Redirect(routes.Application.index()))
} yield result
}
case Some(token) =>
userTokenService.remove(id).map {_ => NotFound(views.html.errors.notFound(request))}
}
}
|
signUp 方法首先确认数据库中存在这个令牌 ID。如果该令牌 ID 不在数据库中,该方法将重定向到应用程序的 not-found 错误页面。然后 signUp 确认该令牌 ID 与一个注册令牌对应,该令牌没有过期,而且与该令牌关联的用户存在。如果所有验证都成功,代码将继续完成注册流程,还会登录该用户。在注册流程的最后,会记录该用户已确认注册并删除注册令牌。登录包含 3 个步骤:
- 调用 env.authenticatorService.create 来创建一个身份验证器(已在 “ ” 一节中介绍,这是一个记录经过验证的用户数据的令牌)
- 初始化身份验证器 (env.authenticatorService.init)
- 将身份验证器嵌入到请求处理函数的响应中,并重定向到索引页面 (env.authenticatorService.embed)
这个序列就是完整的登录流程。这 3 个步骤也包含在身份验证代码中。
通过凭据执行身份验证 异步 authenticate 方法(如 清单 20 所示)实现使用在注册期间定义的凭据验证用户的逻辑。
清单 20. authenticate 方法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
| def authenticate = Action.async { implicit request =>
signInForm.bindFromRequest.fold(
bogusForm => Future.successful(
BadRequest(views.html.auth.signIn(bogusForm, socialProviderRegistry))),
signInData => {
val credentials = Credentials(signInData.email, signInData.password)
credentialsProvider.authenticate(credentials).flatMap { loginInfo =>
userService.retrieve(loginInfo).flatMap {
case None =>
Future.successful(Redirect(routes.Auth.signIn())
.flashing("error" -> Messages("error.noUser")))
case Some(user) if !user.profileFor(loginInfo).map(_.confirmed).getOrElse(false) =>
Future.successful(Redirect(routes.Auth.signIn())
.flashing("error" -> Messages("error.unregistered", signInData.email)))
case Some(_) => for {
authenticator <- env.authenticatorService.create(loginInfo).map {
case authenticator if signInData.rememberMe => authenticator.copy(...) // Extend lifetime
case authenticator => authenticator
}
value <- env.authenticatorService.init(authenticator)
result <- env.authenticatorService.embed(value, Redirect(routes.Application.index()))
} yield result
}
}.recover {
case eroviderException =>
Redirect(routes.Auth.signIn()).flashing("error" -> Messages("error.invalidCredentials"))
}
}
)
}
|
authenticate 方法从登录页面调用,如 图 2 所示。
图 2. 登录页面authenticate 方法的逻辑比看起来更简单。跟平常一样,该方法尝试将请求负载绑定到一个 signInForm(一个包含电子邮件、密码和一个 remember-me 标志的元组)。如果该表单无效,在身份验证的最后会重定向到一个显示了验证错误的登录页面。否则,该方法会尝试调用 credentialsProvider.authenticate 来执行身份验证。如果验证失败,它会返回一个包含异常的 Future,代码会通过返回到包含适当错误消息的登录页面而从异常中恢复。否则,credentialsProvider.authenticate 返回一个包含 LoginInfo 实例的 Future。从这里,代码检查与 LoginInfo 关联的用户是否存在,如果存在,则检查该用户是否完成了注册。如果这些检查通过,代码执行 中列出的 3 个步骤 — 即创建一个身份验证器,初始化它,然后将它嵌入到响应中(重定向到索引页面)。一个中间步骤是,如果选择了 Remember me 复选框,代码会创建具有更长生存期的副本来修改该身份验证器。(为简单起见,我在清单中省略了这些细节。) |
|
|
|
|
|