当前位置: 移动技术网 > IT编程>开发语言>Java > REST风格框架实战:从MVC到前后端分离(附完整Demo)

REST风格框架实战:从MVC到前后端分离(附完整Demo)

2018年10月15日  | 移动技术网IT编程  | 我要评论
版权声明:欢迎转载,注明作者和出处就好!如果不喜欢或文章存在明显的谬误,请留言说明原因再踩哦,谢谢,我也可以知道原因,不断进步! https://blog.csdn.net/justloveyou_/article/details/74379479

摘要:

  本人在前辈《从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


一. mvc 概述

1. 理解 mvc

  mvc是一种经典的设计模式,全名为model-view-controller,即模型-视图-控制器。其中,模型是用于封装数据的载体,例如,在java中一般通过一个简单的pojo(plain ordinary java object)来表示,其本质是一个普通的java bean,包含一系列的成员变量及其getter/setter方法。对于视图而言,它更加偏重于展现,也就是说,视图决定了界面到底长什么样子,在java中可通过jsp来充当视图,或者通过纯html的方式进行展现,而后者才是目前的主流。模型和视图需要通过控制器来进行粘合,例如,用户发送一个http请求,此时该请求首先会进入控制器,然后控制器去获取数据并将其封装为模型,最后将模型传递到视图中进行展现。综上所述,mvc的交互过程如下图所示。

              mvc.jpg-21.5kb


2. mvc 模式的优点与不足

  mvc模式早在上个世纪70年代就诞生了,直到今天它依然存在,可见生命力相当之强。mvc模式最早应用于smalltalk语言中,最后在其它许多开发语言中都得到了很好的应用。随着包括struts、spring mvc在内的mvc框架的出现,mvc模式真正落地,并使得开发更加高效、代码耦合度尽量减小、应用程序各部分的职责更加清晰。

  既然mvc模式这么好,难道它就没有不足的地方吗?我认为mvc至少有以下三点不足:

  • 每次请求必须经过“控制器->模型->视图”这个流程,用户才能看到最终的展现的界面,这个过程似乎有些复杂;

  • 实际上视图是依赖于模型的,换句话说,如果没有模型,视图也无法呈现出最终的效果;

  • 渲染视图的过程是在服务端来完成的,最终呈现给浏览器的是带有模型的视图页面,性能无法得到很好的优化。

      为了使数据展现过程更加直接,并且提供更好的用户体验,我们有必要对mvc模式进行改进。不妨这样来尝试:首先从浏览器发送ajax请求,然后服务端接受该请求并返回json数据返回给浏览器,最后在浏览器中进行界面渲染。改进后的mvc模式如下图所示:

                  mvc+前后端分离.jpg-24.2kb

      也就是说,我们输入的是ajax请求,输出的是json数据,市面上有这样的技术来实现这个功能吗?答案是rest。

      rest全称是representational state transfer(表述性状态转移),它是roy fielding博士在2000年写的一篇关于软件架构风格的论文,此文一出,威震四方!国内外许多知名互联网公司纷纷开始采用这种轻量级的web服务,大家习惯将其称为restful web services,或简称rest服务。]

      如果将浏览器这一端视为前端,而服务器那一端视为后端的话,可以将以上改进后的mvc模式简化为以下前后端分离模式,如下图所示:

                    前后端分离.jpg-24.1kb

      可见,采用rest分格的架构可以使得前端关注界面展现,后端关注业务逻辑,分工明确,职责清晰。那么,如何使用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请求进行简单描述:

                rest例子.jpg-41.7kb

  可见,请求路径相同但请求方式不同,所代表的业务操作也不同,例如,/advertiser/1这个请求,带有get、put、delete三种不同的请求方式,对应三种不同的业务操作。

  虽然rest看起来还是很简单的,实际上我们往往需要提供一个rest框架,让其实现前后端分离架构,让开发人员将精力集中在业务上,而并非那些具体的技术细节。下面我们将使用java技术来实现这个rest框架,整体框架会基于spring进行开发。


三. 实现rest框架

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>  
  • 1
  • 2
  • 3
  • 4
  • 5

  需要注意的是,hibernate validator与hibernate没有任何依赖关系,唯一有联系的只是都属于jboss公司的开源项目而已。然后,我们需要在spring配置文件中开启该特性,需添加如下配置:

<bean class="org.springframework.validation.beanvalidation.methodvalidationpostprocessor"/>  
  • 1

  最后,我们在全局异常处理类中添加对参数验证异常的处理方法,代码如下:

@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>  

四. 关于demo部署的若干建议

  本项目是一个使用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技术,期望对大家能够起到抛砖引玉的效果。


引用

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网