從 MVC 到前后端分離

轉載自:https://my.oschina.net/huangyong/blog/521891

從MVC到前后端分離

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語言中,最后在其它許多開發語言中都得到了很好的應用,例如,Java 中的 Struts、Spring MVC等框架。正是因為這些MVC框架的出現,才讓MVC模式真正落地,讓開發更加高效,讓代碼耦合度盡量減小,讓應用程序各部分的職責更加清晰。
既然MVC模式這么好,難道它就沒有不足的地方嗎?我認為MVC至少有以下三點不足:
1)每次請求必須經過“控制器->模型->視圖”這個流程,用戶才能看到最終的展現的界面,這個過程似乎有些復雜。
2)實際上視圖是依賴于模型的,換句話說,如果沒有模型,視圖也無法呈現出最終的效果。
3)渲染視圖的過程是在服務端來完成的,最終呈現給瀏覽器的是帶有模型的視圖頁面,性能無法得到很好的優化。
為了使數據展現過程更加直接,并且提供更好的用戶體驗,我們有必要對MVC模式進行改進。不妨這樣來嘗試,首先從瀏覽器發送 AJAX 請求,然后服務端接受該請求并返回 JSON 數據返回給瀏覽器,最后在瀏覽器中進行界面渲染。
改進后的 MVC 模式如下圖所示:

?

也就是說,我們輸入的是AJAX請求,輸出的是JSON數據,市面上有這樣的技術來實現這個功能嗎?答案是 REST。

REST 全稱是 Representational State Transfer(表述性狀態轉移),它是 Roy Fielding 博士在 2000 年寫的一篇關于軟件架構風格的論文,此文一出,威震四方!國內外許多知名互
聯網公司紛紛開始采用這種輕量級的 Web 服務,大家習慣將其稱為 RESTful Web Services,或簡稱 REST 服務。

如果將瀏覽器這一端視為前端,而服務器那一端視為后端的話,可以將以上改進后的MVC模式簡化為以下前后端分離模式:

?

可見,有了REST服務,前端關注界面展現,后端關注業務邏輯,分工明確,職責清晰。那么,如何使用 REST 服務將應用程序進行前后端分離呢?我們接下來繼續探討,首先我們需要認識 REST。

