摘要:
本人在前辈《从mvc到前后端分离(rest-个人也认为是目前比较流行和比较好的方式)》一文的基础上,实现了一个基于spring的符合rest风格的完整demo,具有mvc分层结构并实现前后端分离,该项目体现了一个具有rest风格项目的基本特征,即具有统一响应结构、 前后台数据流转机制(http消息与java对象的互相转化机制)、统一的异常处理机制、参数验证机制、cors跨域请求机制以及鉴权机制。此外,该项目的完整源码可移步到我的github参考,项目地址:https://github.com/githubofrico/restspringmvcdemo。
版权声明与致谢:
本文原创作者:
作者博客地址:
本文是在csdn博友shao.bing的专栏的一文基础上整理而成,实现了文中所述的rest框架并进行分享,相关源码移步至我的github进行下载,项目地址:https://github.com/githubofrico/restspringmvcdemo。
1. 理解 mvc
mvc是一种经典的设计模式,全名为model-view-controller,即模型-视图-控制器。其中,模型是用于封装数据的载体,例如,在java中一般通过一个简单的pojo(plain ordinary java object)来表示,其本质是一个普通的java bean,包含一系列的成员变量及其getter/setter方法。对于视图而言,它更加偏重于展现,也就是说,视图决定了界面到底长什么样子,在java中可通过jsp来充当视图,或者通过纯html的方式进行展现,而后者才是目前的主流。模型和视图需要通过控制器来进行粘合,例如,用户发送一个http请求,此时该请求首先会进入控制器,然后控制器去获取数据并将其封装为模型,最后将模型传递到视图中进行展现。综上所述,mvc的交互过程如下图所示。
2. mvc 模式的优点与不足
mvc模式早在上个世纪70年代就诞生了,直到今天它依然存在,可见生命力相当之强。mvc模式最早应用于smalltalk语言中,最后在其它许多开发语言中都得到了很好的应用。随着包括struts、spring mvc在内的mvc框架的出现,mvc模式真正落地,并使得开发更加高效、代码耦合度尽量减小、应用程序各部分的职责更加清晰。
既然mvc模式这么好,难道它就没有不足的地方吗?我认为mvc至少有以下三点不足:
每次请求必须经过“控制器->模型->视图”这个流程,用户才能看到最终的展现的界面,这个过程似乎有些复杂;
实际上视图是依赖于模型的,换句话说,如果没有模型,视图也无法呈现出最终的效果;
渲染视图的过程是在服务端来完成的,最终呈现给浏览器的是带有模型的视图页面,性能无法得到很好的优化。
为了使数据展现过程更加直接,并且提供更好的用户体验,我们有必要对mvc模式进行改进。不妨这样来尝试:首先从浏览器发送ajax请求,然后服务端接受该请求并返回json数据返回给浏览器,最后在浏览器中进行界面渲染。改进后的mvc模式如下图所示:
也就是说,我们输入的是ajax请求,输出的是json数据,市面上有这样的技术来实现这个功能吗?答案是rest。
rest全称是representational state transfer(表述性状态转移),它是roy fielding博士在2000年写的一篇关于软件架构风格的论文,此文一出,威震四方!国内外许多知名互联网公司纷纷开始采用这种轻量级的web服务,大家习惯将其称为restful web services,或简称rest服务。]
如果将浏览器这一端视为前端,而服务器那一端视为后端的话,可以将以上改进后的mvc模式简化为以下前后端分离模式,如下图所示:
可见,采用rest分格的架构可以使得前端关注界面展现,后端关注业务逻辑,分工明确,职责清晰。那么,如何使用rest架构将应用程序进行前后端分离呢?我们接下来继续探讨,首先我们需要认识rest。
rest本质上是使用url来访问资源的一种方式。众所周知,url(uniform resoure locator:统一资源定位器)就是我们平常使用的请求地址了,其中包括两部分:请求方式与请求路径,比较常见的请求方式是get与post,但在rest中又提出了几种其它类型的请求方式,汇总起来有六种:get、post、put、delete、head、options。尤其是前四种,正好与crud(create-retrieve-update-delete,增删改查)四种操作相对应,例如,get(查)、post(增)、put(改)、delete(删),这正是rest与crud的异曲同工之妙!需要强调的是,rest是面向资源(roa)的,这里提到的资源,实际上就是我们常说的领域对象,在系统设计过程中,我们经常通过领域对象来进行数据建模。
rest是一个无状态的架构模式,因为在任何时候都可以由客户端发出请求到服务端,最终返回自己想要的数据,当前请求不会受到上次请求的影响。也就是说,服务端将内部资源发布rest服务,客户端通过url来定位这些资源并通过http协议来访问它们。下面我们举几个例子对rest请求进行简单描述:
可见,请求路径相同但请求方式不同,所代表的业务操作也不同,例如,/advertiser/1这个请求,带有get、put、delete三种不同的请求方式,对应三种不同的业务操作。
虽然rest看起来还是很简单的,实际上我们往往需要提供一个rest框架,让其实现前后端分离架构,让开发人员将精力集中在业务上,而并非那些具体的技术细节。下面我们将使用java技术来实现这个rest框架,整体框架会基于spring进行开发。
1、统一响应结构
使用rest框架实现前后端分离架构,我们需要首先确定返回的json响应结构是统一的,也就是说,每个rest请求将返回相同结构的json响应结构。不妨定义一个相对通用的json响应结构,其中包含两部分:元数据与返回值,其中,元数据表示操作是否成功与返回值消息等,返回值对应服务端方法所返回的数据。该json响应结构如下:
{ "meta": { "success": true, "message": "ok" }, "data": ... }
为了在框架中映射以上json响应结构,我们需要编写一个response类与其对应:
/** * title: 统一响应结构 * description:使用rest框架实现前后端分离架构,我们需要首先确定返回的json响应结构是统一的, * 也就是说,每个rest请求将返回相同结构的json响应结构。不妨定义一个相对通用的json响应结构,其 * 中包含两部分:元数据与返回值,其中,元数据表示操作是否成功与返回值消息等,返回值对应服务端方法所返回的数据。 * { "meta": { "success": true, "message": "ok" }, "data": ... } * * @author rico * @created 2017年7月4日 下午5:06:00 */ public class response { private static final string ok = "ok"; private static final string error = "error"; private meta meta; // 元数据 private object data; // 响应内容 public response success() { this.meta = new meta(true, ok); return this; } public response success(object data) { this.meta = new meta(true, ok); this.data = data; return this; } public response failure() { this.meta = new meta(false, error); return this; } public response failure(string message) { this.meta = new meta(false, message); return this; } public meta getmeta() { return meta; } public object getdata() { return data; } /** * title: 请求元数据 * @author rico * @created 2017年7月4日 下午5:08:12 */ public class meta { private boolean success; private string message; public meta(boolean success) { this.success = success; } public meta(boolean success, string message) { this.success = success; this.message = message; } public boolean issuccess() { return success; } public string getmessage() { return message; } } }
以上response类包括两类通用返回值消息:ok 与 error,还包括两个常用的操作方法:success( )与failure( ),通过一个内部类来展现元数据结构,我们在下文中多次会使用该response类。
实现该rest框架需要考虑许多问题,首当其冲的就是前后数据流转问题,即http消息与java对象之间的转化问题。
2、前后台数据流转
前后台数据流转问题具体指的是什么?不妨通过一些例子进行说明。比如,通过浏览器发送了一个普通的http请求,该请求携带了一个json格式的参数,在服务端需要将该json参数转换为普通的java对象;再比如,在服务端获取了数据,此时该数据是一个普通的java对象,然后需要将这个java对象转换为json字符串,并将其返回到浏览器中进行渲染,这个过程就涉及http消息与java对象之间的转化问题。
实际上,spring mvc已经为我们提供了这类转化特性,只需在controller的方法参数中使用@requestbody注解定义需要转化的参数即可;类似地,若需要对controller的方法返回值进行转化,则需要在该返回值上使用@responsebody注解来定义,如以下代码片段:
@requestmapping("/users") public class usercontroller { private userservice userservice; /** log4j日志处理(@author: rico) */ private static final logger log = logger.getlogger(usercontroller.class); public userservice getuserservice() { return userservice; } @resource(name = "userservice") public void setuserservice(userservice userservice) { this.userservice = userservice; } @requestmapping(value = "/user", method = requestmethod.put, produces = "application/json", consumes = "application/json") @responsebody // 将 java 对象转化为特定的http消息 public user adduser(@requestbody user user) { // 将接收到的http消息转化为java对象 userservice.adduser(user); log.debug("添加用户 :" + user); return user; } }
当然,@responsebody注解也可以定义在类上,这样所有的方法都继承了该特性。由于经常会使用到@responsebody注解,所以spring提供了一个名为@restcontroller的注解来取代以上的@controller注解,这样我们就可以省略返回值前面的@responsebody注解了,但参数前面的@requestbody注解是无法省略的。实际上,看看spring中对应@restcontroller注解的源码便可知晓:
@target({elementtype.type}) @retention(retentionpolicy.runtime) @documented @controller @responsebody public @interface restcontroller { string value() default ""; }
可见,@restcontroller注解已经被@controller与@responsebody注解定义过了,spring框架会识别这类注解。需要注意的是,该特性在spring 4.0中才引入。因此,我们可将以上代码进行如下改写:
@restcontroller @requestmapping("/users") public class usercontroller { private userservice userservice; /** log4j日志处理(@author: rico) */ private static final logger log = logger.getlogger(usercontroller.class); public userservice getuserservice() { return userservice; } @resource(name = "userservice") public void setuserservice(userservice userservice) { this.userservice = userservice; } @requestmapping(value = "/user", method = requestmethod.put, produces = "application/json", consumes = "application/json") public user adduser(@requestbody user user) { // 将接收到的http消息转化为java对象 userservice.adduser(user); log.debug("添加用户 :" + user); return user; } }
除了使用注解来定义消息转化行为以外,我们还需要添加jackson包进行支持,maven依赖如下:
<!-- json: jackson --> <dependency> <groupid>org.codehaus.jackson</groupid> <artifactid>jackson-core-asl</artifactid> <version>1.9.12</version> </dependency> <dependency> <groupid>org.codehaus.jackson</groupid> <artifactid>jackson-mapper-lgpl</artifactid> <version>1.9.12</version> </dependency>
在spring配置文件中添加以下配置即可:
<!-- 该配置会自动注册requestmappinghandlermapping与requestmappinghandleradapter两个bean, 这是springmvc为@controllers分发请求所必需的,并提供了数据绑定支持、@numberformatannotation支持、 @datetimeformat支持、@valid支持、读写xml的支持和读写json的支持等功能。 --> <mvc:annotation-driven />
通过以上过程,我们已经完成了一个基于springmvc的rest框架,只不过该框架还非常单薄,还缺乏很多关键性特性,尤其是异常处理。
3、处理异常行为
在spring mvc中,我们可以使用aop技术,编写一个全局的异常处理切面类,用它来统一处理所有的异常行为,在spring 3.2中才开始提供。使用很简单,只需定义一个类,并通过@controlleradvice注解将其标注即可,同时需要使用@responsebody注解表示返回值可序列化为json字符串。代码如下:
/** * title: 全局异常处理切面 * description: 利用 @controlleradvice + @exceptionhandler 组合处理controller层runtimeexception异常 * @author rico * @created 2017年7月4日 下午4:29:07 */ @controlleradvice // 控制器增强 @responsebody public class exceptionaspect { /** log4j日志处理(@author: rico) */ private static final logger log = logger.getlogger(exceptionaspect.class); /** * 400 - bad request */ @responsestatus(httpstatus.bad_request) @exceptionhandler(httpmessagenotreadableexception.class) public response handlehttpmessagenotreadableexception( httpmessagenotreadableexception e) { log.error("could_not_read_json...", e); return new response().failure("could_not_read_json"); } /** * 400 - bad request */ @responsestatus(httpstatus.bad_request) @exceptionhandler({methodargumentnotvalidexception.class}) public response handlevalidationexception(methodargumentnotvalidexception e) { log.error("parameter_validation_exception...", e); return new response().failure("parameter_validation_exception"); } /** * 405 - method not allowed。httprequestmethodnotsupportedexception * 是servletexception的子类,需要servlet api支持 */ @responsestatus(httpstatus.method_not_allowed) @exceptionhandler(httprequestmethodnotsupportedexception.class) public response handlehttprequestmethodnotsupportedexception( httprequestmethodnotsupportedexception e) { log.error("request_method_not_supported...", e); return new response().failure("request_method_not_supported"); } /** * 415 - unsupported media type。httpmediatypenotsupportedexception * 是servletexception的子类,需要servlet api支持 */ @responsestatus(httpstatus.unsupported_media_type) @exceptionhandler({ httpmediatypenotsupportedexception.class }) public response handlehttpmediatypenotsupportedexception(exception e) { log.error("content_type_not_supported...", e); return new response().failure("content_type_not_supported"); } /** * 500 - internal server error */ @responsestatus(httpstatus.internal_server_error) @exceptionhandler(tokenexception.class) public response handletokenexception(exception e) { log.error("token is invaild...", e); return new response().failure("token is invaild"); } /** * 500 - internal server error */ @responsestatus(httpstatus.internal_server_error) @exceptionhandler(exception.class) public response handleexception(exception e) { log.error("internal server error...", e); return new response().failure("internal server error"); } }
可见,在exceptionadvice类中包含一系列的异常处理方法,每个方法都通过@responsestatus注解定义了响应状态码,此外还通过@exceptionhandler注解指定了具体需要拦截的异常类。以上过程只是包含了一部分的异常情况,若需处理其它异常,可添加方法具体的方法。需要注意的是,在运行时从上往下依次调用每个异常处理方法,匹配当前异常类型是否与@exceptionhandler注解所定义的异常相匹配,若匹配,则执行该方法,同时忽略后续所有的异常处理方法,最终会返回经json序列化后的response对象。
4、支持参数验证
我们回到上文所提到的示例,这里处理一个普通的put请求,代码如下:
@restcontroller @requestmapping("/users") public class usercontroller { private userservice userservice; /** log4j日志处理(@author: rico) */ private static final logger log = logger.getlogger(usercontroller.class); public userservice getuserservice() { return userservice; } @resource(name = "userservice") public void setuserservice(userservice userservice) { this.userservice = userservice; } @requestmapping(value = "/user", method = requestmethod.put, produces = "application/json", consumes = "application/json") public user adduser(@requestbody user user) { // 将接收到的http消息转化为java对象 userservice.adduser(user); log.debug("添加用户 :" + user); return user; } ... }
其中,user参数包含若干属性,通过以下类结构可见,它是一个传统的pojo:
public class user implements serializable{ private static final long serialversionuid = 1l; private int id; private string uname; private string passwd; private string gentle; private string email; private string city; public user() { super(); } // getter/setter // tostring }
如果业务上需要确保user对象的uname属性必填,如何实现呢?若将这类参数验证的代码写死在controller中,势必会与正常的业务逻辑搅在一起,导致责任不够单一,违背于“单一责任原则”。建议将其参数验证行为从controller中剥离出来,放到另外的类中,这里仅通过@valid注解来定义uname参数,并通过bean validation的参考实现hibernate validator的@notempty注解来定义user类中的uname属性,就像下面这样:
@restcontroller @requestmapping("/users") public class usercontroller { private userservice userservice; /** log4j日志处理(@author: rico) */ private static final logger log = logger.getlogger(usercontroller.class); public userservice getuserservice() { return userservice; } @resource(name = "userservice") public void setuserservice(userservice userservice) { this.userservice = userservice; } @requestmapping(value = "/user", method = requestmethod.put, produces = "application/json", consumes = "application/json") public user adduser(@requestbody @valid user user) { // 将接收到的http消息转化为java对象 userservice.adduser(user); log.debug("添加用户 :" + user); return user; } ... } public class user implements serializable{ private static final long serialversionuid = 1l; private int id; @notempty private string uname; private string passwd; private string gentle; private string email; private string city; public user() { super(); } // getter/setter // tostring }
这里的@valid注解实际上是validation bean规范提供的注解,该规范已由hibernate validator框架实现,因此需要添加以下maven依赖到pom.xml文件中:
<dependency> <groupid>org.hibernate</groupid> <artifactid>hibernate-validator</artifactid> <version>${hibernate-validator.version}</version> </dependency>
需要注意的是,hibernate validator与hibernate没有任何依赖关系,唯一有联系的只是都属于jboss公司的开源项目而已。然后,我们需要在spring配置文件中开启该特性,需添加如下配置:
<bean class="org.springframework.validation.beanvalidation.methodvalidationpostprocessor"/>
最后,我们在全局异常处理类中添加对参数验证异常的处理方法,代码如下:
@controlleradvice @responsebody public class exceptionadvice { /** * 400 - bad request */ @responsestatus(httpstatus.bad_request) @exceptionhandler(validationexception.class) public response handlevalidationexception(validationexception e) { logger.error("参数验证失败", e); return new response().failure("validation_exception"); } }
至此,rest框架已集成了bean validation特性,我们可以使用各种注解来完成所需的参数验证行为了。
看似该框架可以在本地成功跑起来,整个架构包含两个应用,前端应用提供纯静态的html页面,后端应用发布rest api,前端需要通过ajax调用后端发布的rest api,然而ajax是不支持跨域访问的,也就是说,前后端两个应用必须在同一个域名下才能访问。这是非常严重的技术障碍,一定需要找到解决方案。
5、解决跨域问题
比如,前端应用为静态站点且部署在域下,后端应用发布rest api并部署在域下,如何使前端应用通过ajax跨域访问后端应用呢?这需要使用到cors技术来实现,这也是目前最好的解决方案了。
cors全称为cross origin resource sharing(跨域资源共享),服务端只需添加相关响应头信息,即可实现客户端发出ajax跨域请求。
cors技术非常简单,易于实现,目前绝大多数浏览器均已支持该技术(ie8浏览器也支持了),服务端可通过任何编程语言来实现,只要能将cors响应头写入response对象中即可。
下面我们继续扩展rest框架,通过cors技术实现ajax跨域访问。首先,我们需要编写一个filter,用于过滤所有的http请求,并将cors响应头写入response对象中,代码如下:
/** * title: 跨域访问处理(跨域资源共享) * description: 解决前后端分离架构中的跨域问题 * @author rico * @created 2017年7月4日 下午5:00:09 */ public class corsfilter implements filter { /** log4j日志处理(@author: rico) */ 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"); } /** * @description 通过cors技术实现ajax跨域访问,只要将cors响应头写入response对象中即可 * @author rico * @created 2017年7月4日 下午5:02:38 * @param req * @param res * @param chain * @throws ioexception * @throws servletexception * @see javax.servlet.filter#dofilter(javax.servlet.servletrequest, javax.servlet.servletresponse, javax.servlet.filterchain) */ @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:允许访问的客户端域名,例如:,若为*,则表示从任意域都能访问,即不做任何限制;
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>
完成以上过程即可实现ajax跨域功能了,但似乎还存在另外一个问题,由于rest是无状态的,后端应用发布的rest api可在用户未登录的情况下被任意调用,这显然是不安全的,如何解决这个问题呢?我们需要为rest请求提供安全机制。
6、提供安全机制
解决rest安全调用问题,可以做得很复杂,也可以做得特简单,可按照以下过程提供rest安全机制:
(1). 当用户登录成功后,在服务端生成一个token,并将其放入内存中(可放入jvm或redis中),同时将该token返回到客户端;
(2). 在客户端中将返回的token写入cookie中,并且每次请求时都将token随请求头一起发送到服务端;
(3). 提供一个aop切面,用于拦截所有的controller方法,在切面中判断token的有效性;
(4). 当登出时,只需清理掉cookie中的token即可,服务端token可设置过期时间,使其自行移除。
首先,我们需要定义一个用于管理token的接口,包括创建token与检查token有效性的功能。代码如下:
/** * title: rest 鉴权 * description: 登录用户的身份鉴权 * @author rico * @created 2017年7月4日 下午4:41:43 */ public interface tokenmanager { string createtoken(string username); boolean checktoken(string token); void deletetoken(string token); }
然后,我们可提供一个简单的tokenmanager实现类,将token存储到jvm内存中。代码如下:
/** * title: tokenmanager的默认实现 * description: 管理 token * @author rico * @created 2017年7月4日 下午4:41:32 */ public class defaulttokenmanager implements tokenmanager { /** 将token存储到jvm内存(concurrenthashmap)中 (@author: rico) */ private static map<string, string> tokenmap = new concurrenthashmap<string, string>(); /** * @description 利用uuid创建token(用户登录时,创建token) * @author rico * @created 2017年7月4日 下午4:46:46 * @param username * @return * @see cn.edu.tju.rico.authorization.tokenmanager#createtoken(java.lang.string) */ public string createtoken(string username) { string token = codecutil.createuuid(); tokenmap.put(token, username); return token; } /** * @description token验证(用户登录验证) * @author rico * @created 2017年7月4日 下午4:46:50 * @param token * @return * @see cn.edu.tju.rico.authorization.tokenmanager#checktoken(java.lang.string) */ public boolean checktoken(string token) { return !stringutil.isempty(token) && tokenmap.containskey(token); } /** * @description token删除(用户登出时,删除token) * @author rico * @created 2017年7月4日 下午4:46:54 * @param token * @see cn.edu.tju.rico.authorization.tokenmanager#deletetoken(java.lang.string) */ @override public void deletetoken(string token) { // todo auto-generated method stub tokenmap.remove(token); } }
需要注意的是,如果需要做到分布式集群,建议基于redis提供一个实现类,将token存储到redis中,并利用redis与生俱来的特性,做到token的分布式一致性。
然后,我们可以基于spring aop写一个切面类,用于拦截controller类的方法,并从请求头中获取token,最后对token有效性进行判断。代码如下:
/** * title:安全检查切面(是否登录检查) * description: 通过验证token维持登录状态 * * @author rico * @created 2017年7月4日 下午4:32:34 */ @component @aspect public class securityaspect { /** log4j日志处理(@author: rico) */ 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>
本项目是一个使用maven进行构建的项目,关于maven的了解、使用推荐大家看孤傲苍狼的一系列博客;
关于rest服务的调试推荐大家使用postman这款工具,请大家自行下载与安装,具体见;
本文从经典的mvc模式开始,对mvc模式是什么以及该模式存在的不足进行了简述。然后引出了如何对mvc模式的改良,让其转变为前后端分离架构,以及解释了为何要进行前后端分离。最后通过rest服务将前后端进行解耦,并提供了一款基于java的rest框架的主要实现过程,尤其是需要注意的核心技术问题及其解决方案。希望本文对正在探索前后端分离的读者们有所帮助,期待与大家共同探讨。
本项目的全部完整源码可以在我的github上找到,项目名为restspringmvcdemo,项目地址为:https://github.com/githubofrico/restspringmvcdemo。
更多关于rest的介绍,请移步我的博文。
更多关于 java web 方面的内容,请关注我的专栏 《java web 成神之路》。本专栏全面记录了java web开发相关知识,不但包括对http, servlet,session等基础知识的讲解,还包括对流行框架(ssm,springmvc等)、中间件(redis等)等进阶知识的深入分析。笔者将持续跟进最新web技术,期望对大家能够起到抛砖引玉的效果。
引用
如对本文有疑问, 点击进行留言回复!!
Mybatis PersistenceException异常:Error building SqlSession The error may exist in SQL Mapper Configur
MyBatis——动态SQL语句——if标签和where标签复合使用
网友评论