From MVC to front-end and back-end separation (REST - also considered by individuals to be the more popular and better way at present)

Keywords: REST Spring JSON Java

ABSTRACT: MVC model was born as early as the 1970s, and it still exists today. It can be seen that its vitality is quite strong. MVC pattern was first used in Smalltalk language, and finally it has been well applied in many other development languages, such as Struts, Spring MVC and other frameworks in Java.

1. Understanding MVC

MVC is a classical design pattern, named Model-View-Controller, that is, Model-View-Controller.

Among them, the model is a carrier for encapsulating data, for example, in the Java Usually through a simple POJO (Plain Ordinary) Java Object) to indicate that its essence is ordinary java Bean, which contains a series of member variables and their getter/setter methods. For views, it is more focused on presentation, that is to say, views determine what the interface looks like. In Java, JSP can be used as a view, or through pure HTML, the latter is the current mainstream. Models and views need to be glued by controllers, for example, when a user sends an HTTP request, the request first enters the controller, then the controller obtains data and encapsulates it as a model, and finally the model is transmitted to the view for presentation.

In summary, the interaction process of MVC is shown in Figure 1.

2. Advantages and disadvantages of MVC model

MVC model was born in the 1970s, and it still exists today. It shows that the vitality of MVC model is quite strong. MVC pattern was first used in Smalltalk language, and finally it has been well applied in many other development languages, such as Struts in Java, Struts in Java, etc. spring MVC and other frameworks. It is precisely because of the emergence of these MVC frameworks that the MVC model really landed, making development more efficient, making code coupling as small as possible, and making the responsibilities of various parts of the application more clear.

Since the MVC model is so good, is it not inadequate? I think MVC has at least three shortcomings:

  1. Each request must go through the process of "Controller - > Model - > View" before the user can see the final interface, which seems to be somewhat complicated.
  2. In fact, views depend on models. In other words, without models, views can not show the final effect.
  3. The rendering process of view is accomplished on the server side, and the view page with model is presented to the browser, so the performance can not be well optimized.

In order to make the data presentation process more direct and provide a better user experience, it is necessary to improve the MVC model. Let's try this, first send AJAX request from the browser, then the server accepts the request and returns JSON data to the browser, and finally render the interface in the browser.

The improved MVC pattern is shown in Figure 2.


That is to say, we input AJAX requests and output JSON data. Is there any technology on the market to implement this function? The answer is REST.

The full name of REST is Representational State Transfer. It is a piece about software written by Dr. Roy Fielding in 2000. Framework Style of the paper, this article, Megatron Quartet! Many well-known Internet companies at home and abroad have begun to adopt this lightweight Web service, which is customarily called RESTful. Web Services, or REST Services for short. ]

If the browser side is regarded as the front end and the server side as the back end, the improved MVC mode can be simplified to the following front-end and back-end separation mode, as shown in Figure 3.


Obviously, with REST services, the front-end focuses on the interface display, the back-end focuses on business logic, a clear division of labor, clear responsibilities. So how do you use REST services to separate the front and back ends of your application? Let's move on. First of all, we need to recognize REST.

3. Understanding REST

REST is essentially a way of accessing resources using URLs. As we all know, URL is the request address that we usually use. It includes two parts: request mode and request path. GET and POST are the common request modes, but several other types of request modes are put forward in REST, which can be summarized into six types: GET, POST, PUT, DELETE, HEAD and OPTIONS. Especially the first four operations correspond to CRUD (Create-Retrieve-Update-Delete), such as GET (Check), POST (Add), PUT (Change), DELETE (Delete). These are the similarities and differences between REST and CRUD. It needs to be emphasized that REST is "resource-oriented". The resources mentioned here are actually domain objects, which we often call domain objects. In the process of system design, we often use domain objects to model data.