3 認識 REST

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 {@NotEmptyprivate 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> {@Overridepublic void initialize(NotEmpty constraintAnnotation) {}@Overridepublic 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;@Overridepublic 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");}@Overridepublic 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);}@Overridepublic 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<>();@Overridepublic String createToken(String username) {String token = CodecUtil.createUUID();tokenMap.put(token, username);return token;}@Overridepublic 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 中獲取當前 tokenString 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 框架的主要實現過程,尤其是需要注意的核心技術問題及其解決方案。希望本文對正在探索前后端分離的讀者們有所幫助,期待與大家共同探討。

?

轉載于:https://www.cnblogs.com/winner-0715/p/6385426.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/393899.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/393899.shtml
英文地址,請注明出處:http://en.pswp.cn/news/393899.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

leetcode93. 復原IP地址(回溯)

給定一個只包含數字的字符串&#xff0c;復原它并返回所有可能的 IP 地址格式。 有效的 IP 地址正好由四個整數&#xff08;每個整數位于 0 到 255 之間組成&#xff09;&#xff0c;整數之間用 ‘.’ 分隔。 示例: 輸入: “25525511135” 輸出: [“255.255.11.135”, “255…

vj節點_創意編碼—如何在JavaScript中創建VJ引擎

vj節點by George Gally通過喬治加利 創意編碼—如何在JavaScript中創建VJ引擎 (Creative Coding — How to create a VJ engine in JavaScript) 了解如何將JavaScript動態注入網頁 (Learn how to dynamically inject JavaScript into webpages) For years I’ve been using th…

上傳下載

# 默寫 TCP UDP 文件夾中的代碼# 完成一個上傳和下載文件的小程序 # server端 :根據客戶端需求自定義 # client端 # 客戶端啟動之后 # 選擇 上傳操作 還是 下載操作 # 如果是上傳操作 : 輸入要上傳的文件路徑 # 基礎需求 :直接將文件上傳到默認目錄 # 進階需求 :將…

qt 串口 環形緩存_qt?linux串口?緩沖區多大

滿意答案Zc的愛丶很美2016.09.11采納率&#xff1a;51% 等級&#xff1a;9已幫助&#xff1a;515人一、程序設計的基礎&#xff0c;例如&#xff1a;基本的編程語言基礎&#xff0c;至少對數據類型、程序的結構及流程控制等最基本的內容要相當清楚&#xff01;另外有不少同學…

在.NET中使用SMTP發送郵件

這是一篇轉載&#xff0c;可能對大家很有用啊&#xff0c;放首頁看看是否有參考價值。本文提到的方案仍然不能算是完全解決所有問題&#xff0c;最佳的dotNET下通過SMTP&#xff08;帶驗證&#xff09;發送郵件的機制是什么&#xff0c;不知道大家有什么好的看法&#xff01; …

oracle堆,oracle被一堆insert和update堵死解決方案

當前位置:我的異常網 Oracle技術 oracle被一堆insert和update堵死解決方案oracle被一堆insert和update堵死解決方案www.myexceptions.net 網友分享于&#xff1a;2014-07-22 瀏覽&#xff1a;0次oracle被一堆insert和update堵死在生產環境下&#xff0c;幾乎每天都會發生一次…

leetcode306. 累加數(回溯)

累加數是一個字符串&#xff0c;組成它的數字可以形成累加序列。 一個有效的累加序列必須至少包含 3 個數。除了最開始的兩個數以外&#xff0c;字符串中的其他數都等于它之前兩個數相加的和。 給定一個只包含數字 ‘0’-‘9’ 的字符串&#xff0c;編寫一個算法來判斷給定輸…

使用Typescript和React的最佳實踐

by Christopher Diggins克里斯托弗迪金斯(Christopher Diggins) 使用Typescript和React的最佳實踐 (Best practices for using Typescript with React) There are numerous tools and tutorials to help developers start writing simple React applications with TypeScript.…

LeetCode || Copy List with Random Pointer

A linked list is given such that each node contains an additional random pointer which could point to any node in the list or null. Return a deep copy of the list. 思路1&#xff1a;最傻瓜的方法是首先遍歷一次建立next關系的新list。然后第二次遍歷處理random關系…

oracle存儲過程多分支怎樣寫,如何從存儲過程返回多行? (Oracle PL / SQL)

如何從存儲過程返回多行&#xff1f; (Oracle PL / SQL)我想用一個參數創建一個存儲過程&#xff0c;該存儲過程將根據參數返回不同的記錄集。 這是怎么做的&#xff1f; 我可以從普通SQL中調用它嗎&#xff1f;5個解決方案65 votes這是如何構建一個函數&#xff0c;該函數返回…

京東布局消費物聯網 聚合產業鏈共建生態

據Gartner發布的數據顯示&#xff0c;到2020年&#xff0c;全球聯網設備數量將達260億臺&#xff0c;物聯網市場規模將達1.9萬億美元。如今&#xff0c;互聯網已經從人與人的連接發展到人與物、物與物的連接&#xff0c;物聯網時代帶來。 5月9日&#xff0c;京東聚合三大運營商…

xshell監聽端口_監聽端口修改_笨辦法學Linux 遠程訪問 (原理、實踐、記錄與排錯)-視頻課程_Linux視頻-51CTO學院...

聰明人下笨功夫。本課程所倡導“笨辦法”的核心是&#xff1a;● 深入理解原理● 精讀man幫助、官方文檔…● 做所有的實驗&#xff0c;盡量不要復制粘貼&#xff01;● 詳細記錄實驗過程● 使用思維導圖等輔助工具● 享受排錯的過程&#xff0c;在尋求幫助之前先嘗試自己解決本…

leetcode632. 最小區間(堆+多指針)

你有 k 個升序排列的整數數組。找到一個最小區間&#xff0c;使得 k 個列表中的每個列表至少有一個數包含在其中。 我們定義如果 b-a < d-c 或者在 b-a d-c 時 a < c&#xff0c;則區間 [a,b] 比 [c,d] 小。 示例 1: 輸入:[[4,10,15,24,26], [0,9,12,20], [5,18,22,3…

【SLAM】安裝 g2o_viewer

2017年2月8日&#xff0c;那是一個陰天。為了完成高翔博士的《一起做RGB-D SLAM》教程&#xff0c;我在 Ubuntu 14.04 安裝 g2o。遇到困難&#xff0c;怎奈我眼瞎&#xff0c;找錯了方向&#xff0c;浪費時間&#xff0c;沒有成功安裝。 問題如下&#xff08;跳到最后一個問題描…

CSS動畫快速介紹

Interested in learning CSS? Get my CSS Handbook 有興趣學習CSS嗎&#xff1f; 獲取我的CSS手冊 介紹 (Introduction) An animation is applied to an element using the animation property.使用animation屬性將動畫應用于元素。 .container { animation: spin 10s linear…

2_sat

要求字典序的情況的話&#xff0c;爆搜 不要求的話 1:建圖&#xff0c;有向邊A--->B的意義為選擇A則必須選擇B&#xff0c;一般一個點的兩種取值情況會拆點。 2:縮點。 3:建反向圖&#xff0c;跑拓撲排序&#xff08;有說不用建再跑&#xff0c;但我不懂為什么&#xff09;。…

[Spark][Python]Spark 訪問 mysql , 生成 dataframe 的例子:

[Spark][Python]Spark 訪問 mysql , 生成 dataframe 的例子&#xff1a; mydf001sqlContext.read.format("jdbc").option("url","jdbc:mysql://localhost/loudacre")\ .option("dbtable","accounts").option("user&quo…

ffmpeg mac 批量腳本_使用批處理腳本(BAT)調用FFMPEG批量編碼視頻

使用批處理腳本(BAT)編碼視頻非常方便&#xff0c;尤其當視頻序列非常多的時候&#xff0c;更是省了不少簡單重復性勞動。只要學會批處理里面幾個基本的命令就行了&#xff0c;感覺和c/c差不多。set&#xff1a;設置變量(注意&#xff1a;變量一般情況下是字符串&#xff0c;而…

單實例oracle ha,Oracle單實例啟動多個實例

Oracle單實例啟動多個實例多實例運行&#xff0c;單個實例就是一個數據庫&#xff01;一個數據庫對應多個實例是RAC。Linux建立oracle的實例步驟&#xff1a;1、在linux服務器的圖形界面下&#xff0c;打開一個終端&#xff0c;輸入如下的命令&#xff1b; xhost ###遠程調用…

leetcode357. 計算各個位數不同的數字個數(回溯)

給定一個非負整數 n&#xff0c;計算各位數字都不同的數字 x 的個數&#xff0c;其中 0 ≤ x < 10n 。示例:輸入: 2 輸出: 91 解釋: 答案應為除去 11,22,33,44,55,66,77,88,99 外&#xff0c;在 [0,100) 區間內的所有數字。代碼 class Solution {int numbers0;public int …