Xitrum学习笔记04 - RESTful APIs

RESTful API:

符合RESTful架构的API称为RESTful API,不同的前端设备与后端进行通信的一种统一机制

什么是RESTful架构:

  (1)每一个URI代表一种资源;

  (2)客户端和服务器之间,传递这种资源的某种表现层;

  (3)客户端通过HTTP动词(GET用来获取资源,POST用来新建、更新资源,PUT用来更新资源,DELETE用来删除资源),对服务器端资源进行操作,实现"表现层状态转化"。

REST -- REpresentational State Transfer
全称是 Resource Representational State Transfer:通俗来讲就是:资源在网络中以某种表现形式进行状态转移。分解开来:
Resource:资源,即数据(前面说过网络的核心)。比如 newsfeed,friends等;
Representational:某种表现形式,比如用JSON,XML,JPEG等;
State Transfer:状态变化。通过HTTP动词实现。

参考文档:

http://www.ruanyifeng.com/blog/2011/09/restful       --理解RESTful架构

http://www.ruanyifeng.com/blog/2014/05/restful_api.html   --RESTful API 设计指南

https://www.zhihu.com/question/28557115    

Xitrum RESTful API示例:

import xitrum.Action
import xitrum.annotation.GET
@GET("articles")
class ArticlesIndex extends Action {
  def execute() {...}
}
@GET("articles/:id")
class ArticlesShow extends Action {
  def execute() {...}
}

POST, PUT, PATCH, DELETE, and OPTIONS的使用与GET相同,Xitrum自动把HEAD当做响应体为空的GET来处理。

对于不支持PUT和DELETE的HTTP客户端,通过发送响应体中带有 _method=put 和 _method=delete 的POST来模拟PUT和DELETE动作

Web应用程序启动时,Xitrum会扫描所有annotations,创建路由表并在Console里打印出来,如:

[INFO] Normal routes:
GET        /articles/new                  demos.action.ArticlesNew
GET        /                              demos.action.SiteIndex
POST       /api/articles                  demos.action.ApiArticlesCreate
PATCH      /api/articles/:id              demos.action.ApiArticlesUpdate
DELETE     /api/articles/:id              demos.action.ApiArticlesDestroy
GET        /articles/:id<[0-9]+>.:format  demos.action.ArticlesDotShow
[INFO] SockJS routes:
/sockJsChat         demos.action.SockJsChatActor    websocket: true, cookie_needed: false
/fileMonitorSocket  demos.action.FileMonitorSocket  websocket: true, cookie_needed: false
[INFO] Error routes:
404  demos.action.NotFoundError
500  demos.action.ServerError
[INFO] Xitrum routes:
GET  /xitrum/xitrum-3.28.3.js  xitrum.js
GET  /xitrum/swagger.json      xitrum.routing.SwaggerJson
GET  /xitrum/swagger           xitrum.routing.SwaggerUi
GET  /xitrum/metrics/viewer    xitrum.metrics.XitrumMetricsViewer
[INFO] Xitrum SockJS routes:
/xitrum/metrics/channel  xitrum.metrics.XitrumMetricsChannel  websocket: true, cookie_needed: false

路由(Routes)会自动被收集,不需要额外的声明工作,我们也可以以类型安全的方式重建URLs

Route cache(路由缓存)

为了加快启动速度,路由被缓存到了文件 routes.cache中。在开发过程中,在target目录下的.class中的路由不会被缓存。

如果改变了包含路由的依赖库,需要删除routes.cache。routes.cache不应该被提交到代码版本库中

使用First和Last定义Route优先级

import xitrum.annotation.{GET, First}
@GET("articles/:id")
class ArticlesShow extends Action {
  def execute() {...}
}

@First // This route has higher priority than "ArticlesShow" above
@GET("articles/new")
class ArticlesNew extends Action {
  def execute() {...}
}

这样在定义routes表时,ArticlesNew相应的路由就会排到最前面。Last注解的使用与First相同

一个Action有多个路由

@GET("image", "image/:format")
class Image extends Action {
  def execute() {
    val format = paramo("format").getOrElse("png")
    // ...
  }
}

路由中有点号和正则表达式

@GET("articles/:id", "articles/:id.:format")
class ArticlesShow extends Action {
  def execute() {
    val id = param[Int]("id")
    val format = paramo("format").getOrElse("html")
    // ...
  }
}

@GET("articles/:id<[0-9]+>")
...

获取其余路由

/ 斜杠是特殊字符,所以不能出现在路由的参数中。需要参数中有斜杠的话,要把参数放在最后,且要用星号,例如

GET("service/:id/proxy/:*")

这个写法可以匹配  /service/123/proxy/http://foo.com/bar

获取 :* 的部分可以用以下代码实现

val url = param("*")   // Will be "http://foo.com/bar"

通过超链接标记<a>链接到一个Action

在View中的写法

<a href={url[ArticlesShow]("id" -> myArticle.id)}>{myArticle.title}</a>

重定向和转发到另一个action

重定向和转发:

转发是服务器行为,重定向是客户端行为。为什么这样说呢,这就要看两个动作的工作流程:

转发过程:客户浏览器发送http请求——》web服务器接受此请求——》调用内部的一个方法在容器内部完成请求处理和转发动作——》将目标资源发送给客户;在这里,转发的路由必须是同一个web容器下的url,其不能转向到其他的web路由上去,中间传递的是自己的容器内的request。在客户浏览器地址栏显示的仍然是其第一次访问的路由,也就是说客户是感觉不到服务器做了转发的。转发行为是浏览器只做了一次访问请求。