REST is a "stateless" architecture mode, because at any time the client can send a request to the server, and eventually return the data they want, the current request will not be affected by the previous request. That is to say, the server publishes REST services with internal resources, and the client accesses these resources through the URL. Isn't that the idea of "service-oriented" advocated by SOA? Therefore, REST is also regarded as a "lightweight" SOA implementation technology, so it has been widely used in enterprise applications and Internet applications.

Let's give a few examples to briefly describe REST requests:


It can be seen that the request path is the same, but the request way is different, and the business operation represented is also different. For example, / advertiser/1 request, with GET, PUT, DELETE three different request ways, corresponds to three different business operations.

Although REST seems simple, we often need to provide a REST framework to implement front-end and back-end decoupling architecture, so that developers can focus on business rather than on specific technical details. Next we will use Java technology to implement this REST framework, which will be developed based on Spring.

4. Implementing REST Framework

4.1 Unified Response Structure

Using REST framework to implement front-end and back-end decoupling architecture, we need to first determine that the JSON response structure returned is uniform, that is to say, each REST request will return the same JSON response structure. We may define a relatively general JSON response structure, which contains two parts: metadata and return value, in which metadata indicates whether the operation is successful or not and return value message, and return value corresponds to the data returned by the server-side method. The JSON response structure is as follows:

{
	"meta": {
		"success": true,
		"message": "ok"
	},
	"data": ...
}
In order to map the JSON response structure above in the framework, we need to write a Response class corresponding to it:
  1. public class Response {  
  2.   
  3.     private static final String OK = "ok";  
  4.     private static final String ERROR = "error";  
  5.   
  6.     private Meta meta;  
  7.     private Object data;  
  8.   
  9.     public Response success() {  
  10.         this.meta = new Meta(true, OK);  
  11.         return this;  
  12.     }  
  13.   
  14.     public Response success(Object data) {  
  15.         this.meta = new Meta(true, OK);  
  16.         this.data = data;  
  17.         return this;  
  18.     }  
  19.   
  20.     public Response failure() {  
  21.         this.meta = new Meta(false, ERROR);  
  22.         return this;  
  23.     }  
  24.   
  25.     public Response failure(String message) {  
  26.         this.meta = new Meta(false, message);  
  27.         return this;  
  28.     }  
  29.   
  30.     public Meta getMeta() {  
  31.         return meta;  
  32.     }  
  33.   
  34.     public Object getData() {  
  35.         return data;  
  36.     }  
  37.   
  38.     public class Meta {  
  39.   
  40.         private boolean success;  
  41.         private String message;  
  42.   
  43.         public Meta(boolean success) {  
  44.             this.success = success;  
  45.         }  
  46.   
  47.         public Meta(boolean success, String message) {  
  48.             this.success = success;  
  49.             this.message = message;  
  50.         }  
  51.   
  52.         public boolean isSuccess() {  
  53.             return success;  
  54.         }  
  55.   
  56.         public String getMessage() {  
  57.             return message;  
  58.         }  
  59.     }  
  60. }  
The above Response class includes two types of generic return value messages: ok and error, as well as two commonly used methods of operation: success() and failure(), which are used many times in the following sections to show the metadata structure through an internal class.

Many problems need to be considered to implement the REST framework, and the first one is the problem of object serialization.

4.2 Implementing Object Serialization

Want to explain what object serialization is? It may be illustrated by some examples. For example, a normal HTTP request is sent through a browser, which carries a parameter in JSON format. On the server side, the JSON parameter needs to be converted into a normal Java object. This conversion process is called serialization. For example, when data is retrieved on the server side, the data is a normal Java object, and then the Java object needs to be converted into a JSON string and returned to the browser for rendering. This conversion process is called deserialization. Whether serialization or deserialization, we generally call it serialization.

In fact, Spring MVC has provided us with such serialization features, just using the @RequestBody annotation in Controller's method parameters to define parameters that need to be deserialized, such as the following code snippets:

  1. @Controller  
  2. public class AdvertiserController {  
  3.   
  4.     @RequestMapping(value = "/advertiser", method = RequestMethod.POST)  
  5.     public Response createAdvertiser(@RequestBody AdvertiserParam advertiserParam) {  
  6.         ...  
  7.     }  
  8. }  
