从 MVC 到先后端分离

转载自:https://my.oschina.net/huangyong/blog/521891前端

从MVC到先后端分离web

1.理解 MVCspring

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语言中,最后在其它许多开发语言中都获得了很好的应用,例如,Java 中的 Struts、Spring MVC等框架。正是由于这些MVC框架的出现,才让MVC模式真正落地,让开发更加高效,让代码耦合度尽可能减少,让应用程序各部分的职责更加清晰。
既然MVC模式这么好,难道它就没有不足的地方吗?我认为MVC至少有如下三点不足:
1)每次请求必须通过“控制器->模型->视图”这个流程,用户才能看到最终的展示的界面,这个过程彷佛有些复杂。
2)实际上视图是依赖于模型的,换句话说,若是没有模型,视图也没法呈现出最终的效果。
3)渲染视图的过程是在服务端来完成的,最终呈现给浏览器的是带有模型的视图页面,性能没法获得很好的优化。
为了使数据展示过程更加直接,而且提供更好的用户体验,咱们有必要对MVC模式进行改进。不妨这样来尝试,首先从浏览器发送 AJAX 请求,而后服务端接受该请求并返回 JSON 数据返回给浏览器,最后在浏览器中进行界面渲染。
改进后的 MVC 模式以下图所示:编程

 

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

REST 全称是 Representational State Transfer(表述性状态转移),它是 Roy Fielding 博士在 2000 年写的一篇关于软件架构风格的论文,此文一出,威震四方!国内外许多知名互
联网公司纷纷开始采用这种轻量级的 Web 服务,你们习惯将其称为 RESTful Web Services,或简称 REST 服务。

若是将浏览器这一端视为前端,而服务器那一端视为后端的话,能够将以上改进后的MVC模式简化为如下先后端分离模式:后端

 

可见,有了REST服务,前端关注界面展示,后端关注业务逻辑,分工明确,职责清晰。那么,如何使用 REST 服务将应用程序进行先后端分离呢?咱们接下来继续探讨,首先咱们须要认识 REST。设计模式

3 认识 RESTapi

REST本质上是使用 URL 来访问资源种方式。众所周知,URL 就是咱们日常使用的请求地址了,其中包括两部分:请求方式请求路径,比较常见的请求方式是GET与POST,但在REST中又提出了几种其它类型的请求方式,汇总起来有六种:GET、POST、PUT、DELETE、HEAD、OPTIONS。尤为是前四种,正好与 CRUD(Create-Retrieve-Update-Delete,增删改查)四种操做相对应,例如,GET(查)、POST(增)、PUT(改)、DELETE(删),这正是 REST 与 CRUD 的殊途同归之妙!须要强调的是,REST 是“面向资源”的,这里提到的资源,实际上就是咱们常说的领域对象,在系统设计过程当中,咱们常常经过领域对象来进行数据建模。跨域

REST 是一个“无状态”的架构模式,由于在任什么时候候均可以由客户端发出请求到服务端,最终返回本身想要的数据,当前请求不会受到上次请求的影响。也就是说,服务端将内部资源发布REST服务,客户端经过URL来访问这些资源,这不就是SOA所提倡的“面向服务”的思想吗?因此,REST也被人们看作是一种“轻量级”的 SOA 实现技术,所以在企业级应用与互联网应用中都获得了普遍应用。 

下面咱们举几个例子对 REST 请求进行简单描述:

 

REST 请求 描述
GET:/advertisers 获取全部的广告主
GET:/advertiser/1 获取 ID 为 1 的广告主
PUT:/advertiser/1 更新 ID 为 1 的广告主
DELETE:/advertiser/1 删除 ID 为 1 的广告主
POST:/advertiser 建立广告主

可见,请求路径相同,但请求方式不一样,所表明的业务操做也不一样,例如,/advertiser/1 这个请求,带有 GET、PUT、DELETE 三种不一样的请求方式,对应三种不一样的业务操做。

虽然 REST 看起来仍是很简单的,实际上咱们每每须要提供一个 REST 框架,让其实现先后端分离架构,让开发人员将精力集中在业务上,而并不是那些具体的技术细节。下面咱们将使用 Java 技术来实现这个 REST 框架,总体框架会基于 Spring 进行开发。

4 实现 REST 框架

4.1 统一响应结构

使用 REST 框架实现先后端分离架构,咱们须要首先肯定返回的JSON响应结构是统一的,也就是说,每一个REST请求将返回相同结构的JSON响应结构。不妨定义一个相对通用的 JSON 响应结构,其中包含两部分:元数据返回值,其中,元数据表示操做是否成功与返回值消息等,返回值对应服务端方法所返回的数据。该 JSON 响应结构以下: 