重定向过程:客户浏览器发送http请求——》web服务器接受后发送302状态码响应及对应新的location给客户浏览器——》客户浏览器发现是302响应,则自动再发送一个新的http请求,请求url是新的location地址——》服务器根据此请求寻找资源并发送给客户。在这里location可以重定向到任意URL,既然是浏览器重新发出了请求,则就没有什么request传递的概念了。在客户浏览器地址栏显示的是其重定向的路由,客户可以观察到地址的变化的。重定向行为是浏览器做了至少两次的访问请求的。

典型的应用场景:
1. forward: 访问 Servlet 处理业务逻辑,然后 forward 到 jsp 显示处理结果,浏览器里 URL 不变
2. redirect: 提交表单,处理成功后 redirect 到另一个 jsp,防止表单重复提交,浏览器里 URL 变了

Xitrum的重定向:

import xitrum.Action
import xitrum.annotation.{GET, POST}
@GET("login")
class LoginInput extends Action {
  def execute() {...}
}
@POST("login")
class DoLogin extends Action {
  def execute() {
    ...
    // After login success
    redirectTo[AdminIndex]()
  }
}
@GET("admin")
class AdminIndex extends Action {
  def execute() {
    ...
    // Check if the user has not logged in, redirect him to the login page
    redirectTo[LoginInput]()  //产生新的request
  }
}

重定向到当前action可以用 redirectToThis() 方法

转发到另一个action

forwardTo[AnotherAction]()   //不产生新的request

如何确定需求是否是一个Ajax需求

用 isAjax

// In an action
val msg = "A message"
if (isAjax)
  jsRender("alert(" + jsEscape(msg) + ")")
else
  respondText(msg)

CSRF相关

什么事CSRF

Cross-site request forgery,跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。

危害是在用户登录受信任网站A,并在本地生成Cookie后,在不登出A的情况下,访问危险网站B。

这样攻击者可以盗用你的身份,以你的名义发送在网站A上的恶意请求。比如可以盗取你的账号,以你的身份发送邮件,购买商品等。

防御CSRF

对于非GET请求, Xitrum默认为web应用防御CSRF

在View代码中加入 antiCsrfMeta 后,

import xitrum.Action
import xitrum.view.DocType
trait AppAction extends Action {
  override def layout = DocType.html5(
    <html>
      <head>
        {antiCsrfMeta}
        {xitrumCss}
        {jsDefaults}
        <title>Welcome to Xitrum</title>
      </head>
      <body>
        {renderedView}
        {jsForView}
       </body>
    </html>
  )
}

相应地在HTML页面的head内,会生成

<meta name="csrf-token" content="5402330e-9916-40d8-a3f4-16b271d583be" />

如果将xitrum.js加到View模板,这个token会被自动包含到所有非GET Ajax请求中,作为由jQuery发出的X-CSRF-Token头信息。 

View中调用jsDefaults方法,就可以把xitrum.js加入到view中。

另一种把xitrum.js加入到view中的方法是,在View中加入如下代码

<script type="text/javascript" src={url[xitrum.js]}></script>

Xitrum从X-CSRF-Token请求头中获取CSRF token,如果这个头不存在,Xitrum从csrf-token请求体参数中获取(不是从URL中的参数获取)

如果head中没有使用csrf-token meta标签和xitrum.js,在form中需要添加antiCsrfInput或antiCsrfToken

form(method="post" action={url[AdminAddGroup]})
  != antiCsrfInput
//或者
form(method="post" action={url[AdminAddGroup]})
  input(type="hidden" name="csrf-token" value={antiCsrfToken})

需要跳过CSRF检查时,混入特质 xitrum.SkipCsrfCheck到Action中,如

import xitrum.{Action, SkipCsrfCheck}
import xitrum.annotation.POST
trait Api extends Action with SkipCsrfCheck
@POST("api/positions")
class LogPositionAPI extends Api {
  def execute() {...}
}
@POST("api/todos")
class CreateTodoAPI extends Api {
  def execute() {...}
}

变更已收集的路由

Xitrum在Web应用启动时自动收集Action路由,可以通过在Boot.scala中使用xitrum.Config.routes变更路由

import xitrum.{Config, Server}
object Boot {
  def main(args: Array[String]) {
    // You can modify routes before starting the server
    val routes = Config.routes

    // Remove routes to an action by its class
    routes.removeByClass[MyClass]()
    if (demoVersion) {
      // Remove routes to actions by a prefix
      routes.removeByPrefix("premium/features")
      // This also works
      routes.removeByPrefix("/premium/features")
    }
    ...
    Server.start()
  }
}

获取整个请求内容

一般情况下,如果请求的内容类型不是application/x-www-form-urlencoded,可能需要在代码中解析整个请求内容

//To get it as a string:
val body = requestContentString

//To get it as JSON:
val myJValue = requestContentJValue // => JSON4S (http://json4s.org) JValue
val myMap = xitrum.util.SeriDeseri.fromJValue[Map[String, Int]](myJValue)

如果想获得全面控制,使用 request.getContent,它返回一个ByteBuf类型的值(查阅了http://netty.io/4.0/api/io/netty/handler/codec/http/FullHttpRequest.html,没有getContent方法,这句话有错误

posted @ 2017-05-15 13:23  子秦  阅读(875)  评论(0编辑  收藏  举报