从SpringMVC获取用户信息谈起
2019-09-30 10:15
1461 查看
- Github地址:https://github.com/andyslin/spring-ext
- 编译、运行环境:JDK 8 + Maven 3 + IDEA + Lombok
- spring-boot:2.1.0.RELEASE(Spring:5.1.2.RELEASE)
- 如要本地运行g 20c4 ithub上的项目,需要安装lombok插件
上周末拜读了一位牛人的公众号文章<<Token认证,如何快速方便获取用户信息>>,语言风趣,引人入胜,为了表示涛涛敬仰之情,已经转载到自己的公众号了。
回顾一下文章内容,为了在
Controller的方法中获取已经认证过的用户信息(比如通过JWT-JSON Web Token传输的
Token),文中提供了三种方式:
- 方式一(很挫)直接在
Controller
方法中获取Token
头,然后解析; - 方式二(优雅)在过滤器
Filter
中验证JWT
后,直接使用HttpServletRequestWrapper
偷梁换柱,覆盖getHeader
方法,然后在Controller
方法中调用getHeader
,这样就不需要再次解析了; - 方式三(很优雅)同样在过滤器
Filter
中使用HttpServletRequestWrapper
,只是覆盖getParameterNames
、getParameterValues
(针对表单提交)和getInputStream
(针对JSON提交),然后就可以和客户端参数相同的方式获取了。
方式一需要重复解析
JWT,而且控制器和
Servlet API绑定,不方便测试,但是胜在简单直接。方式二和方式三虽然是一个很好的练习
HttpServletRequestWrapper的示例,但是可能还算不上是优雅的获取用户信息的方式。
不妨思考一下:
- 除了获取
userId
外,如果还想获取JWT中PAYLOAD
的其它信息,能不能做到只修改Controller
?还是需要再次修改验证JWT
的过滤器Filter
呢? HttpServletRequest
的getInpustStream()
方法,Web容器实现基本都是只能调用一次的,因而方式三在扩展getInpustStream()
的时候,先将其转换为byte[]
,然后为了添加用户信息,再将byte[]
反序列化为map
,添加用户信息之后又序列化为byte[]
,反复多次,这种方式性能怎么样?如果是文件上传,这种方式能否行得通?- 方式三中
HttpServletRequestWrapper
会无形中启到屏蔽loginUserId
参数的作用,但如果客户端的的确确传入了一个loginUserId
的参数(当然,这种情况还是需要尽量避免),在Controller
中怎么又获取到客户端的这个参数?
有没有什么其它的方式呢?
SpringMVC中关于参数绑定有很多接口,其中很关键的一个是
HandlerMethodArgumentResolver,可以通过添加新实现类来实现获取用户信息吗?当然可以,对应该接口的两个方法,首先要能够识别什么情况下需要绑定用户信息,一般来说,可以根据参数的特殊类型,也可以根据参数的特殊注解;其次要能够获取到用户信息,类似于原文中做的那样。虽然这样做也可以实现功能,但是却很繁琐。
不如抛开怎么获取用户信息不谈,先来看看
SpringMVC在控制器的处理方法
HandlerMethod中绑定参数是怎么做的?
熟悉
SpringMVC处理流程的朋友,自然知道,主控制器是
DispatcherServlet,在
doDispatch()方法中根据
HandlerMapping找到处理器,然后找到可以调用该处理器的
HandlerAdapter,其中最常用也最核心的莫过于
RequestMappingHandlerMapping、
HandlerMethod、
RequestMappingHandlerAdapter组合了。查看
RequestMappingHandlerAdapter的源码,找到调用
HandlerMethod的方法:
@Override protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ModelAndView mav; checkRequest(request); // Execute invokeHandlerMethod in synchronized block if required. if (this.synchronizeOnSession) { HttpSession session = request.getSession(false); if (session != null) { Object mutex = WebUtils.getSessionMutex(session); synchronized (mutex) { mav = invokeHandlerMethod(request, response, handlerMethod); } } else { // No HttpSession available -> no mutex necessary mav = invokeHandlerMethod(request, response, handlerMethod); } } else { // No synchronization on session demanded at all... mav = invokeHandlerMethod(request, response, handlerMethod); } if (!response.containsHeader(HEADER_CACHE_CONTROL)) { if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) { applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers); } else { prepareResponse(response); } } return mav; }
可以看到,真正的调用是委托给
invokeHandlerMethod()方法了:
@Nullable protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest = new ServletWebRequest(request, response); try { // 创建数据绑定工厂 WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); // 创建可调用的方法 ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); if (this.argumentResolvers != null) { invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } if (this.returnValueHandlers != null) { invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); } invocableMethod.setDataBinderFactory(binderFactory); invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); ModelAndViewContainer mavContainer = new ModelAndViewContainer(); mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request)); modelFactory.initModel(webRequest, mavContainer, invocableMethod); mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect); // 省略异步处理相关代码 // 这里才是真正的方法调用 invocableMethod.invokeAndHandle(webRequest, mavContainer); // 处理返回结果 return getModelAndView(mavContainer, modelFactory, webRequest); } finally { webRequest.requestCompleted(); } }
这个方法很关键,如果需要研读
SpringMVC,可以从这个方法着手。不过由于这篇文章关注的是参数绑定,所以这里只关心
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);这句代码,接着看
getDataBinderFactory()方法:
private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception { Class<?> handlerType = handlerMethod.getBeanType(); Set<Method> methods = this.initBinderCache.get(handlerType); if (methods == null) { methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS); this.initBinderCache.put(handlerType, methods); } List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>(); // Global methods first this.initBinderAdviceCache.forEach((clazz, methodSet) -> { if (clazz.isApplicableToBeanType(handlerType)) { Object bean = clazz.resolveBean(); for (Method method : methodSet) { initBinderMethods.add(creat 576 eInitBinderMethod(bean, method)); } } }); for (Method method : methods) { Object bean = handlerMethod.getBean(); initBinderMethods.add(createInitBinderMethod(bean, method)); } return createDataBinderFactory(initBinderMethods); }
这个方法前面的代码都是一些准备工作,比如调用
ControllerAdvice,最终还是调用
createDataBinderFactory()方法:
protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods) throws Exception { return new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer()); }
终于看到数据绑定工厂实例的创建了,方法体非常简单,只有一个
new,而且非常幸运,这个方法是
protected的,这说明,
SpringMVC的设计者原本就预留了扩展点给我们,如果需要扩展数据绑定相关的功能,这里应该是一个不错的入口,具体做法是:
- 实现新的
WebDataBinderFactory
,当然,最好是继承这里的ServletRequestDataBinderFactory
; - 继承
RequestMappingHandlerAdapter
,覆盖createDataBinderFactory()
方法,返回新实现的WebDataBinderFact aec ory
实例; - 在
SpringMVC
容器中使用新的RequestMappingHandlerAdapter
。
我们从后往前看:
有多种方式实现第3步,在
SpringBoot应用中,比较简单的是通过向容器注册一个
WebMvcRegistrations的实现类,这个接口定义如下:
public interface WebMvcRegistrations { default RequestMappingHandlerMapping getRequestMappingHandlerMapping() { return null; } default RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() { return null; } default ExceptionHandlerExceptionResolver getExceptionHandlerExceptionResolver() { return null; } }
实现第二个方法就可以。
第2步更简单,上面已经说明,这里就不赘述了。
再看第1步,查看
ServletRequestDataBinderFactory的源码:
public class ServletRequestDataBinderFactory extends InitBinderDataBinderFactory { public ServletRequestDataBinderFactory(@Nullable List<InvocableHandlerMethod> binderMethods, @Nullable WebBindingInitializer initializer) { super(binderMethods, initializer); } @Override protected ServletRequestDataBinder createBinderInstance( @Nullable Object target, String objectName, NativeWebRequest request) throws Exception { return new ExtendedServletRequestDataBinder(target, objectName); } }
除了构造函数,只定义了一个
createBinderInstance()方法(一个工厂类创建一种实例,很熟悉的味道吧?),返回
ExtendedServletRequestDataBinder的实例,真正的绑定逻辑在这个类里面,还需要扩展这个类:
public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder { public ExtendedServletRequestDataBinder(@Nullable Object target) { super(target); } public ExtendedServletRequestDataBinder(@Nullable Object target, String objectName) { super(target, objectName); } @Override @SuppressWarnings("unchecked") protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) { String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; Map<String, String> uriVars = (Map<String, String>) request.getAttribute(attr); if (uriVars != null) { uriVars.forEach((name, value) -> { if (mpvs.contains(name)) { if (logger.isWarnEnabled()) { logger.warn("Skipping URI variable '" + name + "& 105a #39; because request contains bind value with same name."); } } else { mpvs.addPropertyValue(name, value); } }); } } }
要扩展一个类,首先还是找一下有哪些
protected方法,可以看到有一个
addBindValues()方法,然后再看这个方法被谁调用了,发现在父类
ServletRequestDataBinder中有:
public void bind(ServletRequest request) { MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request); MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class); if (multipartRequest != null) { bindMultipart(multipartRequest.getMultiFileMap(), mpvs); } // 绑定前添加绑定参数 addBindValues(mpvs, request); // 执行参数绑定,包括参数格式化、参数校验等 doBind(mpvs); // 可以添加一些绑定之后的处理 }
至此,已经找到扩展接入点了,为了更好的对扩展开放,引入一个新的接口
PropertyValuesProvider:
/** * 属性值提供器接口 */ public interface PropertyValuesProvider { /** * 绑定前添加绑定属性,仍然需要经过参数校验 */ default void addBindValues(MutablePropertyValues mpvs, ServletRequest request, Object target, String name) { } /** * 绑定后修改目标对象,修改后的参数不需要经过参数校验 * */ default void afterBindValues(PropertyAccessor accessor, ServletRequest request, Object target, String name) { } }
然后实现新的
DataBinder,整个代码如下:
class ArgsBindRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter { private final List<PropertyValuesProvider> providers; public ArgsBindRequestMappingHandlerAdapter(List<PropertyValuesProvider> providers) { this.providers = providers; } @Override protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods) throws Exception { return new ArgsBindServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer()); } private class ArgsBindServletRequestDataBinderFactory extends ServletRequestDataBinderFactory { public ArgsBindServletRequestDataBinderFactory(List<InvocableHandlerMethod> binderMethods, WebBindingInitializer initializer) { super(binderMethods, initializer); } @Override protected ServletRequestDataBinder createBinderInstance(Object target, String objectName, NativeWebRequest request) { return new ArgsBindServletRequestDataBinder(target, objectName); } } private class ArgsBindServletRequestDataBinder extends ExtendedServletRequestDataBinder { public ArgsBindServletRequestDataBinder(Object target, String objectName) { super(target, objectName); } /** * 属性绑定前 */ @Override protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) { super.addBindValues(mpvs, request); if (null != providers) { Object target = getTarget(); String name = getObjectName(); providers.forEach(provider -> provider.addBindValues(mpvs, request, target, name)); } } /** * 属性绑定后 */ @Override public void bind(ServletRequest request) { super.bind(request); if (null != providers) { ConfigurablePropertyAccessor mpvs = getPropertyAccessor(); Object target = getTarget(); String name = getObjectName(); providers.forEach(provider -> provider.afterBindValues(mpvs, request, target, name)); } } } }
最后,加上
SpringBoot自动配置类:
[code]@Configuration
public class ArgsBindAutoConfiguration {
@Bean
@ConditionalOnBean(PropertyValuesProvider.class)
@ConditionalOnMissingBean(ArgsBindWebMvcRegistrations.class)
public ArgsBindWebMvcRegistrations argsBindWebMvcRegistrations(List<PropertyValuesProvider> providers) {
return new ArgsBindWebMvcRegistrations(providers);
}
static class ArgsBindWebMvcRegistrations implements WebMvcRegistrations {
private final List<PropertyValuesProvider> providers;
public ArgsBindWebMvcRegistrations(List<PropertyValuesProvider> providers) {
this.providers = providers;
}
@Override
public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
return new ArgsBindRequestMappingHandlerAdapter(providers);
}
}
}
好了,有了新的接口,要实现文章开始的获取用户信息的问题,也就是添加一个新接口
PropertyValuesProvider的实现类,并注入到
SpringMVC的容器中即可,如果需要获取
PAYLOAD中的其它信息,或者有其它的自定义参数绑定逻辑,可以再加几个实现类。
在我的Github上有一个简单的测试示例,有兴趣的朋友不妨一试。
相关文章推荐
- springMVC框架,后台中session添加用户信息和获取用户信息的问题
- 在Linux系统中使用who和whoami命令获取用户信息
- 支付宝开发平台之第三方授权登录与获取用户信息
- 获取微信用户openid及基本信息的的三种方法
- 获取客户端用户信息
- 在InfoPath中如何获取当前用户的信息(Profile)
- 信息设计:如何让用户快速高效的获取信息?
- 微信开发之获取OAuth2.0网页授权认证和获取用户信息进行关联(转:http://playxinz.iteye.com/blog/2249634)
- 根据仿人人客户端教程,编程实现Demo(二)---实现JSON解析人人API2.0,获取用户信息以及新鲜事信息
- sharepoint 获取userprofileManager中用户详细信息
- LDAP方式连接AD获取用户信息
- 怎样获取用户及用户组信息
- 微信公众号开发之授权获取用户信息
- CAS获取用户更多信息
- 微信获取用户地理位置信息-微信jsapi接口
- 微信获取用户信息
- 微信公众号静默授权注册及获取用户信息
- 获取用户所在城市信息
- 分享功能/获取用户信息
- XMPP获取用户信息及修改用户信息