上期讨论过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": "",
"userid": "",
"vchnum":
}

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" -> "") ++ ("userid" -> "") ++ ("vchnum" -> )
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",("", ""))
,UserInfo("tiger", "secret", ("" , ""))) 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) = (,"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, 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, seconds)) scala.io.StdIn.readLine()
system.terminate()
} }

运行后输出结果:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhcHBJbmZvIjp7IjExMDEiOiIxMDEifX0.i46FUinT0n1brYGInFZz-6embOj15SKpIpO9QHkpSZs

(,)

HttpResponse( OK,List(Server: akka-http/10.1., Date: Tue,  Jul  :: GMT),HttpEntity.Strict(text/plain; charset=UTF-,It worked! token = eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhcHBJbmZvIjp7IjExMDEiOiIxMDEifX0.i46FUinT0n1brYGInFZz-6embOj15SKpIpO9QHkpSZs, appInfo = Some((,))),HttpProtocol(HTTP/1.1))

Process finished with exit code  (interrupted by signal : 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"
)

Akka-CQRS(15)- Http标准安全解决方案:OAuth2+JWT的更多相关文章

  1. PKCS 15 个标准

    PKCS 全称是 Public-Key Cryptography Standards ,是由 RSA 实验室与其它安全系统开发商为促进公钥密码的发展而制订的一系列标准. 可以到官网上看看 What i ...

  2. PKCS 发布的15 个标准与X509

    PKCS 发布的15 个标准,转自:http://falchion.iteye.com/blog/1472453 PKCS 全称是 Public-Key Cryptography Standards ...

  3. SpringCloud微服务实战——搭建企业级开发框架(二十三):Gateway+OAuth2+JWT实现微服务统一认证授权

      OAuth2是一个关于授权的开放标准,核心思路是通过各类认证手段(具体什么手段OAuth2不关心)认证用户身份,并颁发token(令牌),使得第三方应用可以使用该token(令牌)在限定时间.限定 ...

  4. SpringSecurity+Oauth2+Jwt实现toekn认证和刷新token

    简单描述:最近在处理鉴权这一块的东西,需求就是用户登录需要获取token,然后携带token访问接口,token认证成功接口才能返回正确的数据,如果访问接口时候token过期,就采用刷新token刷新 ...

  5. Spring Security + OAuth2 + JWT 基本使用

    Spring Security + OAuth2 + JWT 基本使用 前面学习了 Spring Security 入门,现在搭配 oauth2 + JWT 进行测试. 1.什么是 OAuth2 OA ...

  6. 【Spring Cloud & Alibaba 实战 | 总结篇】Spring Cloud Gateway + Spring Security OAuth2 + JWT 实现微服务统一认证授权和鉴权

    一. 前言 hi,大家好~ 好久没更文了,期间主要致力于项目的功能升级和问题修复中,经过一年时间的打磨,[有来]终于迎来v2.0版本,相较于v1.x版本主要完善了OAuth2认证授权.鉴权的逻辑,结合 ...

  7. Akka-CQRS(14)- Http标准安全解决方案:OAuth2-资源使用授权

    上一篇讨论了SSL/TLS安全连接,主要是一套在通信层面的数据加密解决方案.但我们更需要一套方案来验证客户端.要把不能通过验证的网络请求过滤掉. OAuth2是一套行业标准的网络资源使用授权协议,也就 ...

  8. python基础课程_学习笔记15:标准库:有些收藏夹——fileinput

    标准库:有些收藏夹 fileinput 重要功能 性能 叙述性说明 input([files[,inplace[,backup]]) 便于遍历多个输入流中的行 filename() 返回当前文件的名称 ...

  9. Akka(15): 持久化模式:AtLeastOnceDelivery-消息保证送达模式

    消息保证送达是指消息发送方保证在任何情况下都会至少一次确定的消息送达.AtleastOnceDelivery是一个独立的trait,主要作用是对不确定已送达的消息进行补发,这是一种自动的操作,无需用户 ...

随机推荐

  1. WPF中Polyline拐角的bug

    原文:WPF中Polyline拐角的bug       Polyline绘制折线在小角度(比如几度)的时候会出现不连续的现象,形成拐角的两条线段中有一段会超出,角度越小越明显.       问题如下图 ...

  2. React实现checkbox group多组选项和标签组显示的联动

    实现功能:勾选checkbox项,确定后,已勾选的checkbox项以tag标签的形式展示,tag标签可快捷删除. 实现过程: 使用React. 使用Ant Design的Checkbox.Tag组件 ...

  3. GDI+ Bitmap与WPF BitmapImage的相互转换

    原文:GDI+ Bitmap与WPF BitmapImage的相互转换 using System.Windows.Interop; //... // Convert BitmapImage to Bi ...

  4. VS2012中使用CEGUI项目发布到XP平台的问题(核心方法就一句话。“你项目使用的所有外部依赖库都用/MT编译。”)

    接着上一篇文章,详细说说如何把一个带CEGUI的项目发布到XP平台. 这个问题纠缠了我好几天.这里把详细解决思路记下来.有同样问题的朋友可以少走很多弯路. 核心方法就一句话.“你项目使用的所有外部依赖 ...

  5. 类选择器和所作用的标签一起写为什么不起作用? - CSDN博客

    原文:类选择器和所作用的标签一起写为什么不起作用? - CSDN博客 HTML代码: css样式: 这不是将样式作用于circle类下的有current类的li标签吗?为什么不起作用? 原因: 选择器 ...

  6. uwp之拍照(使用后置摄像头)

    参考:wp8.1之拍照(获取焦点,使用后置摄像头) uwp开启摄像头要借助CaptureElement呈现来自捕获设备(如照相机或网络摄像机)的流.今天讲讲如何打开摄像头,获取焦点,以及拍照.废话不多 ...

  7. C#WebBroswer控件的使用

    在WebBroswer中可以嵌入一个网页文件,通过Url属性绑定. URI,统一资源标识符,用来唯一的标识一个资源. URL,统一资源定位器,它是一种具体的URI,即URL可以用来标识一个资源. 它包 ...

  8. 栈内存不是只有2M吗?为什么不溢出?

    #include <stdio.h> #include <wchar.h> #include <stdlib.h> #define MAX_PATH 1024 FI ...

  9. Qt4学习笔记 (7) 本篇说一下Qt对于脚本的支持, 即QtScript模块.

    本篇说一下Qt对于脚本的支持, 即QtScript模块. Qt支持的脚本基于ECMAScript脚本语言, 这个东西又是javascript, jscript的基础. 所以, 一般只要学过javasc ...

  10. 关于跨进程使用回调函数的研究:以跨进程获取Richedit中RTF流为例(在Delphi 初始化每一个TWinControl 对象时,将会在窗体 的属性(PropData)中加入一些标志,DLL的HInstance的值与HOST 进程的HInstance并不一致)

    建议先参考我上次写的博文跨进程获取Richedit中Text: 获得QQ聊天输入框中的内容 拿到这个问题,我习惯性地会从VCL内核开始分析.找到TRichEdit声明的单元,分析TRichEdit保存 ...