经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Scala » 查看文章
Akka-CQRS(14)- Http标准安全解决方案:OAuth2-资源使用授权
来源:cnblogs  作者:雪川大虫  时间:2019/7/5 9:16:44  对本文有异议

   上一篇讨论了SSL/TLS安全连接,主要是一套在通信层面的数据加密解决方案。但我们更需要一套方案来验证客户端。要把不能通过验证的网络请求过滤掉。

OAuth2是一套行业标准的网络资源使用授权协议,也就是为用户提供一种授权凭证,用户凭授权凭证来使用网络资源。申请凭证、然后使用凭证进行网络操作流程如下:

 

??

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

实际上OAuth2是一套3方授权模式,但我们只需要资源管理方授权,所以划去了1、2两个步骤。剩下的两个步骤,包括:申请令牌,使用令牌,这些在官方文件中有详细描述。用户身份和令牌的传递是通过Http Header实现的,具体情况可参考RFC2617,RFC6750

简单来说:用户向服务器提交身份信息申请令牌,下面是一个HttpRequest样例:

  1. POST /token HTTP/1.1
  2. Host: server.example.com
  3. Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
  4. Content-Type: application/x-www-form-urlencoded

上面Basic后面一串代码就是 user+password的加密文,它的产生方法示范如下:

  1. final case class BasicHttpCredentials(username: String, password: String) extends jm.headers.BasicHttpCredentials {
  2. val cookie = {
  3. val userPass = username + ':' + password
  4. val bytes = userPass.getBytes(`UTF-8`.nioCharset)
  5. Base64.rfc2045.encodeToChar(bytes, false)
  6. }
  7. def render[R <: Rendering](r: R): r.type = r ~~ "Basic " ~~ cookie
  8. override def scheme: String = "Basic"
  9. override def token: String = String.valueOf(cookie)
  10. override def params: Map[String, String] = Map.empty
  11. }

注:在OAuth2版本中如果使用https://,则容许明文用户和密码。

服务端在返回的HttpResponse中返回令牌access_token:

  1. {"access_token":"2e510027-0eb9-4367-b310-68e1bab9dc3d", "token_type":"bearer", "expires_in":3600}

注意:这个expires_in是应用系统自定义内部使用的参数,也就是说应用系统必须自备令牌过期失效处理机制。

得到令牌后每个使用网络资源的Request都必须在Authorization类Header里附带这个令牌,如:

  1. GET /resource HTTP/1.1
  2. Host: server.example.com
  3. Authorization: Bearer 2e510027-0eb9-4367-b310-68e1bab9dc3d

Bearer后就是服务端返回的令牌值。我们还是设计一个例子来示范整个授权使用过程。先看看下面一些基本操作代码:

  1. object JsonMarshaller extends SprayJsonSupport with DefaultJsonProtocol {
  2. case class UserInfo(username: String, password: String)
  3. case class AuthToken(access_token: String = java.util.UUID.randomUUID().toString,
  4. token_type: String = "bearer",
  5. expires_in: Int = 3600)
  6. case class AuthUser(credentials: UserInfo,
  7. token: AuthToken = new AuthToken(expires_in = 60 * 60 * 8),
  8. loggedInAt: String = LocalDateTime.now().toString)
  9. val validUsers = Seq(UserInfo("johnny", "p4ssw0rd"),UserInfo("tiger", "secret"))
  10. val loggedInUsers = mutable.ArrayBuffer.empty[AuthUser]
  11. def getValidUser(credentials: Credentials): Option[UserInfo] =
  12. credentials match {
  13. case p @ Credentials.Provided(_) =>
  14. validUsers.find(user => user.username == p.identifier && p.verify(user.password))
  15. case _ => None
  16. }
  17. def authenticateUser(credentials: Credentials): Option[AuthUser] =
  18. credentials match {
  19. case p @ Credentials.Provided(_) =>
  20. loggedInUsers.find(user => p.verify(user.token.access_token))
  21. case _ => None
  22. }
  23. implicit val fmtCredentials = jsonFormat2(UserInfo.apply)
  24. implicit val fmtToken = jsonFormat3(AuthToken.apply)
  25. implicit val fmtUser = jsonFormat3(AuthUser.apply)
  26. }

