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

记录请求的返回内容

这里提供一种比较通用的方法,基于过滤器实现,所以在非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();
}
}

然后创建一个过滤器,将原有的 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() { }
}

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

Spring MVC学习总结(6)——一些Spring MVC的使用技巧的更多相关文章

  1. Spring MVC 学习总结(一)——MVC概要与环境配置 转载自【张果】博客

    Spring MVC 学习总结(一)--MVC概要与环境配置   目录 一.MVC概要 二.Spring MVC介绍 三.第一个Spring MVC 项目:Hello World 3.1.通过Mave ...

  2. Spring MVC 学习笔记2 - 利用Spring Tool Suite创建一个web 项目

    Spring MVC 学习笔记2 - 利用Spring Tool Suite创建一个web 项目 Spring Tool Suite 是一个带有全套的Spring相关支持功能的Eclipse插件包. ...

  3. .NET MVC 学习笔记(三)— MVC 数据显示

    . NET MVC 学习笔记(三)—— MVC 数据显示 在目前做的项目中,用的最多的数据展示控件就是table展示(说不是的请走开,不是一路人),以下详细阐述下table的使用方法. 先看效果: 上 ...

  4. ASP.NET MVC 学习笔记-2.Razor语法 ASP.NET MVC 学习笔记-1.ASP.NET MVC 基础 反射的具体应用 策略模式的具体应用 责任链模式的具体应用 ServiceStack.Redis订阅发布服务的调用 C#读取XML文件的基类实现

    ASP.NET MVC 学习笔记-2.Razor语法   1.         表达式 表达式必须跟在“@”符号之后, 2.         代码块 代码块必须位于“@{}”中,并且每行代码必须以“: ...

  5. ASP.NET MVC 学习笔记-7.自定义配置信息 ASP.NET MVC 学习笔记-6.异步控制器 ASP.NET MVC 学习笔记-5.Controller与View的数据传递 ASP.NET MVC 学习笔记-4.ASP.NET MVC中Ajax的应用 ASP.NET MVC 学习笔记-3.面向对象设计原则

    ASP.NET MVC 学习笔记-7.自定义配置信息   ASP.NET程序中的web.config文件中,在appSettings这个配置节中能够保存一些配置,比如, 1 <appSettin ...

  6. SSM(spring mvc+spring+mybatis)学习路径——2-1、spring MVC入门

    目录 2-1 Spring MVC起步 一.回顾Servlet 二.SpringMVC简介 三.搭建SpringMVC第一个案例 四.简单流程及配置 五.使用注解开发Controller 六.参数绑定 ...

  7. SSM(spring mvc+spring+mybatis)学习路径——1-1、spring入门篇

    目录 1-1 Spring入门篇 专题一.IOC 接口及面向接口编程 什么是IOC Spring的Bean配置 Bean的初始化 Spring的常用注入方式 专题二.Bean Bean配置项 Bean ...

  8. Spring MVC 学习总结(一)——MVC概要与环境配置

    一.MVC概要 MVC是模型(Model).视图(View).控制器(Controller)的简写,是一种软件设计规范,用一种将业务逻辑.数据.显示分离的方法组织代码,MVC主要作用是降低了视图与业务 ...

  9. Spring MVC 学习总结(一)——MVC概要与环境配置(IDea与Eclipse示例)

    一.MVC概要 MVC是模型(Model).视图(View).控制器(Controller)的简写,是一种软件设计规范,用一种将业务逻辑.数据.显示分离的方法组织代码,MVC主要作用是降低了视图与业务 ...

  10. Spring框架学习(7)spring mvc入门

    内容源自:spring mvc入门 一.spring mvc和spring的关系 spring mvc是spring框架提供的七层体系架构中的一个层,是spring框架的一部分,是spring用于处理 ...

随机推荐

  1. POJ 1064 Cable master (二分答案,G++不过,C++就过了)

    题目: 这题有点坑,G++过不了,C++能过. 条件:n个数据a[],分成k段,结果精度要求两位小数. 问题:每段最长为多少? 思路:因为精度要求为两位小数,我先把所有的长度a[]*100. 我们对答 ...

  2. php 生成 guid

    function guid( $opt = true ){ // Set to true/false as your default way to do this. if( function_exis ...

  3. xml中单词下面有提示下划线

    xml中单词下面有提示下划线,表示单词拼写错误或者大小写错误

  4. centos安装nvidia驱动

    大部分 Linux 发行版都使用开源的显卡驱动 nouveau,对于 nvidia 显卡来说,还是闭源的官方驱动的效果更好.最明显的一点是,在使用 SAC 拾取震相的时候,使用官方显卡驱动在刷新界面的 ...

  5. NodeJS学习笔记 (8)网络服务-http-server(ok)

    http服务端概览 创建server 几行代码搞定 var http = require('http'); var requestListener = function(req, res){ res. ...

  6. NodeJS 第一天学习

    NodeJS 第一天学习 严格模式 ECMAScript 5的严格模式是采用具有限制性JavaScript变体的一种方式,从而使代码显示地 脱离"马虎模式/稀松模式/懒散模式"(s ...

  7. codevs 1288 埃及分数 (迭代加深搜索)

    题目大意:给你一个分数$a/b$,把它拆解成$\sum_{i=1}^{n}1/ai$的形式,必须保证$ai$互不相同的情况下,尽量保证n最小,其次保证分母最大的分数的分母最小 什么鬼玄学题!!! 因为 ...

  8. python 模块一览

    一个模块可以对应一个文件 同一个模块,可以import多次,但只会被导入一次 模块的导入顺序 导入模块写在文件最上面 自己写的和内置的以及扩展的分开 顺序:内置,扩展,自己写的 按字母大小写排序 一行 ...

  9. 紫书 习题 11-7 UVa 10801 (单源最短路变形)

    把每个电梯口看作一个节点, 然后计算边的权值的时候处理一下, 就ok了. #include<cstdio> #include<vector> #include<queue ...

  10. enterprise architect (EA) 源码生成UML类图,帮助理解项目工程

    用VS看大型工程代码,尤其是很多层类的,很容易头晕,即便是装了visual assist 插件.用VS生成类图吧,只能生成一堆框,只有一些小的类关系有箭头表示.远远不能满足要求.下面介绍建模工具EA来 ...