从SpringMVC获取用户信息谈起
- Github地址:https://github.com/andyslin/spring-ext
- 编译、运行环境:JDK 8 + Maven 3 + IDEA + Lombok
- spring-boot:2.1.0.RELEASE(Spring:5.1.2.RELEASE)
- 如要本地运行github上的项目,需要安装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(createInitBinderMethod(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()方法,返回新实现的WebDataBinderFactory实例; - 在
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 +
"' 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自动配置类:
@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获取用户信息谈起的更多相关文章
- 再谈Token认证,如何快速方便获取用户信息
前面我写了一篇<Token认证,如何快速方便获取用户信息>的文章,引起了各位读者的积极参与,除了文章中我提出的三种方式,各位读者大佬们也贡献了其他多种实现方式. 今天决定基于大家提供的思路 ...
- 微信快速开发框架(八)-- V2.3--增加语音识别及网页获取用户信息,代码已更新至Github
不知不觉,版本以每周更新一次的脚步进行着,接下来应该是重构我的代码及框架的结构,有朋友反应代码有点乱,确实如此,当时写的时候只是按照订阅号来写的,后来才慢慢增加到支持API接口.目前还在开发第三方微信 ...
- .NET微信开发通过Access Token和OpenID获取用户信息
本文介绍如何获得微信公众平台关注用户的基本信息,包括昵称.头像.性别.国家.省份.城市.语言. 本文的方法将囊括订阅号和服务号以及自定义菜单各种场景,无论是否有高级接口权限,都有办法来获得用户基本信息 ...
- 微信第三方登陆,无需注册一键登录,获取用户信息,PHP实现方法
今天讲讲利用微信oauth2实现第三方登陆的实现方法. 先说说前提吧! 首先你得是服务号,并且是经过认证的.这样微信会给你很多第三方接口的权限,如果是订阅号或者没有认证的服务号那就不用想了! 一开始你 ...
- QQ登入(2)获取用户信息
private void initView() { mUserInfo = (TextView) findViewById(R.id.user_info); mUserLogo = (ImageVie ...
- [iOS微博项目 - 3.4] - 获取用户信息
github: https://github.com/hellovoidworld/HVWWeibo A.获取用户信息 1.需求 获取用户信息并储存 把用户昵称显示在“首页”界面导航栏的标题上 ...
- Laravel OAuth2 (一) ---简单获取用户信息
前言 本来要求是使用微信进行第三方登陆,所以想着先用 github 测试成功再用微信测试,可是最近拖了好久都还没申请好微信开放平台的 AppID ,所以就只写 github 的第三方登陆吧,估计微信的 ...
- C# 脚本代码自动登录淘宝获取用户信息
C# 脚本代码自动登录淘宝获取用户信息 最近遇到的一个需求是如何让程序自动登录淘宝, 获取用户名称等信息. 其实这个利用SS (SpiderStudio的简称) 实现起来非常简单. 十数行代码就可 ...
- SharePoint 2013 APP 开发示例 (二)获取用户信息
SharePoint 2013 APP 开发示例 (二)获取用户信息 这个示例里,我们将演示如何获取用户信息: 1. 打开 Visual Studio 2012. 2. 创建一个新的 SharePo ...
随机推荐
- ZooKeeper异步调用命令
在ZooKeeper中,所有的同步调用命令,都会有一个相应的异步调用方法.异步调用能在一个单独线程中同时提交更多的命令,也能在一定程度上简化代码实现. 1 异步create方法 如创建zNode的命令 ...
- 设计模式(C#)——02抽象工厂模式
推荐阅读: 我的CSDN 我的博客园 QQ群:704621321 在工厂模式中,一个工厂只能创建一种产品,但我们往往希望,一个工厂能创建一系列产品.很明显工厂模式已经不能满足我们的需 ...
- 给手机端页面留一个调试后门吧(vue)
当我们在浏览器开发vue页面时,由于浏览器对于调试有天然的支持,我们开发起来很方便.但是现在已经进入了移动端时代,移动端页面的需求越来越大. 在开发移动端页面的时候我们通常是在浏览器完成开发完成,之后 ...
- poj 2763 Housewife Wind(树链剖分+单点查询+区间修改)
题目链接:http://poj.org/problem?id=2763 题意:给一个数,边之间有权值,然后两种操作,第一种:求任意两点的权值和,第二,修改树上两点的权值. 题解:简单的树链剖分. #i ...
- java中自定义注解的应用
要想深刻的理解注解,我们必须能实现自己的注解,然后应用自己的注解去实现特定的业务,使用注解可以更优雅的做到某些事情. 有这样一个场景,在需要文件导出时,我们需要将一个model中的一些重要字段导出到c ...
- LVM的创建及管理
创建及管理LVM分区. Lvm(logical volume manager)逻辑卷管理 作用:动态调整磁盘容量,提高磁盘管理的灵活性. 注意:/boot分区用于存放引导文件,不能基于LVM创建. ...
- c++调试在容器释放内存时报Unknown Signal 或 Trace/breakpoint trap异常
在做一道题时,用到的板子中出现了很多的容器的使用,,一开始都是开MAXN大小的容器,,但是有几率出现程序运行完后不正常退出,, 在多次尝试断点调试后,发现主要的异常是程序在结束时,要进行资源的释放,, ...
- springcloud超简单的入门2--Eureka服务治理
Eureka服务治理 下面请听第一个话题,母...咳咳,拿错书了. Eureka简介 eureka是什么呢? 简单来说呢,当我的微服务应用多了起来,一个一个写死再程序里是件很不优雅的事情,而且同一服务 ...
- [淘宝客技术篇005]如何取站点id和推广位id
我们知道,生成一个用于推广的淘客链接,是需要指定对应的站点id和推广位id的,也就是siteid和adzoneid. 今天,火星来客跟大家分享两个不同的方法获取站点id和推广位id. 方法一:直接获取 ...
- 使用Hypothesis生成测试数据
Hypothesis是Python的一个高级测试库.它允许编写测试用例时参数化,然后生成使测试失败的简单易懂的测试数据.可以用更少的工作在代码中发现更多的bug. 安装 pip install hyp ...