If you need to serialize the method return value of Controller, you need to define it with the @ResponseBody annotation, such as the following code snippet:
  1. @Controller  
  2. public class AdvertiserController {  
  3.   
  4.     @RequestMapping(value = "/advertiser/{id}", method = RequestMethod.GET)  
  5.     public @ResponseBody Response getAdvertiser(@PathVariable("id") String advertiserId) {  
  6.         ...  
  7.     }  
  8. }  
Of course, the @ResponseBody annotation can also be defined on classes, so that all methods inherit this feature. Because the @ResponseBody annotation is often used, Spring provides a annotation called @RestController to replace the above @Controller annotation, so that we can omit the @ResponseBody annotation before the return value, but the @RequestBody annotation before the parameter cannot be omitted. In fact, look at the source code for the @RestController annotation in Spring to see that:
  1. @Target({ElementType.TYPE})  
  2. @Retention(RetentionPolicy.RUNTIME)  
  3. @Documented  
  4. @Controller  
  5. @ResponseBody  
  6. public @interface RestController {  
  7.   
  8.     String value() default "";  
  9. }  
As you can see, @RestController annotations have been defined by @Controller and @ResponseBody annotations, and the Spring framework recognizes these annotations. It should be noted that this feature was introduced only in Spring 4.0.

Therefore, we can rewrite the above code as follows:

  1. @RestController  
  2. public class AdvertiserController {  
  3.   
  4.     @RequestMapping(value = "/advertiser", method = RequestMethod.POST)  
  5.     public Response createAdvertiser(@RequestBody AdvertiserParam advertiserParam) {  
  6.         ...  
  7.     }  
  8.   
  9.     @RequestMapping(value = "/advertiser/{id}", method = RequestMethod.GET)  
  10.     public Response getAdvertiser(@PathVariable("id") String advertiserId) {  
  11.         ...  
  12.     }  
  13. }  

In addition to using annotations to define serialization behavior, we also need to use Jackson to provide JSON serialization operations, just add the following configurations in the Spring configuration file:

  1. <mvc:annotation-driven>  
  2.     <mvc:message-converters>  
  3.         <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>  
  4.     </mvc:message-converters>  
  5. </mvc:annotation-driven>  
If you need to customize Jackson's serialization behavior, such as excluding empty attributes, indenting output, turning humps into underscores, date formatting, etc., how do you achieve this?

First, we need to extend the ObjectMapper class provided by Jackson with the following code:

  1. public class CustomObjectMapper extends ObjectMapper {  
  2.   
  3.     private boolean camelCaseToLowerCaseWithUnderscores = false;  
  4.     private String dateFormatPattern;  
  5.   
  6.     public void setCamelCaseToLowerCaseWithUnderscores(boolean camelCaseToLowerCaseWithUnderscores) {  
  7.         this.camelCaseToLowerCaseWithUnderscores = camelCaseToLowerCaseWithUnderscores;  
  8.     }  
  9.   
  10.     public void setDateFormatPattern(String dateFormatPattern) {  
  11.         this.dateFormatPattern = dateFormatPattern;  
  12.     }  
  13.   
  14.     public void init() {  
  15.         //Exclusion value is null attribute  
  16.         setSerializationInclusion(JsonInclude.Include.NON_NULL);  
  17.         //Conduct indentation output  
  18.         configure(SerializationFeature.INDENT_OUTPUT, true);  
  19.         //Conversion of hump to underscore  
  20.         if (camelCaseToLowerCaseWithUnderscores) {  
  21.             setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);  
  22.         }  
  23.         //Date formatting  
  24.         if (StringUtil.isNotEmpty(dateFormatPattern)) {  
  25.             DateFormat dateFormat = new SimpleDateFormat(dateFormatPattern);  
  26.             setDateFormat(dateFormat);  
  27.         }  
  28.     }  
  29. }  
