Akka-CQRS(14)- Http标准安全解决方案:OAuth2-资源使用授权
上一篇讨论了SSL/TLS安全连接,主要是一套在通信层面的数据加密解决方案。但我们更需要一套方案来验证客户端。要把不能通过验证的网络请求过滤掉。
OAuth2是一套行业标准的网络资源使用授权协议,也就是为用户提供一种授权凭证,用户凭授权凭证来使用网络资源。申请凭证、然后使用凭证进行网络操作流程如下:

实际上OAuth2是一套3方授权模式,但我们只需要资源管理方授权,所以划去了1、2两个步骤。剩下的两个步骤,包括:申请令牌,使用令牌,这些在官方文件中有详细描述。用户身份和令牌的传递是通过Http Header实现的,具体情况可参考RFC2617,RFC6750
简单来说:用户向服务器提交身份信息申请令牌,下面是一个HttpRequest样例:
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
上面Basic后面一串代码就是 user+password的加密文,它的产生方法示范如下:
final case class BasicHttpCredentials(username: String, password: String) extends jm.headers.BasicHttpCredentials {
val cookie = {
val userPass = username + ':' + password
val bytes = userPass.getBytes(`UTF-`.nioCharset)
Base64.rfc2045.encodeToChar(bytes, false)
}
def render[R <: Rendering](r: R): r.type = r ~~ "Basic " ~~ cookie
override def scheme: String = "Basic"
override def token: String = String.valueOf(cookie)
override def params: Map[String, String] = Map.empty
}
注:在OAuth2版本中如果使用https://,则容许明文用户和密码。
服务端在返回的HttpResponse中返回令牌access_token:
{"access_token":"2e510027-0eb9-4367-b310-68e1bab9dc3d", "token_type":"bearer", "expires_in":}
注意:这个expires_in是应用系统自定义内部使用的参数,也就是说应用系统必须自备令牌过期失效处理机制。
得到令牌后每个使用网络资源的Request都必须在Authorization类Header里附带这个令牌,如:
GET /resource HTTP/1.1
Host: server.example.com
Authorization: Bearer 2e510027-0eb9--b310-68e1bab9dc3d
Bearer后就是服务端返回的令牌值。我们还是设计一个例子来示范整个授权使用过程。先看看下面一些基本操作代码:
object JsonMarshaller extends SprayJsonSupport with DefaultJsonProtocol {
case class UserInfo(username: String, password: String)
case class AuthToken(access_token: String = java.util.UUID.randomUUID().toString,
token_type: String = "bearer",
expires_in: Int = )
case class AuthUser(credentials: UserInfo,
token: AuthToken = new AuthToken(expires_in = * * ),
loggedInAt: String = LocalDateTime.now().toString)
val validUsers = Seq(UserInfo("johnny", "p4ssw0rd"),UserInfo("tiger", "secret"))
val loggedInUsers = mutable.ArrayBuffer.empty[AuthUser]
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
}
def authenticateUser(credentials: Credentials): Option[AuthUser] =
credentials match {
case p @ Credentials.Provided(_) =>
loggedInUsers.find(user => p.verify(user.token.access_token))
case _ => None
}
implicit val fmtCredentials = jsonFormat2(UserInfo.apply)
implicit val fmtToken = jsonFormat3(AuthToken.apply)
implicit val fmtUser = jsonFormat3(AuthUser.apply)
}
validUers: Seq[UserInfo] 模拟是个在服务端数据库里的用户登记表,loggedInUsers是一个已经通过验证的用户请单。函数 getValidUser(credentials: Credentials) 用传人参数Credentials来获取用户信息Option[UserInfo]。Credentials是这样定义的:
object Credentials {
case object Missing extends Credentials
abstract case class Provided(identifier: String) extends Credentials {
/**
* First applies the passed in `hasher` function to the received secret part of the Credentials
* and then safely compares the passed in `secret` with the hashed received secret.
* This method can be used if the secret is not stored in plain text.
* Use of this method instead of manual String equality testing is recommended in order to guard against timing attacks.
*
* See also [[EnhancedString#secure_==]], for more information.
*/
def verify(secret: String, hasher: String ⇒ String): Boolean
/**
* Safely compares the passed in `secret` with the received secret part of the Credentials.
* Use of this method instead of manual String equality testing is recommended in order to guard against timing attacks.
*
* See also [[EnhancedString#secure_==]], for more information.
*/
def verify(secret: String): Boolean = verify(secret, x ⇒ x)
}
def apply(cred: Option[HttpCredentials]): Credentials = {
cred match {
case Some(BasicHttpCredentials(username, receivedSecret)) ⇒
new Credentials.Provided(username) {
def verify(secret: String, hasher: String ⇒ String): Boolean = secret secure_== hasher(receivedSecret)
}
case Some(OAuth2BearerToken(token)) ⇒
new Credentials.Provided(token) {
def verify(secret: String, hasher: String ⇒ String): Boolean = secret secure_== hasher(token)
}
case Some(GenericHttpCredentials(scheme, token, params)) ⇒
throw new UnsupportedOperationException("cannot verify generic HTTP credentials")
case None ⇒ Credentials.Missing
}
}
}
在apply函数里定义了verify函数功能。这个时候Credentials的实际类型是BasicHttpCredentials。另一个函数authenticateUser(credentials: Credentials)是用Crentials来验证令牌的,那么它的类型应该是OAuth2BearerToken了,具体验证令牌的过程是从loggedInUser清单里对比找出拥有相同令牌的用户。这就意味着每次一个用户通过验证获取令牌后服务端必须把用户信息和令牌值保存起来方便以后对比。我们再来看看route的定义:
val route =
pathEndOrSingleSlash {
get {
complete("Welcome!")
}
} ~
path("auth") {
authenticateBasic(realm = "auth", getValidUser) { user =>
post {
val loggedInUser = AuthUser(user)
loggedInUsers.append(loggedInUser)
complete(loggedInUser.token)
}
}
} ~
path("api") {
authenticateOAuth2(realm = "api", authenticateUser) { validToken =>
complete(s"It worked! user = $validToken")
}
}
现在这段代码就比较容易理解了:authenticateBasic(realm = "auth", getValidUser) {user => ...} 用上了自定义的geValidUser来产生user对象。而authenticateOAuth2(realm = "api", authenticateUser) { validToken =>...}则用了自定义的authenticateUser函数来验证令牌。
下面我们写一段客户端代码来测试上面这个webserver的功能:
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 org.json4s._
import org.json4s.jackson.JsonMethods._
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) val token = (parse(jstr).asInstanceOf[JObject] \ "access_token").values
println(token) val authentication = headers.Authorization(OAuth2BearerToken(token.toString))
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()
} }
测试显示结果如下:
{"access_token":"6280dcd7-71fe-4203-8163-8ac7dbd5450b","expires_in":,"token_type":"bearer"}
6280dcd7-71fe---8ac7dbd5450b
HttpResponse( OK,List(Server: akka-http/10.1., Date: Wed, Jul :: GMT),HttpEntity.Strict(text/plain; charset=UTF-,It worked! user = AuthUser(UserInfo(johnny,p4ssw0rd),AuthToken(6280dcd7-71fe---8ac7dbd5450b,bearer,),--03T17::32.627)),HttpProtocol(HTTP/1.1))
下面是服务端源代码:
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"
)
OAuth2Server.scala
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 java.time.LocalDateTime
import scala.collection.mutable import akka.http.scaladsl.marshallers.sprayjson._
import spray.json._ object JsonMarshaller extends SprayJsonSupport with DefaultJsonProtocol { case class UserInfo(username: String, password: String) case class AuthToken(access_token: String = java.util.UUID.randomUUID().toString,
token_type: String = "bearer",
expires_in: Int = ) case class AuthUser(credentials: UserInfo,
token: AuthToken = new AuthToken(expires_in = * * ),
loggedInAt: String = LocalDateTime.now().toString) val validUsers = Seq(UserInfo("johnny", "p4ssw0rd"),UserInfo("tiger", "secret"))
val loggedInUsers = mutable.ArrayBuffer.empty[AuthUser] 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
} def authenticateUser(credentials: Credentials): Option[AuthUser] =
credentials match {
case p @ Credentials.Provided(_) =>
loggedInUsers.find(user => p.verify(user.token.access_token))
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 loggedInUser = AuthUser(user)
loggedInUsers.append(loggedInUser)
complete(loggedInUser.token)
}
}
} ~
path("api") {
authenticateOAuth2(realm = "api", authenticateUser) { validToken =>
complete(s"It worked! user = $validToken")
}
} 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()) }
Akka-CQRS(14)- Http标准安全解决方案:OAuth2-资源使用授权的更多相关文章
- Ubuntu 14.04 鼠标消失解决方案
Ubuntu 14.04 鼠标消失解决方案: 进入文字命令行模式,输入startx, 返回图像模式.
- Spring Cloud 微服务中搭建 OAuth2.0 认证授权服务
在使用 Spring Cloud 体系来构建微服务的过程中,用户请求是通过网关(ZUUL 或 Spring APIGateway)以 HTTP 协议来传输信息,API 网关将自己注册为 Eureka ...
- 微信公众平台开发(71)OAuth2.0网页授权
微信公众平台开发 OAuth2.0网页授权认证 网页授权获取用户基本信息 作者:方倍工作室 微信公众平台最近新推出微信认证,认证后可以获得高级接口权限,其中一个是OAuth2.0网页授权,很多朋友在使 ...
- Force.com微信开发系列(七)OAuth2.0网页授权
OAuth是一个开放协议,允许用户让第三方应用以安全且标准的方式获取该用户在某一网站上存储的私密资源(如用户个人信息.照片.视频.联系人列表),而无须将用户名和密码提供给第三方应用.本文将详细介绍OA ...
- 使用Owin中间件搭建OAuth2.0认证授权服务器
前言 这里主要总结下本人最近半个月关于搭建OAuth2.0服务器工作的经验.至于为何需要OAuth2.0.为何是Owin.什么是Owin等问题,不再赘述.我假定读者是使用Asp.Net,并需要搭建OA ...
- ***微信公众平台开发: 获取用户基本信息+OAuth2.0网页授权
本文介绍如何获得微信公众平台关注用户的基本信息,包括昵称.头像.性别.国家.省份.城市.语言.本文的方法将囊括订阅号和服务号以及自定义菜单各种场景,无论是否有高级接口权限,都有办法来获得用户基本信息, ...
- C#-MVC开发微信应用(2)--OAuth2.0网页授权
微信公众平台最近新推出微信认证,认证后可以获得高级接口权限,其中一个是OAuth2.0网页授权,很多朋友在使用这个的时候失败了或者无法理解其内容,希望我出个教程详细讲解一下,于是便有了这篇文章. 一. ...
- 黄聪:微信公众平台开发OAuth2.0网页授权(转)
微信公众平台开发 OAuth2.0网页授权认证 网页授权获取用户基本信息 作者:方倍工作室 微信公众平台最近新推出微信认证,认证后可以获得高级接口权限,其中一个是OAuth2.0网页授权,很多朋友在使 ...
- 用Chrome查看微信访问需要OAuth2.0网页授权的页面
在PC浏览器打开某网站页面提示页面错误,是因为进行了OAuth2.0网页授权 有以下限制, 1.必须在微信打开 2.微信页面授权 其中第一步比较容易实现,修改下ua(user-agent),让其携带“ ...
- 微信公众平台OAuth2.0网页授权
微信公众平台最近新推出微信认证,认证后可以获得高级接口权限,其中一个是OAuth2.0网页授权,很多朋友在使用这个的时候失败了或者无法理解其内容,希望我出个教程详细讲解一下,于是便有了这篇文章. 一. ...
随机推荐
- 參数传递(引用,指针,值传递)C++11
C++中,函数的參数传递方式有值传递.地址传递.传地址有指针和引用方式. 在函数參数中,传地址的理由有: 1.使被调函数能够改动主调函数中的数据对象: 2.传地址能够降低数据拷贝,提高程序运行速度. ...
- WPF - 善用路由事件
原文:WPF - 善用路由事件 在原来的公司中,编写自定义控件是常常遇到的任务.但这些控件常常拥有一个不怎么好的特点:无论是内部还是外部都没有使用路由事件.那我们应该怎样宰自定义控件开发中使用路由事件 ...
- WCF的几个注意事项
wcf托管服务注意的问题 加上项目分为客户端-WCF服务-逻辑层-数据库三层wcf一直出现异常,说没有初始化啊之类的,如果你的逻辑代码确定没有问题的话,思考是不是wcf的配置文件(app.config ...
- ES6中的Promise详解
Promise 在 JavaScript 中很早就有各种的开源实现,ES6 将其纳入了官方标准,提供了原生 api 支持,使用更加便捷. 定义 Promise 是一个对象,它用来标识 JavaScri ...
- 图像滤镜艺术---Photoshop实现Instagram之Mayfair滤镜效果
原文:图像滤镜艺术---Photoshop实现Instagram之Mayfair滤镜效果 本文介绍一下如何使用Photoshop来实现Instagram中的Mayfair滤镜的效果. 以上就是这个滤镜 ...
- Generating Names and Classifying Names with Character-Level RNN
原文地址: Generating Names with Character-Level RNN 搬运只为督促自己学习,没有其他目的. Preparing the Data Download the ...
- IT回忆录-2
随着网络的发展,下载工具也不断地更新. 印象比较深的下载工具,从网络蚂蚁.网际快车,到BT. BT出来的时候,对下载真的是一个革命啊,以前下载东西,下载的人越多肯定就越慢,我们之前还会跑到一些FTP上 ...
- 利用Socket通信
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket. 建立网络通信连接至少要一对端口号(socket).socket本质是编程接口(API),对TCP/IP的封装 ...
- Mongodb Compile C++ Driver
之前发现直接编译mongo源码中的驱动,静态库的驱动会很大,在链接使用的时候会报很多链接错误. 转而直接编译单独提供驱动源码,同样vc2008的版本也要做我的另一篇博文中修改,在这不多说,具体参见: ...
- 使用dumpbin命令查看dll导出函数及重定向输出到文件(VS自带)
以前查看dll导出函数,一般使用Viewdll等第三方工具.但由于Viewdll采用dephi编写,因此仅能查看32位的dll.其实微软已经帮我们提供一个查看dll导出函数的命令,嵌在VS开发环境中, ...