validUers: Seq[UserInfo] 模拟是个在服务端数据库里的用户登记表,loggedInUsers是一个已经通过验证的用户请单。函数 getValidUser(credentials: Credentials) 用传人参数Credentials来获取用户信息Option[UserInfo]。Credentials是这样定义的:

  1. object Credentials {
  2. case object Missing extends Credentials
  3. abstract case class Provided(identifier: String) extends Credentials {
  4. /**
  5. * First applies the passed in `hasher` function to the received secret part of the Credentials
  6. * and then safely compares the passed in `secret` with the hashed received secret.
  7. * This method can be used if the secret is not stored in plain text.
  8. * Use of this method instead of manual String equality testing is recommended in order to guard against timing attacks.
  9. *
  10. * See also [[EnhancedString#secure_==]], for more information.
  11. */
  12. def verify(secret: String, hasher: String ? String): Boolean
  13. /**
  14. * Safely compares the passed in `secret` with the received secret part of the Credentials.
  15. * Use of this method instead of manual String equality testing is recommended in order to guard against timing attacks.
  16. *
  17. * See also [[EnhancedString#secure_==]], for more information.
  18. */
  19. def verify(secret: String): Boolean = verify(secret, x ? x)
  20. }
  21. def apply(cred: Option[HttpCredentials]): Credentials = {
  22. cred match {
  23. case Some(BasicHttpCredentials(username, receivedSecret)) ?
  24. new Credentials.Provided(username) {
  25. def verify(secret: String, hasher: String ? String): Boolean = secret secure_== hasher(receivedSecret)
  26. }
  27. case Some(OAuth2BearerToken(token)) ?
  28. new Credentials.Provided(token) {
  29. def verify(secret: String, hasher: String ? String): Boolean = secret secure_== hasher(token)
  30. }
  31. case Some(GenericHttpCredentials(scheme, token, params)) ?
  32. throw new UnsupportedOperationException("cannot verify generic HTTP credentials")
  33. case None ? Credentials.Missing
  34. }
  35. }
  36. }

在apply函数里定义了verify函数功能。这个时候Credentials的实际类型是BasicHttpCredentials。另一个函数authenticateUser(credentials: Credentials)是用Crentials来验证令牌的,那么它的类型应该是OAuth2BearerToken了,具体验证令牌的过程是从loggedInUser清单里对比找出拥有相同令牌的用户。这就意味着每次一个用户通过验证获取令牌后服务端必须把用户信息和令牌值保存起来方便以后对比。我们再来看看route的定义:

  1. val route =
  2. pathEndOrSingleSlash {
  3. get {
  4. complete("Welcome!")
  5. }
  6. } ~
  7. path("auth") {
  8. authenticateBasic(realm = "auth", getValidUser) { user =>
  9. post {
  10. val loggedInUser = AuthUser(user)
  11. loggedInUsers.append(loggedInUser)
  12. complete(loggedInUser.token)
  13. }
  14. }
  15. } ~
  16. path("api") {
  17. authenticateOAuth2(realm = "api", authenticateUser) { validToken =>
  18. complete(s"It worked! user = $validToken")
  19. }
  20. }

现在这段代码就比较容易理解了:authenticateBasic(realm = "auth", getValidUser) {user => ...} 用上了自定义的geValidUser来产生user对象。而authenticateOAuth2(realm = "api", authenticateUser) { validToken =>...}则用了自定义的authenticateUser函数来验证令牌。

下面我们写一段客户端代码来测试上面这个webserver的功能:

  1. import akka.actor._
  2. import akka.stream._
  3. import akka.http.scaladsl.Http
  4. import akka.http.scaladsl.model.headers._
  5. import scala.concurrent._
  6. import akka.http.scaladsl.model._
  7. import org.json4s._
  8. import org.json4s.jackson.JsonMethods._
  9. import scala.concurrent.duration._
  10. object Oauth2Client {
  11. def main(args: Array[String]): Unit = {
  12. implicit val system = ActorSystem()
  13. implicit val materializer = ActorMaterializer()
  14. // needed for the future flatMap/onComplete in the end
  15. implicit val executionContext = system.dispatcher
  16. val helloRequest = HttpRequest(uri = "http://192.168.11.189:50081/")
  17. val authorization = headers.Authorization(BasicHttpCredentials("johnny", "p4ssw0rd"))
  18. val authRequest = HttpRequest(
  19. HttpMethods.POST,
  20. uri = "http://192.168.11.189:50081/auth",
  21. headers = List(authorization)
  22. )
  23. val futToken: Future[HttpResponse] = Http().singleRequest(authRequest)
  24. val respToken = for {
  25. resp <- futToken
  26. jstr <- resp.entity.dataBytes.runFold("") {(s,b) => s + b.utf8String}
  27. } yield jstr
  28. val jstr = Await.result[String](respToken,2 seconds)
  29. println(jstr)
  30. val token = (parse(jstr).asInstanceOf[JObject] \ "access_token").values
  31. println(token)
  32. val authentication = headers.Authorization(OAuth2BearerToken(token.toString))
  33. val apiRequest = HttpRequest(
  34. HttpMethods.POST,
  35. uri = "http://192.168.11.189:50081/api",
  36. ).addHeader(authentication)
  37. val futAuth: Future[HttpResponse] = Http().singleRequest(apiRequest)
  38. println(Await.result(futAuth,2 seconds))
  39. scala.io.StdIn.readLine()
  40. system.terminate()
  41. }
  42. }