{ "meta": { "success": true, "message": "ok" }, "data": ... }

为了在框架中映射以上 JSON 响应结构,咱们须要编写一个 Response 类与其对应:

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; } 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 框架须要考虑许多问题,首当其冲的就是对象序列化问题。 

4.2 实现对象序列化

想要解释什么是 对象序列化?不妨经过一些例子进行说明。好比,在服务端从数据库中获取了数据,此时该数据是一个普通的 Java 对象,而后须要将这个Java 对象转换为JSON 字符串,并将其返回到浏览器中进行渲染,这个转换过程称为序列化;再好比,经过浏览器发送了一个普通的 HTTP 请求,该请求携带了一个 JSON 格式的参数,在服务端须要将该 JSON 参数转换为普通的 Java 对象,这个转换过程称为反序列化。无论是序列化仍是反序列化,咱们通常都称为序列化。

实际上,Spring MVC 已经为咱们提供了这类序列化特性,只需在 Controller 的方法参数中使用 @RequestBody 注解定义须要反序列化的参数便可,如如下代码片断:

@Controller public class AdvertiserController { @RequestMapping(value = "/advertiser", method = RequestMethod.POST) public Response createAdvertiser(@RequestBody AdvertiserParam advertiserParam) { ... } }

若须要对 Controller 的方法返回值进行序列化,则须要在该返回值上使用 @ResponseBody 注解来定义,如如下代码片断:

@Controller public class AdvertiserController { @RequestMapping(value = "/advertiser/{id}", method = RequestMethod.GET) public @ResponseBody Response getAdvertiser(@PathVariable("id") String advertiserId) { ... } }

固然,@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 public class AdvertiserController { @RequestMapping(value = "/advertiser", method = RequestMethod.POST) public Response createAdvertiser(@RequestBody AdvertiserParam advertiserParam) { ... } @RequestMapping(value = "/advertiser/{id}", method = RequestMethod.GET) public Response getAdvertiser(@PathVariable("id") String advertiserId) { ... } }

除了使用注解来定义序列化行为之外,咱们还须要使用 Jackson 来提供 JSON 的序列化操做,在 Spring 配置文件中只需添加如下配置便可:

<mvc:annotation-driven>
    <mvc:message-converters>
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
    </mvc:message-converters>
</mvc:annotation-driven>

若须要对 Jackson 的序列化行为进行定制,好比,排除值为空属性、进行缩进输出、将驼峰转为下划线、进行日期格式化等,这又如何实现呢?

首先,咱们须要扩展 Jackson 提供的 ObjectMapper 类,代码以下:

public class CustomObjectMapper extends ObjectMapper { private boolean camelCaseToLowerCaseWithUnderscores = false; private String dateFormatPattern; public void setCamelCaseToLowerCaseWithUnderscores(boolean camelCaseToLowerCaseWithUnderscores) { this.camelCaseToLowerCaseWithUnderscores = camelCaseToLowerCaseWithUnderscores; } public void setDateFormatPattern(String dateFormatPattern) { this.dateFormatPattern = dateFormatPattern; } public void init() { // 排除值为空属性
 setSerializationInclusion(JsonInclude.Include.NON_NULL); // 进行缩进输出
        configure(SerializationFeature.INDENT_OUTPUT, true); // 将驼峰转为下划线
        if (camelCaseToLowerCaseWithUnderscores) { setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES); } // 进行日期格式化
        if (StringUtil.isNotEmpty(dateFormatPattern)) { DateFormat dateFormat = new SimpleDateFormat(dateFormatPattern); setDateFormat(dateFormat); } } }

而后,将 CustomObjectMapper 注入到 MappingJackson2HttpMessageConverter 中,Spring 配置以下:

bean id="objectMapper" class="com.xxx.api.json.CustomObjectMapper" init-method="init"> <property name="camelCaseToLowerCaseWithUnderscores" value="true"/>
    <property name="dateFormatPattern" value="yyyy-MM-dd HH:mm:ss"/>
</bean>

<mvc:annotation-driven>
    <mvc:message-converters>
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
            <property name="objectMapper" ref="objectMapper"/>
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

经过以上过程,咱们已经完成了一个基于 Spring MVC 的 REST 框架,只不过该框架还很是单薄,还缺少不少关键性特性,尤为是异常处理。

4.3 处理异常行为

在 Spring MVC 中,咱们可使用 AOP 技术,编写一个全局的异常处理切面类,用它来统一处理全部的异常行为,在 Spring 3.2 中才开始提供。使用法很简单,只需定义一个类,并经过 @ControllerAdvice 注解将其标注便可,同时须要使用 @ResponseBody 注解表示返回值可序列化为 JSON 字符串。代码以下:

@ControllerAdvice @ResponseBody public class ExceptionAdvice { /** * 400 - Bad Request */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(HttpMessageNotReadableException.class) public Response handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { logger.error("参数解析失败", e); return new Response().failure("could_not_read_json"); } /** * 405 - Method Not Allowed */ @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public Response handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { logger.error("不支持当前请求方法", e); return new Response().failure("request_method_not_supported"); } /** * 415 - Unsupported Media Type */ @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE) @ExceptionHandler(HttpMediaTypeNotSupportedException.class) public Response handleHttpMediaTypeNotSupportedException(Exception e) { logger.error("不支持当前媒体类型", e); return new Response().failure("content_type_not_supported"); } /** * 500 - Internal Server Error */ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(Exception.class) public Response handleException(Exception e) { logger.error("服务运行异常", e); return new Response().failure(e.getMessage()); } }

可见,在 ExceptionAdvice 类中包含一系列的异常处理方法,每一个方法都经过 @ResponseStatus 注解定义了响应状态码,此外还经过 @ExceptionHandler 注解指定了具体须要拦截的异常类。以上过程只是包含了一部分的异常状况,若需处理其它异常,可添加方法具体的方法。须要注意的是,在运行时从上往下依次调用每一个异常处理方法,匹配当前异常类型是否与 @ExceptionHandler 注解所定义的异常相匹配,若匹配,则执行该方法,同时忽略后续全部的异常处理方法,最终会返回经 JSON 序列化后的 Response 对象。

4.4 支持参数验证

咱们回到上文所提到的示例,这里处理一个普通的 POST 请求,代码以下:

@RestController public class AdvertiserController { @RequestMapping(value = "/advertiser", method = RequestMethod.POST) public Response createAdvertiser(@RequestBody AdvertiserParam advertiserParam) { ... } }

其中,AdvertiserParam 参数包含若干属性,经过如下类结构可见,它是一个传统的 POJO:

public class AdvertiserParam { private String advertiserName; private String description; // 省略 getter/setter 方法
}

若是业务上须要确保 AdvertiserParam 对象的 advertiserName 属性必填,如何实现呢?

若将这类参数验证的代码写死在 Controller 中,势必会与正常的业务逻辑搅在一块儿,致使责任不够单一,违背于“单一责任原则”。建议将其参数验证行为从 Controller 中剥离出来,放到另外的类中,这里仅提供一个 @Valid 注解来定义 AdvertiserParam 参数,并在 AdvertiserParam 类中经过 @NotEmpty 注解来定义 advertiserName 属性,就像下面这样:

@RestController public class AdvertiserController { @RequestMapping(value = "/advertiser", method = RequestMethod.POST) public Response createAdvertiser(@RequestBody @Valid AdvertiserParam advertiserParam) { ... } } public class AdvertiserParam { @NotEmpty private String advertiserName; private String description; // 省略 getter/setter 方法
}

这里的 @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 公司的开源项目而已。

实现 @NotEmpty 注解的功能,咱们须要作如下几件事情。

首先,定义一个 @NotEmpty 注解类,代码以下:

@Documented @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = NotEmptyValidator.class) public @interface NotEmpty { String message() default "not_empty"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }

以上注解类必须包含 message、groups、payload 三个属性,由于这是规范所要求的,此外,须要经过 @Constraint 注解指定一个验证器类,这里对应的是 NotEmptyValidator,其代码以下:

ublic class NotEmptyValidator implements ConstraintValidator<NotEmpty, String> { @Override public void initialize(NotEmpty constraintAnnotation) { } @Override public boolean isValid(String value, ConstraintValidatorContext context) { return StringUtil.isNotEmpty(value); } }

以上验证器类实现了 ConstraintValidator 接口,并在该接口的 isValid( ) 方法中完成了具体的参数验证逻辑。须要注意的是,实现接口时须要指定泛型,第一个参数表示验证注解类型(NotEmpty),第二个参数表示须要验证的参数类型(String)。

而后,咱们须要在 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 是不支持跨域访问的,也就是说,先后端两个应用必须在同一个域名下才能访问。这是很是严重的技术障碍,必定须要找到解决方案。 

4.5 解决跨域问题

好比,前端应用为静态站点且部署在 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 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"); } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (StringUtil.isNotEmpty(allowOrigin)) { List<String> allowOriginList = Arrays.asList(allowOrigin.split(",")); if (CollectionUtil.isNotEmpty(allowOriginList)) { String currentOrigin = request.getHeader("Origin"); 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 的方法:

