Angular.js
Angular.js 是一个 MV*(Model-View-Whatever,不管是 MVC 或者 MVVM,统归 MDV(model Drive View))JavaScript 框架。
Angular.js 是一款开源 JavaScript 库,由 Google 维护,用来协助单一页面应用程序运行的。它的目标是通过 MVC 模式(MVC)功能增强基于浏览器的应用,使开发和测试变得更加容易。
库读取包含附加自定义(标签属性)的 HTML,遵从这些自定义属性中的指令,并将页面中的输入或输出与由 JavaScript 变量表示的模型绑定起来。这些 JavaScript 变量的值可以手工设置,或者从静态或动态 JSON 资源中获取。
Angular.js 六大特性:
- 双向数据绑定
- 模板
- MVVM
- 依赖注入(Dependency Injection,DI)
- Directives(指令)
- 单元测试
Angular.js 简单示例:
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="utf-8">
- <script src="http://apps.bdimg.com/libs/angular.js/1.4.6/angular.min.js"></script>
- </head>
- <body>
-
- <div ng-app="">
- <p>名字 : <input type="text" ng-model="name"></p>
- <h1>Hello {{name}}</h1>
- </div>
-
- </body>
- </html>
Knockout.js
Knockout.js 是一个 JavaScript 库(MVVM),它可以让你声明绑定元素和其对应的数据模型,达到你的UI和模型自动双向更新。
Knockout.js 使用 js 代码达到双向绑定的目的,类似 Silverlight/WPF 里的绑定一样,我们主要就是利用相关的特性进行开发的,极大地减少了代码开发量。
Knockout.js 是一个轻量级的 UI 类库,通过应用 MVVM 模式使 JavaScript 前端 UI 简单化。Knockout 是一个以数据模型(data model)为基础的能够帮助你创建富文本,响应显示和编辑用户界面的 JavaScript 类库。任何时候如果你的 UI 需要自动更新(比如:更新依赖于用户的行为或者外部数据源的改变),KO 能够很简单的帮你实现并且很容易维护。
Knockout.js 有如下4大重要概念:
- 声明式绑定 (Declarative Bindings):使用简明易读的语法很容易地将模型(model)数据关联到 DOM 元素上。
- UI界面自动刷新 (Automatic UI Refresh):当您的模型状态(model state)改变时,您的 UI 界面将自动更新。
- 依赖跟踪 (Dependency Tracking):为转变和联合数据,在你的模型数据之间隐式建立关系。
- 模板 (Templating):为您的模型数据快速编写复杂的可嵌套的 UI。
重要特性:
- 优雅的依赖追踪- 不管任何时候你的数据模型更新,都会自动更新相应的内容。
- 声明式绑定- 浅显易懂的方式将你的用户界面指定部分关联到你的数据模型上。
- 灵活全面的模板- 使用嵌套模板可以构建复杂的动态界面。
- 轻易可扩展- 几行代码就可以实现自定义行为作为新的声明式绑定。
额外的好处:
- 纯 JavaScript 类库 – 兼容任何服务器端和客户端技术
- 可添加到Web程序最上部 – 不需要大的架构改变
- 简洁的 – Gzip 之前大约 25kb
- 兼容任何主流浏览器 (IE 6+、Firefox 2+、Chrome、Safari、其它)
- Comprehensive suite of specifications (采用行为驱动开发) - 意味着在新的浏览器和平台上可以很容易通过验证。
简单示例:
- <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
- <html>
- <head>
- <script type="text/javascript" src="http://knockoutjs.com/js/jquery-1.4.2.min.js"></script>
- <script type="text/javascript" src="http://knockoutjs.com/js/jquery.tmpl.js"></script>
- <script type="text/javascript" src="http://knockoutjs.com/js/knockout-1.2.1.js"></script>
- </head>
- <body>
- Choose a ticket class:
- <select data-bind="options: tickets,
- optionsCaption: 'Choose...',
- optionsText: 'name',
- value: chosenTicket">
- </select>
- </p>
- <p data-bind="template: 'ticketTemplate'">
- </p>
- <script id="ticketTemplate" type="text/x-jquery-tmpl">
- {{if chosenTicket}}
- You have chosen <b>${ chosenTicket().name }</b>
- ($${ chosenTicket().price })
- <button data-bind="click: resetTicket">Clear</button>
- {{/if}}
- </script>
- <script type="text/javascript">
- var viewModel = {
- tickets: [
- { name: "Economy", price: 199.95 },
- { name: "Business", price: 449.22 },
- { name: "First Class", price: 1199.99 }
- ],
- chosenTicket: ko.observable(),
- resetTicket: function () { this.chosenTicket(null) }
- };
- ko.applyBindings(viewModel);
- </script>
- </body>
- </html>
Backbone.js
Backbone.js 是一套 JavaScript 框架与 RESTful JSON 的应用程序接口。也是一套大致上符合 MVC 架构的编程范型。Backbone.js 以轻量为特色,只需依赖一套 Javascript 库即可运行。常被用来开发单页的互联网应用程序,以及用来维护网络应用程序的各种部分(例如多用户与服务器端)的同步。
Backbone 最适合的应用场景是单页面应用,并且页面上有大量数据模型,模型之间需要进行复杂的信息沟通。Backbone 在这种场景下,能很好的实现模块间松耦合和事件驱动。 其他适用产品还有微博,网易微博的前端设计也是和 Backbone 类似的一个结构。
Backbone.js 的适用场景非常广,无论是 Web-Page 还是 Web-App 都可以应用,但最合适的还是大型的 Web-App,对于中小型项目来讲 Backbone.js 的 MVC 结构还是有点臃肿了,用不好很容易 over design。Backbone.js 是非常典型的 MVC 框架,但是相对于传统的 server 端 MVC 来讲还是有一些特殊的地方的。
首先 Backbone 中的几大核心组件 View、Model、Collection、Router 中并没有 Controller。其实 v0.5 以前是有 Backbone.Controller 这个东西的,但由于做的根本不是 C 的事情,这个名字又太具有迷惑性了,后来改名叫做 Backbone.Router。而真正的 C 其实是 Backbone.View,但这个 View 其实是部分的 C(还有一部分在 Backbone.Router 中) + 部分的 V,由于前端的模板功能有限,很多应该在 template 中做的事情不得不被拿到 Backbone.View 中来实现。
其次,由于 MVC 的概念中认为 V 其实是永远不知道用户输入(鼠标、键盘事件等)的,C 是输入和 V 之间的连接,但在浏览器中这点其实是实现不了的,V 就是 HTML,而用户输入是基于 HTML 页面的,所以你可以忽略用户输入,把所有事件都导入到 C 去处理,但不代表 V 不知道这件事情。所以前端的 MVC 多少是对传统的 MVC 模型做了些改变的实现,近些日子更多的人转向 MVVM 就是这个原因。
Backbone.js 的优点:
- 代码质量比较高,通读一遍还是能学到不少东西的。
- 只做框架该做的事情,不做高大全的东西。所以很容易和其他的工具或框架整合。比如有人搞了 Bakcbone.js + Knockout.js 的 Knockback.js。
- 分层的结构很清晰,使得前端工程在扩展性和维护性上都可以进行有效控制。
Backbone.js 缺点:
- Model 结构比较简单,多对多、但对多的数据模型很难搞,用对象做属性也不行。
- 内存控制,View 很容易产生 memory leak 的问题,不过这也和代码的质量有关系,近期的更新有一些是针对这方面的。
React.js
React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己写一套,用来架设 Instagram 的网站。做出来以后,发现这套东西很好用,就在2013年5月开源了。
创造 React 是为了解决一个问题:构建随着时间数据不断变化的大规模应用程序。
React 两个主要的思想:
简单:仅仅只要表达出你的应用程序在任一个时间点应该长的样子,然后当底层的数据变了,React 会自动处理所有用户界面的更新。
声明式 (Declarative):数据变化后,React 概念上与点击“刷新”按钮类似,但仅会更新变化的部分。
React 都是关于构建可复用的组件。事实上,通过 React 你唯一要做的事情就是构建组件。得益于其良好的封装性,组件使代码复用、测试和关注分离(separation of concerns)更加简单。
Ember.js
Ember.js 是 JavaScript 框架包中最新的成员之一。 它演变出了最初于 2007 年创建的 SproutCore 项目,Apple 在包括 MobileMe 在内的各种 web 应用程序中大量使用了该项目。 在 emberjs.com,Ember 被形容为 "一个 JavaScript 框架,用于创建可以消除样板并提供标准应用程序架构的大型 web 应用程序。" 它本身紧密集成了名为 Handlebars 的模板引擎,该引擎为 Ember 提供了其中一个最强大的功能: 双向数据绑定。 Ember 还提供了其他功能,比如状态管理(某个用户状态是已注销还是已登录)、自动更新模板(当底层数据发生变化时,您的 UI 也同样发生变化)以及计算属性 (firstName + lastName = fullName)。 Ember 经过一年可靠的开发后,已经成为一个强大的参与者。
Ember 只有一个依赖项—jQuery。 Ember 应用程序的样板 HTML 设置看起来应该与下面的代码类似。 请注意,jQuery 和 Ember 都从 CDN(内容交付网络)进行更新。 如果用户在早些时候访问需要这些文件的其他网站时已经下载过这些文件,这会加快用户的页面加载速度。
与 Ember.js 相比,Angular.js 更像一个研究项目。比如,来看看它们的学习文档:Ember.js 主要讨论模型、视图和控制器,而 Angular.js 指南要求你去学习一些类似于范围、指示符和 transclusion 方面的内容等。
引用示例:
- <html>
- <head>
- <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
- <script src="http://cloud.github.com/downloads/emberjs/ember.js/ember-0.9.6.min.js"></script>
- <script src="js/app.js"></script>
- </head>
- <body>
- </body>
- </html>
Avalon.js
Avalon.js 是一个功能强大,体积小巧的 MVVM 框架。它遵循“操作数据即操作 DOM”的理念,让你在代码里基本见不到一点 DOM 操作代码。DOM 操作全部在绑定后,交给框架处理。相当后端有了 ORM 一样,不用你手写 SQL,提高生产力!
与其它 js 框架相比,同样实现著名的 todos 功能,在所有 MV* 的实现中 avalon 是让用户写代码最少的。
与其他 MV* 相比,它不仅轻量,最低支持到 IE6,而且性能是最好的。
优势:
- 使用简单,在 HTML 中添加绑定,在 JS 中用 avalon.define 定义 ViewModel,再调用 avalon.scan 方法,它就能动了!
- 兼容到 IE6(其他 mvvm 框架, knockoutjs IE6, angularjs IE7, emberjs IE8, winJS IE9 )
- 没有任何依赖,只有 72K,压缩后 22K
- 支持管道符风格的过滤函数,方便格式化输出
- 局部刷新的颗粒度已细化到一个文本节点,特性节点
- 要操作的节点,在第一次扫描就与视图刷新函数相绑定,并缓存起来,因此没有选择器出场的余地。
- 让 DOM 操作的代码近乎绝迹
- 使用类似 CSS 的重叠覆盖机制,让各个 ViewModel 分区交替地渲染页面
- 节点移除时,智能卸载对应的视图刷新函数,节约内存
- 操作数据即操作 DOM,对 ViewModel 的操作都会同步到 View 与 Model 去。
与其他框架比较:
- 它体积更少,在主要的几个 MVVM 框架(拥有双向绑定机制),knockout 是三千多行,angularjs 1.6万, emberjs 2-3 万行, winjs 是几 M, kendoui 是几 M!
- 兼容情况,kendoui 与 knockoutjs IE6, angularjs IE7, emberjs IE8, winJS IE9
- 让用户写代码更少
- 上手难度,与 knockout 差不多,但借鉴了 angularjs 的,更为易用。
- 与 knockoutjs, angular, winjs 一样是使用动态模板,至少保持第一屏数据是真实的,对 SEO 友好。
- 源码也是它们中最易读的。简单的代码也意味着扩展调试等容易。
Vue.js
官方地址:http://vuejs.org/
Vue.js 尤雨溪老师写的一个用于创建 web 交互界面的库,是一个精简的MVVM。从技术角度讲,Vue.js 专注于 MVVM 模型的 ViewModel 层。它通过双向数据绑定把 View 层和 Model 层连接了起来。实际的 DOM 封装和输出格式都被抽象为了Directives 和 Filters。Vue.js 和其他库相比是一个小而美的库,作者的主要目的是通过一个尽量简单的 API 产生可反映的数据绑定和可组合的视图组件,感觉作者的思路非常清晰。
Vue.js(读音 /vjuː/, 类似于 view)是一个构建数据驱动的 web 界面的库。Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。
Vue.js 自身不是一个全能框架——它只聚焦于视图层。因此它非常容易学习,非常容易与其它库或已有项目整合。另一方面,在与相关工具和支持库一起使用时,Vue.js 也能完美地驱动复杂的单页应用。
优点:
- 简单:官方文档很清晰,比 Angular 简单易学。
- 快速:异步批处理方式更新 DOM。
- 组合:用解耦的、可复用的组件组合你的应用程序。
- 紧凑:~18kb min+gzip,且无依赖。
- 强大:表达式 & 无需声明依赖的可推导属性 (computed properties)。
- 对模块友好:可以通过 NPM、Bower 或 Duo 安装,不强迫你所有的代码都遵循 Angular 的各种规定,使用场景更加灵活。
缺点:
- 新生儿:Vue.js 是一个新的项目,2014 年 3 月 20 日发布的 0.10.0ReleaseCandidate 版本,目前 github 上面最新的是 0.11.4 版本,没有 angular 那么成熟。
- 影响度不是很大:google 了一下,有关于 Vue.js 多样性或者说丰富性少于一些有名的库。
- 不支持 IE8:哈哈不过 AngularJS 1.3 也抛弃了对IE8的支持,但是 @司徒正美 老师的 avalon 是支持 IE6+ 的,应该下了很多努力去优化。这一点对于那些需要支持 IE8 的项目就不好了,不过这也是 web 前端开发的一个趋势,像 IE 低版本就应该退出历史舞台了,通过改变我们的前端思维,而不是顺应那些使用老版本而不去升级的人。玉伯老师就说过一句话,我觉得说的非常好“这年头,支持 IE6、7 早就不再是特性,而是耻辱。努力推动支付宝全面不支持 IE6、7,期待更多兄弟加盟”。
总结:(1) 简洁 (2) 轻量 (3)快速 (4) 数据驱动 (5) 模块友好 (6) 组件化
简单示例:
- // html
- <body>
- <div id="app">
- <p>{{ note }}</p>
- <input type="text" v-model="note">
- </div>
- </body>
-
- // js
- var vm = new Vue({
- el: '#app',
- data: {
- note: ''
- }
- })
前后端分离的坑
1.ajax跨域
解决ajax跨域问题
比如,前端应用为静态站点且部署在http://web.xxx.com域下,后端应用发布REST API并部署在http://api.xxx.com域下,如何使前端应用通过AJAX跨域访问后端应用呢?这需要使用到CORS技术来实现,这也是目前最好的解决方案了。
CORS全称为Cross Origin Resource Sharing(跨域资源共享),服务端只需添加相关响应头信息,即可实现客户端发出AJAX跨域请求。
CORS技术非常简单,易于实现,目前绝大多数浏览器均已支持该技术(IE8浏览器也支持了),服务端可通过任何编程语言来实现,只要能将CORS响应头写入response对象中即可。
下面我们继续扩展REST框架,通过CORS技术实现AJAX跨域访问。首先,我们需要编写一个Filter,用于过滤所有的HTTP请求,并将CORS响应头写入response对象中,代码如下:
- /**
- * 跨域访问处理(跨域资源共享)
- * 解决前后端分离架构中的跨域问题
- */
- public class CorsFilter implements Filter {
- private static final Logger log = Logger.getLogger(UserController.class);
- private String allowOrigin;
- private String allowMethods;
- private String allowCredentials;
- private String allowHeaders;
- private String exposeHeaders;
-
- @Override
- public void init(FilterConfig filterConfig) throws ServletException {
- allowOrigin = filterConfig.getInitParameter("allowOrigin");
- allowMethods = filterConfig.getInitParameter("allowMethods");
- allowCredentials = filterConfig.getInitParameter("allowCredentials");
- allowHeaders = filterConfig.getInitParameter("allowHeaders");
- exposeHeaders = filterConfig.getInitParameter("exposeHeaders");
- }
-
-
- /**
- * 通过CORS技术实现AJAX跨域访问,只要将CORS响应头写入response对象中即可
- * @param req
- * @param res
- * @param chain
- */
- @Override
- public void doFilter(ServletRequest req, ServletResponse res,
- FilterChain chain) throws IOException, ServletException {
- HttpServletRequest request = (HttpServletRequest) req;
- HttpServletResponse response = (HttpServletResponse) res;
- String currentOrigin = request.getHeader("Origin");
- log.debug("currentOrigin : " + currentOrigin);
- if (StringUtil.isNotEmpty(allowOrigin)) {
- List<String> allowOriginList = Arrays
- .asList(allowOrigin.split(","));
- log.debug("allowOriginList : " + allowOrigin);
- if (CollectionUtil.isNotEmpty(allowOriginList)) {
- if (allowOriginList.contains(currentOrigin)) {
- response.setHeader("Access-Control-Allow-Origin",
- currentOrigin);
- }
- }
- }
- if (StringUtil.isNotEmpty(allowMethods)) {
- response.setHeader("Access-Control-Allow-Methods", allowMethods);
- }
- if (StringUtil.isNotEmpty(allowCredentials)) {
- response.setHeader("Access-Control-Allow-Credentials",
- allowCredentials);
- }
- if (StringUtil.isNotEmpty(allowHeaders)) {
- response.setHeader("Access-Control-Allow-Headers", allowHeaders);
- }
- if (StringUtil.isNotEmpty(exposeHeaders)) {
- response.setHeader("Access-Control-Expose-Headers", exposeHeaders);
- }
- chain.doFilter(req, res);
- }
-
- @Override
- public void destroy() {
- }
- }
以上CorsFilter将从web.xml中读取相关Filter初始化参数,并将在处理HTTP请求时将这些参数写入对应的CORS响应头中,下面大致描述一下这些CORS响应头的意义:
-
Access-Control-Allow-Origin:允许访问的客户端域名,例如:http://web.xxx.com,若为*,则表示从任意域都能访问,即不做任何限制;
-
Access-Control-Allow-Methods:允许访问的方法名,多个方法名用逗号分割,例如:GET,POST,PUT,DELETE,OPTIONS;
-
Access-Control-Allow-Credentials:是否允许请求带有验证信息,若要获取客户端域下的cookie时,需要将其设置为true;
-
Access-Control-Allow-Headers:允许服务端访问的客户端请求头,多个请求头用逗号分割,例如:Content-Type;
-
Access-Control-Expose-Headers:允许客户端访问的服务端响应头,多个响应头用逗号分割。
需要注意的是,CORS规范中定义Access-Control-Allow-Origin只允许两种取值,要么为*,要么为具体的域名,也就是说,不支持同时配置多个域名。为了解决跨多个域的问题,需要在代码中做一些处理,这里将Filter初始化参数作为一个域名的集合(用逗号分隔),只需从当前请求中获取Origin请求头,就知道是从哪个域中发出的请求,若该请求在以上允许的域名集合中,则将其放入Access-Control-Allow-Origin响应头,这样跨多个域的问题就轻松解决了。以下是web.xml中配置CorsFilter的方法:
- <!-- 通过CORS技术实现AJAX跨域访问 -->
- <filter>
- <filter-name>corsFilter</filter-name>
- <filter-class>cn.edu.tju.rico.filter.CorsFilter</filter-class>
- <init-param>
- <param-name>allowOrigin</param-name>
- <param-value>http://localhost:8020</param-value>
- </init-param>
- <init-param>
- <param-name>allowMethods</param-name>
- <param-value>GET,POST,PUT,DELETE,OPTIONS</param-value>
- </init-param>
- <init-param>
- <param-name>allowCredentials</param-name>
- <param-value>true</param-value>
- </init-param>
- <init-param>
- <param-name>allowHeaders</param-name>
- <param-value>Content-Type,X-Token</param-value>
- </init-param>
- </filter>
- <filter-mapping>
- <filter-name>corsFilter</filter-name>
- <url-pattern>/*</url-pattern>
- </filter-mapping>
2.因为跨域问题JSESSIONID每次请求都会变化。
解决方案:每次ajax请求都携带上cookie到后端。
前端Ajax关键配置
$.ajax({
type: "post",
url: xxx,
data: xxx,
contentType: 'application/json',
dataType: "json",
xhrFields: {//关键配置
withCredentials: true
},
success: function (data) {}
})
后端过滤器(拦截所有请求)关键配置
- //解决Cookie跨域问题
- response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
- response.setHeader("Access-Control-Allow-Credentials", "true");
- response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
- response.setHeader("Access-Control-Allow-Methods","GET, POST, PUT, DELETE");
-
-
- //解决Content-Type:application/json时跨域设置失败问题
- if("OPTIONS".equals(request.getMethod())) { //放行OPTIONS请求
- filterChain.doFilter(request, response);
- return;
- }
注意:
- "Access-Control-Allow-Origin" 不能设置成 "*"
- 当 Content-Type 为 application/json 时,Ajax实际会发两次请求,第一次是一个OPTIONS请求,这时过滤器一定得放行。
3.ajax跨域安全机制
由于REST是无状态的,后端应用发布的REST API可在用户未登录的情况下被任意调用,这显然是不安全的,如何解决这个问题呢?我们需要为REST请求提供安全机制。同时还存在需要得到当前登陆的用户信息的需求存在,此时可按照以下步骤处理:
(1). 当用户登录成功后,采用session+token的方式进行验证。即除了在session中写入当前登陆的用户信息,还需要在服务端生成一个token,并将其放入内存中(可放入JVM或Redis中),同时将该token返回到客户端;
(2). 在客户端中将返回的token写入cookie中,并且每次请求时都将cookies跟随请求头一起发送到服务端;
(3). 提供一个AOP切面,用于拦截所有的Controller方法,在切面中判断cookies中存放的token的有效性或者判断session是否存放用户信息,从而获取到当前登陆的用户信息;
(4). 当登出时,只需清理掉客户端的cookie中的token即可。服务端token可设置过期时间,使其自行移除token。与此同时进行移除session中的用户信息操作。
首先,我们需要定义一个用于管理token的接口,包括创建token与检查token有效性的功能。代码如下:
- /**
- * 登录用户的身份鉴权
- */
- public interface TokenManager {
-
- String createToken(String username);
-
- boolean checkToken(String token);
-
- void deleteToken(String token);
- }
然后,提供一个简单的TokenManager实现类,将token存储到JVM内存中。代码如下:
- /**
- * TokenManager的默认实现
- * 管理 Token
- */
- public class DefaultTokenManager implements TokenManager {
-
- /** 将token存储到JVM内存(ConcurrentHashMap)中*/
- private static Map<String, String> tokenMap = new ConcurrentHashMap<String, String>();
-
- /**
- * 利用UUID创建Token(用户登录时,创建Token)
- * @param username
- * @return
- */
- public String createToken(String username) {
- String token = CodecUtil.createUUID();
- tokenMap.put(token, username);
- return token;
- }
-
- /**
- * Token验证(用户登录验证)
- * @param token
- * @return
- */
- public boolean checkToken(String token) {
- return !StringUtil.isEmpty(token) && tokenMap.containsKey(token);
- }
-
- /**
- * Token删除(用户登出时,删除Token)
- * @param token
- */
- @Override
- public void deleteToken(String token) {
- tokenMap.remove(token);
- }
- }
需要注意的是,如果需要做到分布式集群,建议基于Redis提供一个实现类,将token存储到Redis中,利用Redis的特性,做到token的分布式一致性。 然后,基于Spring AOP写一个切面类,用于拦截Controller类的方法,并从请求头中获取token,最后对token有效性进行判断。代码如下:
- /**
- * Title:安全检查切面(是否登录检查)
- * Description: 通过验证Token维持登录状态
- */
- @Component
- @Aspect
- public class SecurityAspect {
- private static final Logger log = Logger.getLogger(SecurityAspect.class);
- private TokenManager tokenManager;
-
- @Resource(name = "tokenManager")
- public void setTokenManager(TokenManager tokenManager) {
- this.tokenManager = tokenManager;
- }
-
- @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
- public Object execute(ProceedingJoinPoint pjp) throws Throwable {
- // 从切点上获取目标方法
- MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
- log.debug("methodSignature : " + methodSignature);
- Method method = methodSignature.getMethod();
- log.debug("Method : " + method.getName() + " : "
- + method.isAnnotationPresent(IgnoreSecurity.class));
- // 若目标方法忽略了安全性检查,则直接调用目标方法
- if (method.isAnnotationPresent(IgnoreSecurity.class)) {
- return pjp.proceed();
- }
-
- // 从 request header 中获取当前 token
- String token = WebContextUtil.getRequest().getHeader(
- Constants.DEFAULT_TOKEN_NAME);
- // 检查 token 有效性
- if (!tokenManager.checkToken(token)) {
- String message = String.format("token [%s] is invalid", token);
- log.debug("message : " + message);
- throw new TokenException(message);
- }
- // 调用目标方法
- return pjp.proceed();
- }
- }
若要使SecurityAspect生效,则需要在SpringMVC配置文件中添加如下Spring 配置:
- <!-- 启用注解扫描,并定义组件查找规则 ,mvc层只负责扫描@Controller、@ControllerAdvice -->
- <!-- base-package 如果多个,用“,”分隔 -->
- <context:component-scan base-package="cn.edu.tju.rico"
- use-default-filters="false">
- <!-- 扫描 @Controller -->
- <context:include-filter type="annotation"
- expression="org.springframework.stereotype.Controller" />
- <!-- 控制器增强,使一个Contoller成为全局的异常处理类,类中用@ExceptionHandler方法注解的方法可以处理所有Controller发生的异常 -->
- <context:include-filter type="annotation"
- expression="org.springframework.web.bind.annotation.ControllerAdvice" />
- </context:component-scan>
-
- <!-- 支持Controller的AOP代理 -->
- <aop:aspectj-autoproxy />
最后,在web.xml中添加允许的X-Token响应头,配置如下:
- <init-param>
- <param-name>allowHeaders</param-name>
- <param-value>Content-Type,X-Token</param-value>
- </init-param>
分离部署方案
前后端开发分离之后,应用在部署时也需要进行前后端分离。在进行前后端分离方案选择的时候,需要结合项目的需求情况和用户群体来考虑。目前业内较为常用的前后端分离部署方案有如下几种。
1、Nginx+Server
将前端资源部署在Nginx上,后端服务部署在常规的服务器。当浏览器发起访问请求的时候,如果请求的是页面资源,Nginx直接把资源返回到前端;如果请求是调用后端服务,则经过Nginx转发到后端服务器,完成响应后经Nginx返回到浏览器。
这个方案比较简单,易于实现,而且能达到前后端解耦的目的。
但是对于页面量比较大,需要有良好SEO的应用来说,此方案缺点也较为明显。因为Nginx只是向浏览器返回页面静态资源,而国内的搜索引擎爬虫只会抓取静态数据,不会解析页面中的js,这使得应用得不到良好的搜索引擎支持。同时因为Nginx不会进行页面的组装渲染,需要把静态页面返回到浏览器,然后完成渲染工作,这加重了浏览器的渲染负担。
2、Node+Server
这是淘宝所使用的前后端分离模式,在浏览器与后端服务器之间增加一个了Node Server作为中间层,将前端资源部署到Node Server中。Node Server中还包含了一层Model Proxy,负责与服务端进行通信。
浏览器发出的请求都被Node Server接收,然后通过Model Proxy调用后端服务器提供的服务。Node Server得到后端服务器反馈,接着在Node Server中完成页面的组装渲染,把最终页面返回给浏览器。
如此一来不仅达到了前后端解耦的目的,还解决了浏览器渲染负担过重的问题,为SEO提供了比较好的支持。
但在这样的模式中,浏览器所有发出的请求都需要经过Node Server进行中转,然后才能到达后端服务器。在实际的应用中,并不是所有的请求都需要页面渲染,只要在页面上直接调用后端服务器提供的服务即可。所以这个模式必然会对请求性能有所消耗
3、Nginx+Node+Server
为了能解决方案2中请求性能损失的问题,我们可以考虑在其基础之上增加Nginx。浏览器发起的请求经过Nginx进行分发,URL请求统一分发到Node Server,在Node Server中进行页面组装渲染;API请求则直接发送到后端服务器,完成响应。
目前在已经有一个名为Goku的Go语言框架提供了这样的前后端分离解决方案。
通过对三种前后端分离方案的对比可以看出:
如果是企业级应用,不需要考虑对SEO的支持,浏览器渲染也可以忽略不计,Nginx+Server的模式无疑是最好的选择,实施成本相对来说比较低;
如果是互联网应用,需要有良好的SEO支持,页面渲染工作量大,那应该选择Nginx+Node+Server的方案,各个方面都能得到比较好的兼顾。