Then, Custom Object Mapper is injected into Mapping Jackson 2HttpMessageConverter with Spring configuration as follows:
  1. <bean id="objectMapper" class="com.xxx.api.json.CustomObjectMapper" init-method="init">  
  2.     <property name="camelCaseToLowerCaseWithUnderscores" value="true"/>  
  3.     <property name="dateFormatPattern" value="yyyy-MM-dd HH:mm:ss"/>  
  4. </bean>  
  5.   
  6. <mvc:annotation-driven>  
  7.     <mvc:message-converters>  
  8.         <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">  
  9.             <property name="objectMapper" ref="objectMapper"/>  
  10.         </bean>  
  11.     </mvc:message-converters>  
  12. </mvc:annotation-driven>  
Through the above process, we have completed a REST framework based on Spring MVC, but the framework is still very thin and lacks many key features, especially exception handling.

4.3 Handling Abnormal Behavior

In Spring MVC, we can use AOP technology to write a global exception handling facet class, which can be used to deal with all exception behaviors in a unified way. It was only provided in Spring 3.2. The usage is simple, just define a class and annotate it with the @Controller Advice annotation, and use the @ResponseBody annotation to indicate that the return value can be serialized into a JSON string. The code is as follows:

  1. @ControllerAdvice  
  2. @ResponseBody  
  3. public class ExceptionAdvice {  
  4.   
  5.     /** 
  6.      * 400 - Bad Request 
  7.      */  
  8.     @ResponseStatus(HttpStatus.BAD_REQUEST)  
  9.     @ExceptionHandler(HttpMessageNotReadableException.class)  
  10.     public Response handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {  
  11.         logger.error("Failure of parameter analysis", e);  
  12.         return new Response().failure("could_not_read_json");  
  13.     }  
  14.   
  15.     /** 
  16.      * 405 - Method Not Allowed 
  17.      */  
  18.     @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)  
  19.     @ExceptionHandler(HttpRequestMethodNotSupportedException.class)  
  20.     public Response handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {  
  21.         logger.error("Current request method is not supported", e);  
  22.         return new Response().failure("request_method_not_supported");  
  23.     }  
  24.   
  25.     /** 
  26.      * 415 - Unsupported Media Type 
  27.      */  
  28.     @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)  
  29.     @ExceptionHandler(HttpMediaTypeNotSupportedException.class)  
  30.     public Response handleHttpMediaTypeNotSupportedException(Exception e) {  
  31.         logger.error("Current media types are not supported", e);  
  32.         return new Response().failure("content_type_not_supported");  
  33.     }  
  34.   
  35.     /** 
  36.      * 500 - Internal Server Error 
  37.      */  
  38.     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)  
  39.     @ExceptionHandler(Exception.class)  
  40.     public Response handleException(Exception e) {  
  41.         logger.error("Service Running Exceptions", e);  
  42.         return new Response().failure(e.getMessage());  
  43.     }  
  44. }  
It can be seen that the ExceptionAdvice class contains a series of exception handling methods. Each method defines the response state code through the @ResponseStatus annotation, and specifies the specific exception class to be intercepted through the @ExceptionHandler annotation. The above process only contains a part of the exception, if you need to deal with other exceptions, you can add specific methods. It should be noted that each exception handling method is invoked from top to bottom at runtime to match whether the current exception type matches the exception defined in the @ExceptionHandler annotation. If it matches, the method is executed, ignoring all subsequent exception handling methods, and eventually returning the Response object serialized by JSON.

4.4 Support parameter validation

Let's go back to the example mentioned above, where we process a normal POST request with the following code:

  1. @RestController  
  2. public class AdvertiserController {  
  3.   
  4.     @RequestMapping(value = "/advertiser", method = RequestMethod.POST)  
  5.     public Response createAdvertiser(@RequestBody AdvertiserParam advertiserParam) {  
  6.         ...  
  7.     }  
  8. }  
