一些Spring MVC的使用技巧

_Arvin 3年前
   <h3>APP服务端的Token验证</h3>    <p>通过拦截器对使用了 @Authorization 注解的方法进行请求拦截,从http header中取出token信息,验证其是否合法。非法直接返回401错误,合法将token对应的user key存入request中后继续执行。具体实现代码:</p>    <pre>  <code class="language-java">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;  }</code></pre>    <p>通过拦截器后,使用解析器对修饰了 @CurrentUser 的参数进行注入。从request中取出之前存入的user key,得到对应的user对象并注入到参数中。具体实现代码:</p>    <pre>  <code class="language-java">@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>    <p>详细分析: <a href="/misc/goto?guid=4959674954579023504" rel="nofollow,noindex">RESTful登录设计(基于Spring及Redis的Token鉴权)</a></p>    <p>源码见: <a href="/misc/goto?guid=4959674954662489548" rel="nofollow,noindex">ScienJus/spring-restful-authorization</a></p>    <p>封装好的工具类: <a href="/misc/goto?guid=4959674954753452102" rel="nofollow,noindex">ScienJus/spring-authorization-manager</a></p>    <h3>使用别名接受对象的参数</h3>    <p>请求中的参数名和代码中定义的参数名不同是很常见的情况,对于这种情况Spring提供了几种原生的方法:</p>    <p>对于 @RequestParam 可以直接指定value值为别名( @RequestHeader 也是一样),例如:</p>    <pre>  <code class="language-java">public String home(@RequestParam("user_id") long userId) {      return "hello " + userId;  }</code></pre>    <p>对于 @RequestBody ,由于其使使用Jackson将Json转换为对象,所以可以使用 @JsonProperty 的value指定别名,例如:</p>    <pre>  <code class="language-java">public String home(@RequestBody User user) {      return "hello " + user.getUserId();  }    class User {      @JsonProperty("user_id")      private long userId;  }</code></pre>    <p>但是使用对象的属性接受参数时,就无法直接通过上面的办法指定别名了,例如:</p>    <pre>  <code class="language-java">public String home(User user) {      return "hello " + user.getUserId();  }</code></pre>    <p>这时候需要使用DataBinder手动绑定属性和别名,我在StackOverFlow上找到的 <a href="/misc/goto?guid=4959674954836689754" rel="nofollow,noindex">这篇文章</a> 是个不错的办法,这里就不重复造轮子了。</p>    <h3>关闭默认通过请求的后缀名判断Content-Type</h3>    <p>之前接手的项目的开发习惯是使用.html作为请求的后缀名,这在Struts2上是没有问题的(因为本身Struts2处理Json的几种方法就都很烂)。但是我接手换成Spring MVC后,使用 @ResponseBody 返回对象时就会报找不到转换器错误。</p>    <p>这是因为Spring MVC默认会将后缀名为.html的请求的Content-Type认为是 text/html ,而 @ResponseBody 返回的Content-Type是 application/json ,没有任何一种转换器支持这样的转换。所以需要手动将通过后缀名判断Content-Type的设置关掉,并将默认的Content-Type设置为 application/json :</p>    <pre>  <code class="language-java">@Configuration  public class WebMvcConfig extends WebMvcConfigurerAdapter {        @Override      public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {          configurer.favorPathExtension(false).                  defaultContentType(MediaType.APPLICATION_JSON);      }  }</code></pre>    <h3>更改默认的Json序列化方案</h3>    <p>项目中有时候会有自己独特的Json序列化方案,例如比较常用的使用 0 / 1 替代 false / true ,或是通过 "" 代替 null ,由于 @ResponseBody 默认使用的是 MappingJackson2HttpMessageConverter ,只需要将自己实现的 ObjectMapper 传入这个转换器:</p>    <pre>  <code class="language-java">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>    <h3>自动加密/解密请求中的Json</h3>    <p>涉及到 @RequestBody 和 @ResponseBody 的类型转换问题一般都在 MappingJackson2HttpMessageConverter 中解决,想要自动加密/解密只需要继承这个类并重写 readInternal / writeInternal 方法即可:</p>    <pre>  <code class="language-java">@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>    <h3>基于注解的敏感词过滤功能</h3>    <p>项目需要对用户发布的内容进行过滤,将其中的敏感词替换为 * 等特殊字符。大部分Web项目在处理这方面需求时都会选择过滤器( Filter ),在过滤器中将 Request 包上一层 Wrapper ,并重写其 getParameter 等方法,例如:</p>    <pre>  <code class="language-java">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>    <p>但是这样做会有一些明显的问题,比如无法控制具体对哪些信息进行过滤。如果用户注册的邮箱或是密码中也带有 fuck 之类的敏感词,那就属于误伤了。</p>    <p>所以改用Spring MVC的Formatter进行拓展,只需要在 @RequestParam 的参数上使用 @SensitiveFormat 注解,Spring MVC就会在注入该属性时自动进行敏感词过滤。既方便又不会误伤,实现方法如下:</p>    <p>声明 @SensitiveFormat 注解:</p>    <pre>  <code class="language-java">@Target({ElementType.FIELD, ElementType.PARAMETER})  @Retention(RetentionPolicy.RUNTIME)  @Documented  public @interface SensitiveFormat {  }</code></pre>    <p>创建 SensitiveFormatter 类。实现 Formatter 接口,重写 parse 方法(将接收到的内容转换成对象的方法),在该方法中对接收内容进行过滤:</p>    <pre>  <code class="language-java">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>    <p>创建 SensitiveFormatAnnotationFormatterFactory 类,实现 AnnotationFormatterFactory 接口,将 @SensitiveFormat 与 SensitiveFormatter 绑定:</p>    <pre>  <code class="language-java">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>    <p>最后将 SensitiveFormatAnnotationFormatterFactory 注册到Spring MVC中:</p>    <pre>  <code class="language-java">@Configuration  public class WebMvcConfig extends WebMvcConfigurerAdapter {        @Override      public void addFormatters(FormatterRegistry registry) {          registry.addFormatterForFieldAnnotation(new SensitiveFormatAnnotationFormatterFactory());          super.addFormatters(registry);      }  }</code></pre>    <h3>记录请求的返回内容</h3>    <p>这里提供一种比较通用的方法,基于过滤器实现,所以在非Spring MVC的项目也可以使用。</p>    <p>首先导入 commons-io :</p>    <pre>  <code class="language-java"><dependency>    <groupId>commons-io</groupId>    <artifactId>commons-io</artifactId>    <version>2.4</version>  </dependency></code></pre>    <p>需要用到这个库中的 TeeOutputStream ,这个类可以将一个将内容同时输出到两个分支的输出流,将其封装为 ServletOutputStream :</p>    <pre>  <code class="language-java">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>    <p>然后创建一个过滤器,将原有的 response 的 getOutputStream 方法重写:</p>    <pre>  <code class="language-java">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>    <p>将 super.getOutputStream() 和 ByteArrayOutputStream 分别作为两个分支流,前者会将内容返回给客户端,后者使用 toString 方法即可获得输出内容。</p>    <p> </p>    <p>来自: <a href="/misc/goto?guid=4959674954918551736" rel="nofollow">http://h2ex.com/1198</a></p>    <p> </p>