<filter>
    <filter-name>corsFilter</filter-name>
    <filter-class>com.xxx.api.cors.CorsFilter</filter-class>
    <init-param>
        <param-name>allowOrigin</param-name>
        <param-value>http://web.xxx.com</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</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>corsFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

完成以上过程便可实现 AJAX 跨域功能了,但彷佛还存在另一个问题,因为 REST 是无状态的,后端应用发布的 REST API 可在用户未登陆的状况下被任意调用,这显然是不安全的,如何解决这个问题呢?咱们须要为 REST 请求提供安全机制。

4.6 提供安全机制

解决 REST 安全调用问题,能够作得很复杂,也能够作得特简单,可按照如下过程提供 REST 安全机制: 

  1. 当用户登陆成功后,在服务端生成一个 token,并将其放入内存中(可放入 JVM 或 Redis 中),同时将该 token 返回到客户端。
  2. 在客户端中将返回的 token 写入 cookie 中,而且每次请求时都将 token 随请求头一块儿发送到服务端。
  3. 提供一个 AOP 切面,用于拦截全部的 Controller 方法,在切面中判断 token 的有效性。
  4. 当登出时,只需清理掉 cookie 中的 token 便可,服务端 token 可设置过时时间,使其自行移除。

首先,咱们须要定义一个用于管理 token 的接口,包括建立 token 与检查 token 有效性的功能。代码以下: 

public interface TokenManager { String createToken(String username); boolean checkToken(String token); }

而后,咱们可提供一个简单的 TokenManager 实现类,将 token 存储到 JVM 内存中。代码以下:

public class DefaultTokenManager implements TokenManager { private static Map<String, String> tokenMap = new ConcurrentHashMap<>(); @Override public String createToken(String username) { String token = CodecUtil.createUUID(); tokenMap.put(token, username); return token; } @Override public boolean checkToken(String token) { return !StringUtil.isEmpty(token) && tokenMap.containsKey(token); } }

须要注意的是,若是须要作到分布式集群,建议基于 Redis 提供一个实现类,将 token 存储到 Redis 中,并利用 Redis 与生俱来的特性,作到 token 的分布式一致性。

 而后,咱们能够基于 Spring AOP 写一个切面类,用于拦截 Controller 类的方法,并从请求头中获取 token,最后对 token 有效性进行判断。代码以下:

public class SecurityAspect { private static final String DEFAULT_TOKEN_NAME = "X-Token"; private TokenManager tokenManager; private String tokenName; public void setTokenManager(TokenManager tokenManager) { this.tokenManager = tokenManager; } public void setTokenName(String tokenName) { if (StringUtil.isEmpty(tokenName)) { tokenName = DEFAULT_TOKEN_NAME; } this.tokenName = tokenName; } public Object execute(ProceedingJoinPoint pjp) throws Throwable { // 从切点上获取目标方法
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature(); Method method = methodSignature.getMethod(); // 若目标方法忽略了安全性检查,则直接调用目标方法
        if (method.isAnnotationPresent(IgnoreSecurity.class)) { return pjp.proceed(); } // 从 request header 中获取当前 token
        String token = WebContext.getRequest().getHeader(tokenName); // 检查 token 有效性
        if (!tokenManager.checkToken(token)) { String message = String.format("token [%s] is invalid", token); throw new TokenException(message); } // 调用目标方法
        return pjp.proceed(); } }

若要使 SecurityAspect 生效,则须要添加以下 Spring 配置:

<bean id="securityAspect" class="com.xxx.api.security.SecurityAspect">
    <property name="tokenManager" ref="tokenManager"/>
    <property name="tokenName" value="X-Token"/>
</bean>

<aop:config>
    <aop:aspect ref="securityAspect">
        <aop:around method="execute" pointcut="@annotation(org.springframework.web.bind.annotation.RequestMapping)"/>
    </aop:aspect>
</aop:config>

最后,别忘了在 web.xml 中添加容许的 X-Token 响应头,配置以下:

<init-param>
    <param-name>allowHeaders</param-name>
    <param-value>Content-Type,X-Token</param-value>
</init-param>

5 总结

 本文从经典的 MVC 模式开始,对 MVC 模式是什么以及该模式存在的不足进行了简述。而后引出了如何对 MVC 模式的改良,让其转变为先后端分离架构,以及解释了为什么要进行先后端分离。最后经过 REST 服务将先后端进行解耦,并提供了一款基于 Java 的 REST 框架的主要实现过程,尤为是须要注意的核心技术问题及其解决方案。但愿本文对正在探索先后端分离的读者们有所帮助,期待与你们共同探讨。

相关文章
相关标签/搜索