一些Spring MVC的使用技巧

_Arvin 8年前發布 | 45K 次閱讀 Spring JEE框架 Spring MVC

APP服務端的Token驗證

通過攔截器對使用了 @Authorization 注解的方法進行請求攔截,從http header中取出token信息,驗證其是否合法。非法直接返回401錯誤,合法將token對應的user key存入request中后繼續執行。具體實現代碼:

public boolean preHandle(HttpServletRequest request,
                         HttpServletResponse response, Object handler) throws Exception {
    //如果不是映射到方法直接通過
    if (!(handler instanceof HandlerMethod)) {
        return true;
    }
    HandlerMethod handlerMethod = (HandlerMethod) handler;
    Method method = handlerMethod.getMethod();
    //從header中得到token
    String token = request.getHeader(httpHeaderName);
    if (token != null && token.startsWith(httpHeaderPrefix) && token.length() > 0) {
        token = token.substring(httpHeaderPrefix.length());
        //驗證token
        String key = manager.getKey(token);
        if (key != null) {
            //如果token驗證成功,將token對應的用戶id存在request中,便于之后注入
            request.setAttribute(REQUEST_CURRENT_KEY, key);
            return true;
        }
    }
    //如果驗證token失敗,并且方法注明了Authorization,返回401錯誤
    if (method.getAnnotation(Authorization.class) != null) {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setCharacterEncoding("gbk");
        response.getWriter().write(unauthorizedErrorMessage);
        response.getWriter().close();
        return false;
    }
    //為了防止以某種直接在REQUEST_CURRENT_KEY寫入key,將其設為null
    request.setAttribute(REQUEST_CURRENT_KEY, null);
    return true;
}

通過攔截器后,使用解析器對修飾了 @CurrentUser 的參數進行注入。從request中取出之前存入的user key,得到對應的user對象并注入到參數中。具體實現代碼:

@Override
public boolean supportsParameter(MethodParameter parameter) {
    Class clazz;
    try {
        clazz = Class.forName(userModelClass);
    } catch (ClassNotFoundException e) {
        return false;
    }
    //如果參數類型是User并且有CurrentUser注解則支持
    if (parameter.getParameterType().isAssignableFrom(clazz) &&
            parameter.hasParameterAnnotation(CurrentUser.class)) {
        return true;
    }
    return false;
}

@Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { //取出鑒權時存入的登錄用戶Id Object object = webRequest.getAttribute(AuthorizationInterceptor.REQUEST_CURRENT_KEY, RequestAttributes.SCOPE_REQUEST); if (object != null) { String key = String.valueOf(object); //從數據庫中查詢并返回 Object userModel = userModelRepository.getCurrentUser(key); if (userModel != null) { return userModel; } //有key但是得不到用戶,拋出異常 throw new MissingServletRequestPartException(AuthorizationInterceptor.REQUEST_CURRENT_KEY); } //沒有key就直接返回null return null; }</code></pre>

詳細分析: RESTful登錄設計(基于Spring及Redis的Token鑒權)

源碼見: ScienJus/spring-restful-authorization

封裝好的工具類: ScienJus/spring-authorization-manager

使用別名接受對象的參數

請求中的參數名和代碼中定義的參數名不同是很常見的情況,對于這種情況Spring提供了幾種原生的方法:

對于 @RequestParam 可以直接指定value值為別名( @RequestHeader 也是一樣),例如:

public String home(@RequestParam("user_id") long userId) {
    return "hello " + userId;
}

對于 @RequestBody ,由于其使使用Jackson將Json轉換為對象,所以可以使用 @JsonProperty 的value指定別名,例如:

public String home(@RequestBody User user) {
    return "hello " + user.getUserId();
}

class User { @JsonProperty("user_id") private long userId; }</code></pre>

但是使用對象的屬性接受參數時,就無法直接通過上面的辦法指定別名了,例如:

public String home(User user) {
    return "hello " + user.getUserId();
}

這時候需要使用DataBinder手動綁定屬性和別名,我在StackOverFlow上找到的 這篇文章 是個不錯的辦法,這里就不重復造輪子了。

關閉默認通過請求的后綴名判斷Content-Type

之前接手的項目的開發習慣是使用.html作為請求的后綴名,這在Struts2上是沒有問題的(因為本身Struts2處理Json的幾種方法就都很爛)。但是我接手換成Spring MVC后,使用 @ResponseBody 返回對象時就會報找不到轉換器錯誤。

這是因為Spring MVC默認會將后綴名為.html的請求的Content-Type認為是 text/html ,而 @ResponseBody 返回的Content-Type是 application/json ,沒有任何一種轉換器支持這樣的轉換。所以需要手動將通過后綴名判斷Content-Type的設置關掉,并將默認的Content-Type設置為 application/json :

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    configurer.favorPathExtension(false).
            defaultContentType(MediaType.APPLICATION_JSON);
}

}</code></pre>