Among them, the AdvertiserParam parameter contains several attributes, which can be seen from the following class structure: it is a traditional POJO:
  1. public class AdvertiserParam {  
  2.   
  3.     private String advertiserName;  
  4.       
  5.     private String description;  
  6.   
  7.     //Elimination of getter/setter method  
  8. }  
If the business needs to ensure that the advertiserName attribute of the AdvertiserParam object is mandatory, how can it be implemented?

If the code for parameter verification is written in Controller, it will inevitably mix up with normal business logic, resulting in a lack of single responsibility, contrary to the "single responsibility principle". It is recommended that its parameter validation behavior be stripped from Controller and put into another class. Here only an @Valid annotation is provided to define the AdvertiserParam parameter, and the @NotEmpty annotation is used to define the advertiserName attribute in the AdvertiserParam class, as follows:

  1. @RestController  
  2. public class AdvertiserController {  
  3.   
  4.     @RequestMapping(value = "/advertiser", method = RequestMethod.POST)  
  5.     public Response createAdvertiser(@RequestBody @Valid AdvertiserParam advertiserParam) {  
  6.         ...  
  7.     }  
  8. }  
  9.   
  10. public class AdvertiserParam {  
  11.   
  12.     @NotEmpty  
  13.     private String advertiserName;  
  14.       
  15.     private String description;  
  16.   
  17.     //Elimination of getter/setter method  
  18. }  
The @Valid annotation here is actually the annotation provided by the Validation Bean specification, which has been implemented by the Hibernate Validator framework, so you need to add the following Maven dependencies to the pom.xml file:
  1. <dependency>  
  2.     <groupId>org.hibernate</groupId>  
  3.     <artifactId>hibernate-validator</artifactId>  
  4.     <version>${hibernate-validator.version}</version>  
  5. </dependency>  
It should be noted that Hibernate Validator and Hibernate do not have any dependencies, the only link is that they belong to JBoss's open source projects.

To implement the @NotEmpty annotation, we need to do the following things.

First, define a @NotEmpty annotation class with the following code:

  1. @Documented  
  2. @Target({ElementType.FIELD, ElementType.PARAMETER})  
  3. @Retention(RetentionPolicy.RUNTIME)  
  4. @Constraint(validatedBy = NotEmptyValidator.class)  
  5. public @interface NotEmpty {  
  6.   
  7.     String message() default "not_empty";  
  8.   
  9.     Class<?>[] groups() default {};  
  10.   
  11.     Class<? extends Payload>[] payload() default {};  
  12. }  
The annotation class above must contain three attributes: message, groups, payload, because this is required by the specification. In addition, a validator class needs to be specified through the @Constrain annotation, which corresponds to NotEmptyValidator. The code is as follows:
  1. public class NotEmptyValidator implements ConstraintValidator<NotEmpty, String> {  
  2.   
  3.     @Override  
  4.     public void initialize(NotEmpty constraintAnnotation) {  
  5.     }  
  6.   
  7.     @Override  
  8.     public boolean isValid(String value, ConstraintValidatorContext context) {  
  9.         return StringUtil.isNotEmpty(value);  
  10.     }  
  11. }  
The above validator class implements the ConstraintValidator interface and completes the specific parameter validation logic in the isValid() method of the interface. It is important to note that generics need to be specified when implementing the interface. The first parameter represents the NotEmpty, and the second parameter represents the String.

Then, we need to turn this feature on in the Spring configuration file and add the following configuration:

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

Finally, we need to add a parameter validation method to the global exception handling class. The code is as follows:

  1. @ControllerAdvice  
  2. @ResponseBody  
  3. public class ExceptionAdvice {  
  4.   
  5.     /** 
  6.      * 400 - Bad Request 
  7.      */  
  8.     @ResponseStatus(HttpStatus.BAD_REQUEST)  
  9.     @ExceptionHandler(ValidationException.class)  
  10.     public Response handleValidationException(ValidationException e) {  
  11.         logger.error("Failure of parameter validation", e);  
  12.         return new Response().failure("validation_exception");  
  13.     }  
  14. }  
