上一篇讨论了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-资源使用授权的更多相关文章

  1. Ubuntu 14.04 鼠标消失解决方案

    Ubuntu 14.04 鼠标消失解决方案: 进入文字命令行模式,输入startx, 返回图像模式.

  2. Spring Cloud 微服务中搭建 OAuth2.0 认证授权服务

    在使用 Spring Cloud 体系来构建微服务的过程中,用户请求是通过网关(ZUUL 或 Spring APIGateway)以 HTTP 协议来传输信息,API 网关将自己注册为 Eureka ...

  3. 微信公众平台开发(71)OAuth2.0网页授权

    微信公众平台开发 OAuth2.0网页授权认证 网页授权获取用户基本信息 作者:方倍工作室 微信公众平台最近新推出微信认证,认证后可以获得高级接口权限,其中一个是OAuth2.0网页授权,很多朋友在使 ...

  4. Force.com微信开发系列(七)OAuth2.0网页授权

    OAuth是一个开放协议,允许用户让第三方应用以安全且标准的方式获取该用户在某一网站上存储的私密资源(如用户个人信息.照片.视频.联系人列表),而无须将用户名和密码提供给第三方应用.本文将详细介绍OA ...

  5. 使用Owin中间件搭建OAuth2.0认证授权服务器

    前言 这里主要总结下本人最近半个月关于搭建OAuth2.0服务器工作的经验.至于为何需要OAuth2.0.为何是Owin.什么是Owin等问题,不再赘述.我假定读者是使用Asp.Net,并需要搭建OA ...

  6. ***微信公众平台开发: 获取用户基本信息+OAuth2.0网页授权

    本文介绍如何获得微信公众平台关注用户的基本信息,包括昵称.头像.性别.国家.省份.城市.语言.本文的方法将囊括订阅号和服务号以及自定义菜单各种场景,无论是否有高级接口权限,都有办法来获得用户基本信息, ...

  7. C#-MVC开发微信应用(2)--OAuth2.0网页授权

    微信公众平台最近新推出微信认证,认证后可以获得高级接口权限,其中一个是OAuth2.0网页授权,很多朋友在使用这个的时候失败了或者无法理解其内容,希望我出个教程详细讲解一下,于是便有了这篇文章. 一. ...

  8. 黄聪:微信公众平台开发OAuth2.0网页授权(转)

    微信公众平台开发 OAuth2.0网页授权认证 网页授权获取用户基本信息 作者:方倍工作室 微信公众平台最近新推出微信认证,认证后可以获得高级接口权限,其中一个是OAuth2.0网页授权,很多朋友在使 ...

  9. 用Chrome查看微信访问需要OAuth2.0网页授权的页面

    在PC浏览器打开某网站页面提示页面错误,是因为进行了OAuth2.0网页授权 有以下限制, 1.必须在微信打开 2.微信页面授权 其中第一步比较容易实现,修改下ua(user-agent),让其携带“ ...

  10. 微信公众平台OAuth2.0网页授权

    微信公众平台最近新推出微信认证,认证后可以获得高级接口权限,其中一个是OAuth2.0网页授权,很多朋友在使用这个的时候失败了或者无法理解其内容,希望我出个教程详细讲解一下,于是便有了这篇文章. 一. ...

随机推荐

  1. Viewport3D中的摄像机(二、摄像机动作)

    原文:Viewport3D中的摄像机(二.摄像机动作) 前文介绍了Viewport3D中的两种摄像机:OrthographicCamera和PerspectiveCamera.在3D场景里漫游,最主要 ...

  2. ASP.NET Core 数据库上下文 - ASP.NET Core 基础教程 - 简单教程,简单编程

    原文:ASP.NET Core 数据库上下文 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core 数据库上下文 上一章节中我们了解了 Entity Framewo ...

  3. 方阵的迹(trace)及其微分(导数)

    trace 的一个十分重要的性质在于线性性, Tr(A+B)=Tr(A)+Tr(B)Tr(cA)=cTr(A) 1. 基本性质 Tr(A)=Tr(AT) Tr(AB)=Tr(BA) Tr(ABC)=T ...

  4. 学习git命令

    1.git init @创建仓库 2.git add  filename @添加文件到缓存区 3.git commit -m"注释说明"   @提交修改内容 4.git statu ...

  5. ./configure,make,make install的作用(configure一般用来生成 Makefile,相当于qmake)

    这些都是典型的使用GNU的AUTOCONF和AUTOMAKE产生的程序的安装步骤. ./configure是用来检测你的安装平台的目标特征的.比如它会检测你是不是有CC或GCC,并不是需要CC或GCC ...

  6. CountDownLatch和CyclicBarrier 专题

    4.Runnable接口和Callable接口的区别 有点深的问题了,也看出一个Java程序员学习知识的广度. Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行ru ...

  7. 在vs2015上使用asp.net core+ef core

    官方的文档https://docs.asp.net/en/latest/tutorials/first-mvc-app/start-mvc.html 先来看一下实现的效果

  8. TestDisk 数据恢复 重建分区表恢复文件-恢复diskpart clean

    source:http://www.cgsecurity.org/wiki/TestDisk_CN TestDisk 是一款开源软件,受GNU General Public License (GPL ...

  9. Qt中使用Boost

    编译BOOST库 bjam stage --toolset=qcc --without-graph --without-graph_parallel --without-math --without- ...

  10. QString转换为LPTSTR(使用了reinterpret_cast,真是叹为观止,但是也开阔了思路),三篇文章合起来的各种转换方法

    醉了,windows下宏定义了很多char类型 LPTSTR .今天,直接使用,qt报错,真TM费事. 将“CPU”转化为wcha_t * QString str = "CPU"; ...