zoukankan      html  css  js  c++  java
  • spring-mvc 的一些使用技巧(转)

     

     

    APP 服务端的 Token 验证

    通过拦截器对使用了@Authorization注解的方法进行请求拦截,从 http header 中取出 token 信息,验证其是否合法。非法直接返回 401 错误,合法将 token 对应的 user key 存入 request 中后继续执行。具体实现代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    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 对象并注入到参数中。具体实现代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    @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;
    }

    详细分析:RESTful 登录设计(基于 Spring 及 Redis 的 Token 鉴权)

    源码见:ScienJus/spring-restful-authorization

    封装好的工具类:ScienJus/spring-authorization-manager

    使用别名接受对象的参数

    请求中的参数名和代码中定义的参数名不同是很常见的情况,对于这种情况 Spring 提供了几种原生的方法:

    对于@RequestParam可以直接指定 value 值为别名(@RequestHeader也是一样),例如:

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

    对于@RequestBody,由于其使使用 Jackson 将 Json 转换为对象,所以可以使用@JsonProperty的 value 指定别名,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    public String home (@RequestBody User user) {
    return "hello " + user.getUserId ();
    }
     
    class User {
    @JsonProperty ("user_id")
    private long userId;
    }

    但是使用对象的属性接受参数时,就无法直接通过上面的办法指定别名了,例如:

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

    这时候需要使用 DataBinder 手动绑定属性和别名,我在 Stack Overflow 上找到的 这篇文章 是个不错的办法,这里就不重复造轮子了。

    关闭默认通过请求的后缀名判断 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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Configuration
    public class WebMvcConfig extends WebMvcConfigurerAdapter {
     
    @Override
    public void configureContentNegotiation (ContentNegotiationConfigurer configurer) {
    configurer.favorPathExtension (false).
    defaultContentType (MediaType.APPLICATION_JSON);
    }
    }

    更改默认的 Json 序列化方案

    项目中有时候会有自己独特的 Json 序列化方案,例如比较常用的使用0/1替代false/true,或是通过""代替null,由于@ResponseBody默认使用的是MappingJackson2HttpMessageConverter,只需要将自己实现的ObjectMapper传入这个转换器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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);
    }
    }

    自动加密 / 解密请求中的 Json

    涉及到@RequestBody@ResponseBody的类型转换问题一般都在MappingJackson2HttpMessageConverter中解决,想要自动加密 / 解密只需要继承这个类并重写readInternal/writeInternal方法即可:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @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 ());
    }

    基于注解的敏感词过滤功能

    项目需要对用户发布的内容进行过滤,将其中的敏感词替换为*等特殊字符。大部分 Web 项目在处理这方面需求时都会选择过滤器(Filter),在过滤器中将Request包上一层Wrapper,并重写其getParameter等方法,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    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 () {
     
    }
    }

    但是这样做会有一些明显的问题,比如无法控制具体对哪些信息进行过滤。如果用户注册的邮箱或是密码中也带有fuck之类的敏感词,那就属于误伤了。

    所以改用 Spring MVC 的 Formatter 进行拓展,只需要在@RequestParam的参数上使用@SensitiveFormat注解,Spring MVC 就会在注入该属性时自动进行敏感词过滤。既方便又不会误伤,实现方法如下:

    声明@SensitiveFormat注解:

    1
    2
    3
    4
    5
    @Target ({ElementType.FIELD, ElementType.PARAMETER})
    @Retention (RetentionPolicy.RUNTIME)
    @Documented
    public @interface SensitiveFormat {
    }

    创建SensitiveFormatter类。实现Formatter接口,重写parse方法(将接收到的内容转换成对象的方法),在该方法中对接收内容进行过滤:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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;
    }
    }

    创建SensitiveFormatAnnotationFormatterFactory类,实现AnnotationFormatterFactory接口,将@SensitiveFormatSensitiveFormatter绑定:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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 ();
    }
    }

    最后将SensitiveFormatAnnotationFormatterFactory注册到 Spring MVC 中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Configuration
    public class WebMvcConfig extends WebMvcConfigurerAdapter {
     
    @Override
    public void addFormatters (FormatterRegistry registry) {
    registry.addFormatterForFieldAnnotation (new SensitiveFormatAnnotationFormatterFactory ());
    super.addFormatters (registry);
    }
    }

    记录请求的返回内容

    这里提供一种比较通用的方法,基于过滤器实现,所以在非 Spring MVC 的项目也可以使用。

    首先导入commons-io

    1
    2
    3
    4
    5
    <dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.4</version>
    </dependency>

    需要用到这个库中的TeeOutputStream,这个类可以将一个将内容同时输出到两个分支的输出流,将其封装为ServletOutputStream

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    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 ();
    }
    }

    然后创建一个过滤器,将原有的responsegetOutputStream方法重写:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    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 () {
     
    }
    }

    super.getOutputStream ()ByteArrayOutputStream分别作为两个分支流,前者会将内容返回给客户端,后者使用toString方法即可获得输出内容。

    其他的等想到再补充…

    原文转自 
    ScienJus's Blog

  • 相关阅读:
    多线程操作数据库 异常抛出全部回滚的问题
    递归查询文件夹下面的指定后缀的文件 获取文件的列表
    第二次作业成绩汇总
    第五周助教小结
    第一次作业成绩汇总
    第四周助教小结
    第二次作业-评分
    第一次作业-准备点评
    系统分析与设计团队项目第二次
    系统分析与设计结对项目——WordCount
  • 原文地址:https://www.cnblogs.com/anyehome/p/7782163.html
Copyright © 2011-2022 走看看