So far, the REST framework has integrated Bean Validation features, and we can use various annotations to complete the required parameter validation behavior.

It seems that the framework can run successfully locally. The whole architecture consists of two applications. The front-end application provides pure static HTML pages. The back-end application publishes REST API. The front-end needs to call the back-end published REST API through AJAX. However, AJAX does not support cross-domain access. That is to say, the front-end and back-end applications must be accessed under the same domain name. This is a very serious technical barrier, and solutions must be found.

4.5 Solving cross-domain problems

For example, front-end applications are static sites and deployed in the http://web.xxx.com domain. Back-end applications publish REST API s and deploy them in the http://api.xxx.com domain. How can front-end applications access back-end applications through AJAX cross-domain? This requires the use of CORS technology to achieve, which is currently the best solution.

[CORS is called Cross Origin Resource Sharing (cross-domain resource sharing). The server only needs to add relevant response header information to realize the client sending AJAX cross-domain requests. ]

CORS technology is very simple and easy to implement. At present, most browsers have supported this technology (IE8 browser also supports it). The server can be implemented by any programming language as long as the CORS response header can be written into the response object.

Next, we continue to expand the REST framework to achieve cross-domain access of AJAX through CORS technology.

First, we need to write a Filter to Filter all HTTP requests and write the CORS response header into the response object. The code is as follows:

  1. public class CorsFilter implements Filter {  
  2.   
  3.     private String allowOrigin;  
  4.     private String allowMethods;  
  5.     private String allowCredentials;  
  6.     private String allowHeaders;  
  7.     private String exposeHeaders;  
  8.   
  9.     @Override  
  10.     public void init(FilterConfig filterConfig) throws ServletException {  
  11.         allowOrigin = filterConfig.getInitParameter("allowOrigin");  
  12.         allowMethods = filterConfig.getInitParameter("allowMethods");  
  13.         allowCredentials = filterConfig.getInitParameter("allowCredentials");  
  14.         allowHeaders = filterConfig.getInitParameter("allowHeaders");  
  15.         exposeHeaders = filterConfig.getInitParameter("exposeHeaders");  
  16.     }  
  17.   
  18.     @Override  
  19.     public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {  
  20.         HttpServletRequest request = (HttpServletRequest) req;  
  21.         HttpServletResponse response = (HttpServletResponse) res;  
  22.         if (StringUtil.isNotEmpty(allowOrigin)) {  
  23.             List<String> allowOriginList = Arrays.asList(allowOrigin.split(","));  
  24.             if (CollectionUtil.isNotEmpty(allowOriginList)) {  
  25.                 String currentOrigin = request.getHeader("Origin");  
  26.                 if (allowOriginList.contains(currentOrigin)) {  
  27.                     response.setHeader("Access-Control-Allow-Origin", currentOrigin);  
  28.                 }  
  29.             }  
  30.         }  
  31.         if (StringUtil.isNotEmpty(allowMethods)) {  
  32.             response.setHeader("Access-Control-Allow-Methods", allowMethods);  
  33.         }  
  34.         if (StringUtil.isNotEmpty(allowCredentials)) {  
  35.             response.setHeader("Access-Control-Allow-Credentials", allowCredentials);  
  36.         }  
  37.         if (StringUtil.isNotEmpty(allowHeaders)) {  
  38.             response.setHeader("Access-Control-Allow-Headers", allowHeaders);  
  39.         }  
  40.         if (StringUtil.isNotEmpty(exposeHeaders)) {  
  41.             response.setHeader("Access-Control-Expose-Headers", exposeHeaders);  
  42.         }  
  43.         chain.doFilter(req, res);  
  44.     }  
  45.   
  46.     @Override  
  47.     public void destroy() {  
  48.     }  
  49. }  
