Xitrum学习笔记04 - RESTful APIs
RESTful API:
符合RESTful架构的API称为RESTful API,不同的前端设备与后端进行通信的一种统一机制
什么是RESTful架构:
(1)每一个URI代表一种资源;
(2)客户端和服务器之间,传递这种资源的某种表现层;
(3)客户端通过HTTP动词(GET用来获取资源,POST用来新建、更新资源,PUT用来更新资源,DELETE用来删除资源),对服务器端资源进行操作,实现"表现层状态转化"。
全称是 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方法,这句话有错误)