• Akka-CQRS(15)- Http标准安全解决方案:OAuth2+JWT


      上期讨论过OAuth2, 是一种身份认证+资源授权使用模式。通过身份认证后发放授权凭证。用户凭授权凭证调用资源。这个凭证就是一种令牌,基本上是一段没什么意义的加密文,或者理解成密钥也可以。服务方通过这个令牌来获取用户身份信息,也就是说服务端必须维护一个已经获得身份验证的用户信息清单。研究了一下JWT,发现它本身可以携带加密后的一些信息包括用户信息,而这些信息又可以通过同样的加密算法解密恢复。也就是说服务端是可以直接对收到的JWT解密恢复用户信息,这样用起来就方便多了。还记着我们的POS例子里客户端必须构建一个指令,如:http://www.pos.com/logIn?shopid=1001&userid=234 这个Uri里的shopid是明码的,会造成很大安全风险。使用JWT后,我们可以把shopid,单号什么的都放在JWT里就安全多了。

    先了解一下JWT:JWT也是一个行业标准:RFC7519,是一个用Json格式传递加密信息的方式。JWT的结构如下:

    header.payload.signiture 如:hhhhh.ppppp.ssssss

    header:由两部分组成:1、令牌类型,在这里是JWT, 2、签名算法如 HMAC SHA256 or RSA, 下面是个header例子:

    {
      "alg": "HS256",
      "typ": "JWT"
    }

    payload:可以用来承载用户自定义信息,如userid, shopid, vchnum ...

    {
      "shopid": "1101",
      "userid": "102",
      "vchnum": 12
    }

    signiture: 就是把 加密后的header+加密后的payload+secret 用header提供的签名算法签名,如下:

    HMACSHA256(
      base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      secret)

    我的目标是把一些用来辨识用户、权限以及状态信息加密存在JWT内发送给用户,用户在请求中提交他的JWT,服务端再解密并取出内部信息然后确定如何处理用户请求。

    JWT本身原理并不复杂,应用场景也不是很多,所以不想花太多精力研究它。刚好,找到一个开源的scala JWT工具库jwt-scala. 下面就利用项目源代码来了解一下JWT的操作,包括:加密、解密、验证、获取payload内部claims值。

    JWT encode 方法如下:

      /** Encode a JSON Web Token from its different parts. Both the header and the claim will be encoded to Base64 url-safe, then a signature will be eventually generated from it if you did pass a key and an algorithm, and finally, those three parts will be merged as a single string, using dots as separator.
        *
        * @return $token
        * @param header $headerString
        * @param claim $claimString
        * @param key $key
        * @param algorithm $algo
        */
      def encode(header: String, claim: String, key: String, algorithm: JwtAlgorithm): String = {
        val data = JwtBase64.encodeString(header) + "." + JwtBase64.encodeString(claim)
        data + "." + JwtBase64.encodeString(JwtUtils.sign(data, key, algorithm))
      }

    所以产生JWT的元素都在参数里了。我们可以直接用payload.claims来构建JWT:

     /** An alias to `encode` which will provide an automatically generated header.
        *
        * @return $token
        * @param claim $claimString
        */
      def encode(claim: String): String = encode(JwtHeader().toJson, claim)
    
      /** An alias to `encode` which will provide an automatically generated header and setting both key and algorithm
        * to None.
        *
        * @return $token
        * @param claim the claim of the JSON Web Token
        */
      def encode(claim: JwtClaim): String = encode(claim.toJson)
    
     def encode(header: String, claim: String): String = {
        JwtBase64.encodeString(header) + "." + JwtBase64.encodeString(claim) + "."
      }

    这样看一个正确的JWT可以没有签名那部分的:hhhhh.ppppp。想想还是要用签名,安全点。用下面这个函数就可以了:

      /** An alias to `encode` which will provide an automatically generated header and allowing you to get rid of Option
        * for the key and the algorithm.
        *
        * @return $token
        * @param claim $claimString
        * @param key $key
        * @param algorithm $algo
        */
      def encode(claim: String, key: String, algorithm: JwtAlgorithm): String =
        encode(JwtHeader(algorithm).toJson, claim, key, algorithm)
    
    /** Deserialize an algorithm from its string equivalent. Only real algorithms supported,
        * if you need to support "none", use "optionFromString".
        *
        * @return the actual instance of the algorithm
        * @param algo the name of the algorithm (e.g. HS256 or HmacSHA256)
        * @throws JwtNonSupportedAlgorithm in case the string doesn't match any known algorithm
        */
      def fromString(algo: String): JwtAlgorithm = algo match {
        case "HMD5"        => HMD5
        case "HS224"       => HS224
        case "HS256"       => HS256
        case "HS384"       => HS384
        case "HS512"       => HS512
        case "RS256"       => RS256
        case "RS384"       => RS384
        case "RS512"       => RS512
        case "ES256"       => ES256
        case "ES384"       => ES384
        case "ES512"       => ES512
        case _             => throw new JwtNonSupportedAlgorithm(algo)
        // Missing PS256 PS384 PS512
      }

    key可以是任意字符串。

    JWT decode 代码如下:

      /** Will try to decode a JSON Web Token to raw strings using a HMAC algorithm
        *
        * @return if successful, a tuple of 3 strings, the header, the claim and the signature
        * @param token $token
        * @param key $key
        * @param algorithms $algos
        */
      def decodeRawAll(token: String, key: String, algorithms: Seq[JwtHmacAlgorithm], options: JwtOptions): Try[(String, String, String)] = Try {
        val (header64, header, claim64, claim, signature) = splitToken(token)
        validate(header64, parseHeader(header), claim64, parseClaim(claim), signature, key, algorithms, options)
        (header, claim, signature)
      }
    
      def decodeRawAll(token: String, key: String, algorithms: Seq[JwtHmacAlgorithm]): Try[(String, String, String)] =
        decodeRawAll(token, key, algorithms, JwtOptions.DEFAULT)

    另外,验证JWT方法如下:

      /** An alias for `isValid` if you want to directly pass a string as the key for HMAC algorithms
        *
        * @return a boolean value indicating if the token is valid or not
        * @param token $token
        * @param key $key
        * @param algorithms $algos
        */
      def isValid(token: String, key: String, algorithms: Seq[JwtHmacAlgorithm], options: JwtOptions): Boolean =
        try {
          validate(token, key, algorithms, options)
          true
        } catch {
          case _ : Throwable => false
        }
    
      def isValid(token: String, key: String, algorithms: Seq[JwtHmacAlgorithm]): Boolean = isValid(token, key, algorithms, JwtOptions.DEFAULT)

    下面是一段示范代码:

    import pdi.jwt._
    import org.json4s._
    import org.json4s.jackson.JsonMethods._
    
    object JwtDemo extends App{
    
      import scala.util._
    
      var clms = JwtClaim() ++ ("shopid" -> "1101") ++ ("userid" -> "102") ++ ("vchnum" -> 23)
      val token = Jwt.encode(clms,"OpenSesame", JwtAlgorithm.HS256)
      println(token)
      println(Jwt.isValid(token,"OpenSesame",Seq(JwtAlgorithm.HS256)))
      val claims = Jwt.decodeRawAll(token,"OpenSesame",Seq(JwtAlgorithm.HS256))
      println(claims)
    
      claims match {
        case Success(json) => println(((parse(json._2).asInstanceOf[JObject])  "shopid").values)
        case Failure(err) => println(s"Error: ${err.getMessage}")
      }
    
    }

    现在我们把上次的OAuth2示范代码改改,用JWT替换access_token:

    import akka.actor._
    import akka.stream._
    import akka.http.scaladsl.Http
    import akka.http.scaladsl.server.Directives._
    import akka.http.scaladsl.server.directives.Credentials
    import pdi.jwt._
    import org.json4s._
    import org.json4s.jackson.JsonMethods._
    import scala.util._
    
    //import akka.http.scaladsl.marshallers.sprayjson._
    //import spray.json._
    
    object JsonMarshaller { // extends  SprayJsonSupport with DefaultJsonProtocol {
    
      case class UserInfo(username: String, password: String, appInfo: (String,String))
    
      /* 用JWT替代
      case class AuthToken(access_token: String = java.util.UUID.randomUUID().toString,
                            token_type: String = "bearer",
                            expires_in: Int = 3600)
      */
      /* 无需维护这个验证后用户清单了
      case class AuthUser(credentials: UserInfo,
                          token: AuthToken = new AuthToken(expires_in = 60 * 60 * 8),
                              loggedInAt: String = LocalDateTime.now().toString)
    
       val loggedInUsers = mutable.ArrayBuffer.empty[AuthUser]
    
       */
    
    
      val validUsers = Seq(UserInfo("johnny", "p4ssw0rd",("1101", "101"))
                           ,UserInfo("tiger", "secret", ("1101" , "102")))
    
    
      def getValidUser(credentials: Credentials): Option[UserInfo] =
        credentials match {
          case p @ Credentials.Provided(_) =>
            validUsers.find(user => user.username == p.identifier && p.verify(user.password))
          case _ => None
        }
       /*收到的是JWT
       def authenticateUser(credentials: Credentials): Option[(String,String)] =
        credentials match {
          case p @ Credentials.Provided(_) =>
            loggedInUsers.find(user => p.verify(user.token.access_token))
          case _ => None
        } */
    
      def authenticateJwt(credentials: Credentials): Option[String] =
        credentials match {
          case Credentials.Provided(token) =>
            Jwt.isValid(token,"OpenSesame",Seq(JwtAlgorithm.HS256)) match {
              case true => Some(token)
              case _ => None
            }
          case _ => None
        }
    /*
      implicit val fmtCredentials = jsonFormat2(UserInfo.apply)
      implicit val fmtToken = jsonFormat3(AuthToken.apply)
      implicit val fmtUser = jsonFormat3(AuthUser.apply)
    
     */
    }
    
    
    object Oauth2ServerDemo extends App {
    
      implicit val httpSys = ActorSystem("httpSystem")
      implicit val httpMat = ActorMaterializer()
      implicit val httpEC = httpSys.dispatcher
    
    
      import JsonMarshaller._
    
    
      val route =
        pathEndOrSingleSlash {
          get {
            complete("Welcome!")
          }
        } ~
          path("auth") {
            authenticateBasic(realm = "auth", getValidUser) { user =>
              post {
                val claims = JwtClaim() + ("appInfo" , (user.appInfo._1,user.appInfo._2))
                complete(Jwt.encode(claims,"OpenSesame",JwtAlgorithm.HS256))
              }
            }
          } ~
          path("api") {
            authenticateOAuth2(realm = "api", authenticateJwt) { validToken =>
    
              val pi = Jwt.decodeRawAll(validToken,"OpenSesame",Seq(JwtAlgorithm.HS256)) match {
                case Success(parts) => Some(((parse(parts._2).asInstanceOf[JObject])  "appInfo").values.asInstanceOf[Map[String,String]].toList.head)
                case Failure(_) => None
              }
              complete(s"It worked! token = $validToken, appInfo = ${pi}")
            }
          }
    
    
      val (port, host) = (50081,"192.168.11.189")
    
      val bindingFuture = Http().bindAndHandle(route,host,port)
    
      println(s"Server running at $host $port. Press any key to exit ...")
    
      scala.io.StdIn.readLine()
    
    
      bindingFuture.flatMap(_.unbind())
        .onComplete(_ => httpSys.terminate())
    
    
    }

    下面是客户端测试代码:

    import akka.actor._
    import akka.stream._
    import akka.http.scaladsl.Http
    import akka.http.scaladsl.model.headers._
    import scala.concurrent._
    import akka.http.scaladsl.model._
    import pdi.jwt._
    import org.json4s._
    import org.json4s.jackson.JsonMethods._
    import scala.util._
    import scala.concurrent.duration._
    
    object Oauth2Client  {
        def main(args: Array[String]): Unit = {
          implicit val system = ActorSystem()
          implicit val materializer = ActorMaterializer()
          // needed for the future flatMap/onComplete in the end
          implicit val executionContext = system.dispatcher
    
          val helloRequest = HttpRequest(uri = "http://192.168.11.189:50081/")
    
          val authorization = headers.Authorization(BasicHttpCredentials("johnny", "p4ssw0rd"))
          val authRequest = HttpRequest(
            HttpMethods.POST,
            uri = "http://192.168.11.189:50081/auth",
            headers = List(authorization)
          )
    
          val futToken: Future[HttpResponse] = Http().singleRequest(authRequest)
    
          val respToken = for {
            resp <- futToken
            jstr <- resp.entity.dataBytes.runFold("") {(s,b) => s + b.utf8String}
          } yield jstr
    
          val jstr =  Await.result[String](respToken,2 seconds)
          println(jstr)
    
          scala.io.StdIn.readLine()
    
          val parts = Jwt.decodeRawAll(jstr, "OpenSesame", Seq(JwtAlgorithm.HS256)) match {
            case Failure(exception) => println(s"Error: ${exception.getMessage}")
            case Success(value) =>
              val tt: (String,String) = ((parse(value._2).asInstanceOf[JObject])  "appInfo").values.asInstanceOf[Map[String,String]].toList.head
              println(tt)
          }
    
          scala.io.StdIn.readLine()
    
          val authentication = headers.Authorization(OAuth2BearerToken(jstr))
          val apiRequest = HttpRequest(
            HttpMethods.POST,
            uri = "http://192.168.11.189:50081/api",
          ).addHeader(authentication)
    
          val futAuth: Future[HttpResponse] = Http().singleRequest(apiRequest)
    
          println(Await.result(futAuth,2 seconds))
    
    
          scala.io.StdIn.readLine()
          system.terminate()
        }
    
      }

    运行后输出结果:

    eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhcHBJbmZvIjp7IjExMDEiOiIxMDEifX0.i46FUinT0n1brYGInFZz-6embOj15SKpIpO9QHkpSZs
    
    (1101,101)
    
    HttpResponse(200 OK,List(Server: akka-http/10.1.8, Date: Tue, 09 Jul 2019 04:02:12 GMT),HttpEntity.Strict(text/plain; charset=UTF-8,It worked! token = eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhcHBJbmZvIjp7IjExMDEiOiIxMDEifX0.i46FUinT0n1brYGInFZz-6embOj15SKpIpO9QHkpSZs, appInfo = Some((1101,101))),HttpProtocol(HTTP/1.1))
    
    Process finished with exit code 130 (interrupted by signal 2: SIGINT)

    构建环境 build.sbt:

    name := "oauth2"
    
    version := "0.1"
    
    scalaVersion := "2.12.8"
    
    libraryDependencies ++= Seq(
      "com.typesafe.akka" %% "akka-http"   % "10.1.8",
      "com.typesafe.akka" %% "akka-stream" % "2.5.23",
      "com.pauldijou" %% "jwt-core" % "3.0.1",
      "de.heikoseeberger" %% "akka-http-json4s" % "1.22.0",
      "org.json4s" %% "json4s-native" % "3.6.1",
      "com.typesafe.akka" %% "akka-http-spray-json" % "10.1.8",
      "com.typesafe.scala-logging" %% "scala-logging" % "3.9.0",
      "org.slf4j" % "slf4j-simple" % "1.7.25",
      "org.json4s" %% "json4s-jackson" % "3.6.7"
    )
  • 相关阅读:
    Android Studio:xxx is not an enclosing class 错误的解决方法
    SpringMVC验证框架Validation特殊用法
    在Spring MVC中使用注解的方式校验RequestParams
    Spring4新特性——集成Bean Validation 1.1(JSR-349)到SpringMVC
    Bean Validation 技术规范特性概述
    JSR 303
    SpringMVC学习
    javax.validation.UnexpectedTypeException: No validator could be found for constraint 'org.hibernate.validator.constraints.Length' validating type
    SpringAOP拦截Controller,Service实现日志管理(自定义注解的方式)
    Spring AspectJ切入点语法详解
  • 原文地址:https://www.cnblogs.com/tiger-xc/p/11156527.html
Copyright © 2020-2023  润新知