The above CorsFilter reads the relevant Filter initialization parameters from web.xml and writes them into the corresponding CORS response headers when processing HTTP requests. The following outlines the meaning of these CORS response headers:

It should be noted that the CORS specification defines Access-Control-Allow-Origin to allow only two values, either *, or for specific domain names, that is to say, multiple domain names are not supported at the same time. In order to solve the problem of cross-domain, we need to do some processing in the code. Here, Filter initialization parameters are treated as a set of domain names (separated by commas). We only need to get Origin request header from the current request to know which domain the request is sent from. If the request is in the set of domain names allowed above, it is put into the Access-Control-Allow-Origin response header. This eases the problem across multiple domains.

The following is how to configure CorsFilter in web.xml:

  1. <filter>  
  2.     <filter-name>corsFilter</filter-name>  
  3.     <filter-class>com.xxx.api.cors.CorsFilter</filter-class>  
  4.     <init-param>  
  5.         <param-name>allowOrigin</param-name>  
  6.         <param-value>http://web.xxx.com</param-value>  
  7.     </init-param>  
  8.     <init-param>  
  9.         <param-name>allowMethods</param-name>  
  10.         <param-value>GET,POST,PUT,DELETE,OPTIONS</param-value>  
  11.     </init-param>  
  12.     <init-param>  
  13.         <param-name>allowCredentials</param-name>  
  14.         <param-value>true</param-value>  
  15.     </init-param>  
  16.     <init-param>  
  17.         <param-name>allowHeaders</param-name>  
  18.         <param-value>Content-Type</param-value>  
  19.     </init-param>  
  20. </filter>  
  21. <filter-mapping>  
  22.     <filter-name>corsFilter</filter-name>  
  23.     <url-pattern>/*</url-pattern>  
  24. </filter-mapping>  
Completing the above process can realize the cross-domain function of AJAX, but there seems to be another problem. Because REST is stateless, the REST API released by the back-end application can be arbitrarily invoked without user login. This is obviously unsafe. How to solve this problem? We need to provide security mechanisms for REST requests.

4.6 Providing Security Mechanisms

To solve the problem of REST security invocation, we can do it very complex or very simple. We can provide REST security mechanism according to the following procedures:

  1. When the user logs in successfully, a token is generated on the server side and put into memory (which can be put into JVM or Redis), and the token is returned to the client.
  2. The returned token is written to the cookie in the client and sent to the server with the request header every time the request is made.
  3. Provide an AOP section for intercepting all Controller methods and judging the effectiveness of token in the section.
  4. When logged out, just clean up the token in the cookie, and the server token can set the expiration time to remove it by itself.

First, we need to define an interface for managing token, including the ability to create token and check its validity. The code is as follows:

  1. public interface TokenManager {  
  2.   
  3.     String createToken(String username);  
  4.   
  5.     boolean checkToken(String token);  
  6. }  
Then, we can provide a simple TokenManager implementation class that stores token in JVM memory. The code is as follows:
[java] view plaincopy
  1. public class DefaultTokenManager implements TokenManager {  
  2.   
  3.     private static Map<String, String> tokenMap = new ConcurrentHashMap<>();  
  4.   
  5.     @Override  
  6.     public String createToken(String username) {  
  7.         String token = CodecUtil.createUUID();  
  8.         tokenMap.put(token, username);  
  9.         return token;  
  10.     }  
  11.   
  12.     @Override  
  13.     public boolean checkToken(String token) {  
  14.         return !StringUtil.isEmpty(token) && tokenMap.containsKey(token);  
  15.     }  
  16. }  
It should be noted that if distributed cluster is needed, it is recommended to provide an implementation class based on Redis, store token in Redis, and make use of Redis's inherent characteristics to achieve token's distributed consistency.

Then, we can write a cut class based on Spring AOP to intercept the method of Controller class, get token from the request header, and finally judge the token validity. The code is as follows:

[java] view plaincopy
  1. public class SecurityAspect {  
  2.   
  3.     private static final String DEFAULT_TOKEN_NAME = "X-Token";  
  4.   
  5.     private TokenManager tokenManager;  
  6.     private String tokenName;  
  7.   
  8.     public void setTokenManager(TokenManager tokenManager) {  
  9.         this.tokenManager = tokenManager;  
  10.     }  
  11.   
  12.     public void setTokenName(String tokenName) {  
  13.         if (StringUtil.isEmpty(tokenName)) {  
  14.             tokenName = DEFAULT_TOKEN_NAME;  
  15.         }  
  16.         this.tokenName = tokenName;  
  17.     }  
  18.   
  19.     public Object execute(ProceedingJoinPoint pjp) throws Throwable {  
  20. // Target acquisition from tangent points
  21.         MethodSignature methodSignature = (MethodSignature) pjp.getSignature();  
  22.         Method method = methodSignature.getMethod();  
  23. // If the target method ignores the security check, the target method is called directly.
  24.         if (method.isAnnotationPresent(IgnoreSecurity.class)) {  
  25.             return pjp.proceed();  
  26.         }  
  27. // Get the current token from the request header
  28.         String token = WebContext.getRequest().getHeader(tokenName);  
  29. // Check the effectiveness of token
  30.         if (!tokenManager.checkToken(token)) {  
  31.             String message = String.format("token [%s] is invalid", token);  
  32.             throw new TokenException(message);  
  33.         }  
  34. // Call the target method
  35.         return pjp.proceed();  
  36.     }  
  37. }  
To make SecurityAspect effective, you need to add the following Spring configuration:
[xml] view plaincopy
  1. <bean id="securityAspect" class="com.xxx.api.security.SecurityAspect">  
  2.     <property name="tokenManager" ref="tokenManager"/>  
  3.     <property name="tokenName" value="X-Token"/>  
  4. </bean>  
  5.   
  6. <aop:config>  
  7.     <aop:aspect ref="securityAspect">  
  8.         <aop:around method="execute" pointcut="@annotation(org.springframework.web.bind.annotation.RequestMapping)"/>  
  9.     </aop:aspect>  
  10. </aop:config>  
Finally, don't forget to add the allowable X-Token response header in web.xml, which is configured as follows:
[xml] view plaincopy
  1. <init-param>  
  2.     <param-name>allowHeaders</param-name>  
  3.     <param-value>Content-Type,X-Token</param-value>  
  4. </init-param>  

summary

Starting from the classical MVC mode, this paper briefly describes what MVC mode is and the shortcomings of this mode. Then, it introduces how to improve the MVC model and transform it into a front-end and back-end separation architecture, and explains why front-end and back-end separation is needed. Finally, the front and back end are decoupled by REST service, and the main implementation process of a Java-based REST framework is provided. Especially, the core technical problems and their solutions need attention. I hope this article is helpful to the readers who are exploring the separation of front and back ends, and look forward to discussing with you.

(Responsibility compilation)/ Qian Shuguang Focus on architecture and algorithm Domain, qianshg@csdn .NET)

Author's brief introduction: He has been engaged in Java EE application development for nearly ten years. He was a system architect of Alibaba Company and is currently CTO of tezign.com Company. Distributed Service Architecture and Big data Technologies have been studied in depth and are abundant. B/S Architecture Development Experience and Project Practical Experience, Good at agile Development model. One of the domestic promoters of open source software is active in the "open source China" community website, the founder of Smart Framework open source framework. I love technology exchange and enjoy sharing my work experience.

(Responsibility compilation)/ Qian Shuguang Focus on architecture and algorithmic areas, search for reports or submissions, please email qianshg@csdn .net Exchange and discussion WeChat Qshuguang 2008, name + company + position)

This article is from the October B Architecture Thematic Paper of the Electronic Journal of Programmers. Programmers'electronic subscription in 2015 is in full swing Include: iPad, Android, PDF.

Posted by derksj on Sun, 09 Jun 2019 14:08:03 -0700