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

使用 Play Framework 和 Scala 管理用户身份验证(4)用户管理和身份验证

使用 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                复选框,代码会创建具有更长生存期的副本来修改该身份验证器。(为简单起见,我在清单中省略了这些细节。)
返回列表