更改默認的Json序列化方案

項目中有時候會有自己獨特的Json序列化方案,例如比較常用的使用 0 / 1 替代 false / true ,或是通過 "" 代替 null ,由于 @ResponseBody 默認使用的是 MappingJackson2HttpMessageConverter ,只需要將自己實現的 ObjectMapper 傳入這個轉換器:

public class CustomObjectMapper extends ObjectMapper {

public CustomObjectMapper() {
    super();
    this.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>() {
        @Override
        public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
            jgen.writeString("");
        }
    });
    SimpleModule module = new SimpleModule();
    module.addSerializer(boolean.class, new JsonSerializer<Boolean>() {
        @Override
        public void serialize(Boolean value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
            jgen.writeNumber(value ? 1 : 0);
        }
    });
    this.registerModule(module);
}

}</code></pre>

自動加密/解密請求中的Json

涉及到 @RequestBody 和 @ResponseBody 的類型轉換問題一般都在 MappingJackson2HttpMessageConverter 中解決,想要自動加密/解密只需要繼承這個類并重寫 readInternal / writeInternal 方法即可:

@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
    //解密
    String json = AESUtil.decrypt(inputMessage.getBody());
    JavaType javaType = getJavaType(clazz, null);
    //轉換
    return this.objectMapper.readValue(json, javaType);
}

@Override protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { //使用Jackson的ObjectMapper將Java對象轉換成Json String ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(object); //加密 String result = AESUtil.encrypt(json); //輸出 outputMessage.getBody().write(result.getBytes()); }</code></pre>

基于注解的敏感詞過濾功能

項目需要對用戶發布的內容進行過濾,將其中的敏感詞替換為 * 等特殊字符。大部分Web項目在處理這方面需求時都會選擇過濾器( Filter ),在過濾器中將 Request 包上一層 Wrapper ,并重寫其 getParameter 等方法,例如:

public class SafeTextRequestWrapper extends HttpServletRequestWrapper {
    public SafeTextRequestWrapper(HttpServletRequest req) {
        super(req);
    }

@Override
public Map<String, String[]> getParameterMap() {
    Map<String, String[]> paramMap = super.getParameterMap();
    for (String[] values : paramMap.values()) {
        for (int i = 0; i < values.length; i++) {
            values[i] = SensitiveUtil.filter(values[i]);
        }
    }
    return paramMap ;
}

@Override
public String getParameter(String name) {
    return SensitiveUtil.filter(super.getParameter(name));
}

}

public class SafeTextFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    SafeTextRequestWrapper safeTextRequestWrapper = new SafeTextRequestWrapper((HttpServletRequest) request);
    chain.doFilter(safeTextRequestWrapper, response);
}

@Override
public void destroy() {

}

}</code></pre>

但是這樣做會有一些明顯的問題,比如無法控制具體對哪些信息進行過濾。如果用戶注冊的郵箱或是密碼中也帶有 fuck 之類的敏感詞,那就屬于誤傷了。

所以改用Spring MVC的Formatter進行拓展,只需要在 @RequestParam 的參數上使用 @SensitiveFormat 注解,Spring MVC就會在注入該屬性時自動進行敏感詞過濾。既方便又不會誤傷,實現方法如下:

聲明 @SensitiveFormat 注解:

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SensitiveFormat {
}

創建 SensitiveFormatter 類。實現 Formatter 接口,重寫 parse 方法(將接收到的內容轉換成對象的方法),在該方法中對接收內容進行過濾:

public class SensitiveFormatter implements Formatter<String> {
    @Override
    public String parse(String text, Locale locale) throws ParseException {
        return SensitiveUtil.filter(text);
    }

@Override
public String print(String object, Locale locale) {
    return object;
}

}</code></pre>

創建 SensitiveFormatAnnotationFormatterFactory 類,實現 AnnotationFormatterFactory 接口,將 @SensitiveFormat 與 SensitiveFormatter 綁定:

public class SensitiveFormatAnnotationFormatterFactory implements AnnotationFormatterFactory<SensitiveFormat> {

@Override
public Set<Class<?>> getFieldTypes() {
    Set<Class<?>> fieldTypes = new HashSet<>();
    fieldTypes.add(String.class);
    return fieldTypes;
}

@Override
public Printer<?> getPrinter(SensitiveFormat annotation, Class<?> fieldType) {
    return new SensitiveFormatter();
}

@Override
public Parser<?> getParser(SensitiveFormat annotation, Class<?> fieldType) {
    return new SensitiveFormatter();
}

}</code></pre>

最后將 SensitiveFormatAnnotationFormatterFactory 注冊到Spring MVC中:

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

@Override
public void addFormatters(FormatterRegistry registry) {
    registry.addFormatterForFieldAnnotation(new SensitiveFormatAnnotationFormatterFactory());
    super.addFormatters(registry);
}

}</code></pre>

記錄請求的返回內容

這里提供一種比較通用的方法,基于過濾器實現,所以在非Spring MVC的項目也可以使用。

首先導入 commons-io :

<dependency>
  <groupId>commons-io</groupId>
  <artifactId>commons-io</artifactId>
  <version>2.4</version>
</dependency>

需要用到這個庫中的 TeeOutputStream ,這個類可以將一個將內容同時輸出到兩個分支的輸出流,將其封裝為 ServletOutputStream :

public class TeeServletOutputStream extends ServletOutputStream {

private final TeeOutputStream teeOutputStream;

public TeeServletOutputStream(OutputStream one, OutputStream two) {
    this.teeOutputStream = new TeeOutputStream(one, two);
}

@Override
public boolean isReady() {
    return false;
}

@Override
public void setWriteListener(WriteListener listener) {

}

@Override
public void write(int b) throws IOException {
    this.teeOutputStream.write(b);
}

@Override
public void flush() throws IOException {
    super.flush();
    this.teeOutputStream.flush();
}

@Override
public void close() throws IOException {
    super.close();
    this.teeOutputStream.close();
}

}</code></pre>

然后創建一個過濾器,將原有的 response 的 getOutputStream 方法重寫:

public class LoggingFilter implements Filter {

private static final Logger LOGGER = LoggerFactory.getLogger(LoggingFilter.class);

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

public void doFilter(ServletRequest request, final ServletResponse response, FilterChain chain) throws IOException, ServletException {
    final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

    HttpServletResponseWrapper responseWrapper = new HttpServletResponseWrapper((HttpServletResponse) response) {

        private TeeServletOutputStream teeServletOutputStream;

        @Override
        public ServletOutputStream getOutputStream() throws IOException {
            return new TeeServletOutputStream(super.getOutputStream(), byteArrayOutputStream);
        }
    };
    chain.doFilter(request, responseWrapper);
    String responseLog = byteArrayOutputStream.toString();
    if (LOGGER.isInfoEnabled() && !StringUtil.isEmpty(responseLog)) {
        LOGGER.info(responseLog);
    }
}

@Override
public void destroy() {

}

}</code></pre>

將 super.getOutputStream() 和 ByteArrayOutputStream 分別作為兩個分支流,前者會將內容返回給客戶端,后者使用 toString 方法即可獲得輸出內容。

 

來自: http://h2ex.com/1198

 

 本文由用戶 _Arvin 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!