测试显示结果如下:

  1. {"access_token":"6280dcd7-71fe-4203-8163-8ac7dbd5450b","expires_in":28800,"token_type":"bearer"}
  2. 6280dcd7-71fe-4203-8163-8ac7dbd5450b
  3. HttpResponse(200 OK,List(Server: akka-http/10.1.8, Date: Wed, 03 Jul 2019 09:32:32 GMT),HttpEntity.Strict(text/plain; charset=UTF-8,It worked! user = AuthUser(UserInfo(johnny,p4ssw0rd),AuthToken(6280dcd7-71fe-4203-8163-8ac7dbd5450b,bearer,28800),2019-07-03T17:32:32.627)),HttpProtocol(HTTP/1.1))

下面是服务端源代码:

build.sbt

 

  1. name := "oauth2"
  2. version := "0.1"
  3. scalaVersion := "2.12.8"
  4. libraryDependencies ++= Seq(
  5. "com.typesafe.akka" %% "akka-http" % "10.1.8",
  6. "com.typesafe.akka" %% "akka-stream" % "2.5.23",
  7. "com.pauldijou" %% "jwt-core" % "3.0.1",
  8. "de.heikoseeberger" %% "akka-http-json4s" % "1.22.0",
  9. "org.json4s" %% "json4s-native" % "3.6.1",
  10. "com.typesafe.akka" %% "akka-http-spray-json" % "10.1.8",
  11. "com.typesafe.scala-logging" %% "scala-logging" % "3.9.0",
  12. "org.slf4j" % "slf4j-simple" % "1.7.25",
  13. "org.json4s" %% "json4s-jackson" % "3.6.7"
  14. )

 

OAuth2Server.scala

  1. import akka.actor._
  2. import akka.stream._
  3. import akka.http.scaladsl.Http
  4. import akka.http.scaladsl.server.Directives._
  5. import akka.http.scaladsl.server.directives.Credentials
  6. import java.time.LocalDateTime
  7. import scala.collection.mutable
  8. import akka.http.scaladsl.marshallers.sprayjson._
  9. import spray.json._
  10. object JsonMarshaller extends SprayJsonSupport with DefaultJsonProtocol {
  11. case class UserInfo(username: String, password: String)
  12. case class AuthToken(access_token: String = java.util.UUID.randomUUID().toString,
  13. token_type: String = "bearer",
  14. expires_in: Int = 3600)
  15. case class AuthUser(credentials: UserInfo,
  16. token: AuthToken = new AuthToken(expires_in = 60 * 60 * 8),
  17. loggedInAt: String = LocalDateTime.now().toString)
  18. val validUsers = Seq(UserInfo("johnny", "p4ssw0rd"),UserInfo("tiger", "secret"))
  19. val loggedInUsers = mutable.ArrayBuffer.empty[AuthUser]
  20. def getValidUser(credentials: Credentials): Option[UserInfo] =
  21. credentials match {
  22. case p @ Credentials.Provided(_) =>
  23. validUsers.find(user => user.username == p.identifier && p.verify(user.password))
  24. case _ => None
  25. }
  26. def authenticateUser(credentials: Credentials): Option[AuthUser] =
  27. credentials match {
  28. case p @ Credentials.Provided(_) =>
  29. loggedInUsers.find(user => p.verify(user.token.access_token))
  30. case _ => None
  31. }
  32. implicit val fmtCredentials = jsonFormat2(UserInfo.apply)
  33. implicit val fmtToken = jsonFormat3(AuthToken.apply)
  34. implicit val fmtUser = jsonFormat3(AuthUser.apply)
  35. }
  36. object Oauth2ServerDemo extends App {
  37. implicit val httpSys = ActorSystem("httpSystem")
  38. implicit val httpMat = ActorMaterializer()
  39. implicit val httpEC = httpSys.dispatcher
  40. import JsonMarshaller._
  41. val route =
  42. pathEndOrSingleSlash {
  43. get {
  44. complete("Welcome!")
  45. }
  46. } ~
  47. path("auth") {
  48. authenticateBasic(realm = "auth", getValidUser) { user =>
  49. post {
  50. val loggedInUser = AuthUser(user)
  51. loggedInUsers.append(loggedInUser)
  52. complete(loggedInUser.token)
  53. }
  54. }
  55. } ~
  56. path("api") {
  57. authenticateOAuth2(realm = "api", authenticateUser) { validToken =>
  58. complete(s"It worked! user = $validToken")
  59. }
  60. }
  61. val (port, host) = (50081,"192.168.11.189")
  62. val bindingFuture = Http().bindAndHandle(route,host,port)
  63. println(s"Server running at $host $port. Press any key to exit ...")
  64. scala.io.StdIn.readLine()
  65. bindingFuture.flatMap(_.unbind())
  66. .onComplete(_ => httpSys.terminate())
  67. }

 

原文链接:http://www.cnblogs.com/tiger-xc/p/11132207.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号