文档章节

一些Spring MVC的使用技巧

iBase4J
 iBase4J
发布于 2016/07/01 18:14
字数 1602
阅读 44
收藏 2

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;
}

详细分析: 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;
}

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

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);
    }
}

更改默认的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);
    }
}

自动加密/解密请求中的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());
}

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

项目需要对用户发布的内容进行过滤,将其中的敏感词替换为 * 等特殊字符。大部分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() {

    }
}

但是这样做会有一些明显的问题,比如无法控制具体对哪些信息进行过滤。如果用户注册的邮箱或是密码中也带有 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;
    }
}

创建 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();
    }
}

最后将 SensitiveFormatAnnotationFormatterFactory 注册到Spring MVC中:

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

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

 

本文转载自:

共有 人打赏支持
iBase4J

iBase4J

粉丝 144
博文 23
码字总数 8907
作品 1
郑州
后端工程师
私信 提问
Spring MVC 到 Spring Boot 的简化之路

背景 从Servlet技术到Spring和Spring MVC,开发Web应用变得越来越简捷。但是Spring和Spring MVC的众多配置有时却让人望而却步,相信有过Spring MVC开发经验的朋友能深刻体会到这一痛苦。因为...

别打我会飞
2018/10/21
0
0
Spring MVC 到 Spring BOOT的简化之路

背景 从Servlet技术到Spring和Spring MVC,开发Web应用变得越来越简捷。但是Spring和Spring MVC的众多配置有时却让人望而却步,相信有过Spring MVC开发经验的朋友能深刻体会到这一痛苦。因为...

临江仙卜算子
2018/05/09
0
0
sumk-tool

sumk是一款集合IOC、ORM、微服务、web服务、等功能的框架。有些时候只是想用sumk的分布式锁等工具,也有些时候项目中已经使用了spring mvc、dubbo等框架,只想使用sumk中的一些简单工具。这时...

游夏-sumk
01/05
0
0
从Spring MVC 到 Spring BOOT的简化道路

背景 从Servlet技术到Spring和Spring MVC,开发Web应用变得越来越简捷。但是Spring和Spring MVC的众多配置有时却让人望而却步,相信有过Spring MVC开发经验的朋友能深刻体会到这一痛苦。因为...

微笑向暖wx
2018/11/13
0
0
spring boot与spring mvc的区别是什么?

spring boot与spring mvc的区别是什么? 转载:https://blog.csdn.net/u014590757/article/details/79602309 spring boot只是一个配置工具,整合工具,辅助工具. springmvc是框架,项目中实际运...

Elsa晓冰
2018/09/29
0
0

没有更多内容

加载失败,请刷新页面

加载更多

分布式项目(五)iot-pgsql

书接上回,在Mapping server中,我们已经把数据都整理好了,现在利用postgresql存储历史数据。 iot-pgsql 构建iot-pgsql模块,这里我们写数据库为了性能考虑不在使用mybatis,换成spring jd...

lelinked
今天
2
0
一文分析java基础面试题中易出错考点

前言 这篇文章主要针对的是笔试题中出现的通过查看代码执行结果选择正确答案题材。 正式进入题目内容: 1、(单选题)下面代码的输出结果是什么? public class Base { private Strin...

一看就喷亏的小猿
今天
1
0
cocoapods 用法

cocoapods install pod install 更新本地已经install的仓库 更新所有的仓库 pod update --verbose --no-repo-update 更新制定的仓库 pod update ** --verbose --no-repo-update...

HOrange
今天
3
0
linux下socket编程实现一个服务器连接多个客户端

使用socekt通信一般步骤 1)服务器端:socker()建立套接字,绑定(bind)并监听(listen),用accept()等待客户端连接。 2)客户端:socker()建立套接字,连接(connect)服务器,连接上后...

shzwork
昨天
3
0
android自定义viewgroup画背景

设计部要求背景实现一个背景边框带圆弧的效果: 所以想着用自定义控件画一个背景。 为了方便,继承的是LinearLayout,在onMeasure中先获取控件宽高: @Overrideprotected void onMeasure(in...

醉雨
昨天
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部