• 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,只是覆盖getParameterNamesgetParameterValues(针对表单提交)和getInputStream(针对JSON提交),然后就可以和客户端参数相同的方式获取了。

方式一需要重复解析JWT,而且控制器和Servlet API绑定,不方便测试,但是胜在简单直接。方式二和方式三虽然是一个很好的练习HttpServletRequestWrapper的示例,但是可能还算不上是优雅的获取用户信息的方式。

不妨思考一下:

  • 除了获取userId外,如果还想获取JWT中PAYLOAD的其它信息,能不能做到只修改Controller?还是需要再次修改验证JWT的过滤器Filter呢?
  • HttpServletRequestgetInpustStream()方法,Web容器实现基本都是只能调用一次的,因而方式三在扩展getInpustStream()的时候,先将其转换为byte[],然后为了添加用户信息,再将byte[]反序列化为map,添加用户信息之后又序列化为byte[],反复多次,这种方式性能怎么样?如果是文件上传,这种方式能否行得通?
  • 方式三中HttpServletRequestWrapper会无形中启到屏蔽loginUserId参数的作用,但如果客户端的的确确传入了一个loginUserId的参数(当然,这种情况还是需要尽量避免),在Controller中怎么又获取到客户端的这个参数?

有没有什么其它的方式呢?

SpringMVC中关于参数绑定有很多接口,其中很关键的一个是HandlerMethodArgumentResolver,可以通过添加新实现类来实现获取用户信息吗?当然可以,对应该接口的两个方法,首先要能够识别什么情况下需要绑定用户信息,一般来说,可以根据参数的特殊类型,也可以根据参数的特殊注解;其次要能够获取到用户信息,类似于原文中做的那样。虽然这样做也可以实现功能,但是却很繁琐。

不如抛开怎么获取用户信息不谈,先来看看SpringMVC在控制器的处理方法HandlerMethod中绑定参数是怎么做的?

熟悉SpringMVC处理流程的朋友,自然知道,主控制器是DispatcherServlet,在doDispatch()方法中根据HandlerMapping找到处理器,然后找到可以调用该处理器的HandlerAdapter,其中最常用也最核心的莫过于RequestMappingHandlerMappingHandlerMethodRequestMappingHandlerAdapter组合了。查看RequestMappingHandlerAdapter的源码,找到调用HandlerMethod的方法:

  1. @Override
  2. protected ModelAndView handleInternal(HttpServletRequest request,
  3. HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
  4. ModelAndView mav;
  5. checkRequest(request);
  6. // Execute invokeHandlerMethod in synchronized block if required.
  7. if (this.synchronizeOnSession) {
  8. HttpSession session = request.getSession(false);
  9. if (session != null) {
  10. Object mutex = WebUtils.getSessionMutex(session);
  11. synchronized (mutex) {
  12. mav = invokeHandlerMethod(request, response, handlerMethod);
  13. }
  14. }
  15. else {
  16. // No HttpSession available -> no mutex necessary
  17. mav = invokeHandlerMethod(request, response, handlerMethod);
  18. }
  19. }
  20. else {
  21. // No synchronization on session demanded at all...
  22. mav = invokeHandlerMethod(request, response, handlerMethod);
  23. }
  24. if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
  25. if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
  26. applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
  27. }
  28. else {
  29. prepareResponse(response);
  30. }
  31. }
  32. return mav;
  33. }

可以看到,真正的调用是委托给invokeHandlerMethod()方法了:

  1. @Nullable
  2. protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
  3. HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
  4. ServletWebRequest webRequest = new ServletWebRequest(request, response);
  5. try {
  6. // 创建数据绑定工厂
  7. WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
  8. ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
  9. // 创建可调用的方法
  10. ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
  11. if (this.argumentResolvers != null) {
  12. invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
  13. }
  14. if (this.returnValueHandlers != null) {
  15. invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
  16. }
  17. invocableMethod.setDataBinderFactory(binderFactory);
  18. invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
  19. ModelAndViewContainer mavContainer = new ModelAndViewContainer();
  20. mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
  21. modelFactory.initModel(webRequest, mavContainer, invocableMethod);
  22. mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
  23. // 省略异步处理相关代码
  24. // 这里才是真正的方法调用
  25. invocableMethod.invokeAndHandle(webRequest, mavContainer);
  26. // 处理返回结果
  27. return getModelAndView(mavContainer, modelFactory, webRequest);
  28. }
  29. finally {
  30. webRequest.requestCompleted();
  31. }
  32. }

这个方法很关键,如果需要研读SpringMVC,可以从这个方法着手。不过由于这篇文章关注的是参数绑定,所以这里只关心WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);这句代码,接着看getDataBinderFactory()方法:

  1. private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {
  2. Class<?> handlerType = handlerMethod.getBeanType();
  3. Set<Method> methods = this.initBinderCache.get(handlerType);
  4. if (methods == null) {
  5. methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
  6. this.initBinderCache.put(handlerType, methods);
  7. }
  8. List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();
  9. // Global methods first
  10. this.initBinderAdviceCache.forEach((clazz, methodSet) -> {
  11. if (clazz.isApplicableToBeanType(handlerType)) {
  12. Object bean = clazz.resolveBean();
  13. for (Method method : methodSet) {
  14. initBinderMethods.add(createInitBinderMethod(bean, method));
  15. }
  16. }
  17. });
  18. for (Method method : methods) {
  19. Object bean = handlerMethod.getBean();
  20. initBinderMethods.add(createInitBinderMethod(bean, method));
  21. }
  22. return createDataBinderFactory(initBinderMethods);
  23. }

这个方法前面的代码都是一些准备工作,比如调用ControllerAdvice,最终还是调用createDataBinderFactory()方法:

  1. protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods)
  2. throws Exception {
  3. return new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
  4. }

终于看到数据绑定工厂实例的创建了,方法体非常简单,只有一个new,而且非常幸运,这个方法是protected的,这说明,SpringMVC的设计者原本就预留了扩展点给我们,如果需要扩展数据绑定相关的功能,这里应该是一个不错的入口,具体做法是:

  1. 实现新的WebDataBinderFactory,当然,最好是继承这里的ServletRequestDataBinderFactory
  2. 继承RequestMappingHandlerAdapter,覆盖createDataBinderFactory()方法,返回新实现的WebDataBinderFactory实例;
  3. SpringMVC容器中使用新的RequestMappingHandlerAdapter

我们从后往前看:

有多种方式实现第3步,在SpringBoot应用中,比较简单的是通过向容器注册一个WebMvcRegistrations的实现类,这个接口定义如下:

  1. public interface WebMvcRegistrations {
  2. default RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
  3. return null;
  4. }
  5. default RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
  6. return null;
  7. }
  8. default ExceptionHandlerExceptionResolver getExceptionHandlerExceptionResolver() {
  9. return null;
  10. }
  11. }

实现第二个方法就可以。

第2步更简单,上面已经说明,这里就不赘述了。

再看第1步,查看ServletRequestDataBinderFactory的源码:

  1. public class ServletRequestDataBinderFactory extends InitBinderDataBinderFactory {
  2. public ServletRequestDataBinderFactory(@Nullable List<InvocableHandlerMethod> binderMethods,
  3. @Nullable WebBindingInitializer initializer) {
  4. super(binderMethods, initializer);
  5. }
  6. @Override
  7. protected ServletRequestDataBinder createBinderInstance(
  8. @Nullable Object target, String objectName, NativeWebRequest request) throws Exception {
  9. return new ExtendedServletRequestDataBinder(target, objectName);
  10. }
  11. }

除了构造函数,只定义了一个createBinderInstance()方法(一个工厂类创建一种实例,很熟悉的味道吧?),返回ExtendedServletRequestDataBinder的实例,真正的绑定逻辑在这个类里面,还需要扩展这个类:

  1. public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder {
  2. public ExtendedServletRequestDataBinder(@Nullable Object target) {
  3. super(target);
  4. }
  5. public ExtendedServletRequestDataBinder(@Nullable Object target, String objectName) {
  6. super(target, objectName);
  7. }
  8. @Override
  9. @SuppressWarnings("unchecked")
  10. protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
  11. String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
  12. Map<String, String> uriVars = (Map<String, String>) request.getAttribute(attr);
  13. if (uriVars != null) {
  14. uriVars.forEach((name, value) -> {
  15. if (mpvs.contains(name)) {
  16. if (logger.isWarnEnabled()) {
  17. logger.warn("Skipping URI variable '" + name +
  18. "' because request contains bind value with same name.");
  19. }
  20. }
  21. else {
  22. mpvs.addPropertyValue(name, value);
  23. }
  24. });
  25. }
  26. }
  27. }

要扩展一个类,首先还是找一下有哪些protected方法,可以看到有一个addBindValues()方法,然后再看这个方法被谁调用了,发现在父类ServletRequestDataBinder中有:

  1. public void bind(ServletRequest request) {
  2. MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);
  3. MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class);
  4. if (multipartRequest != null) {
  5. bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
  6. }
  7. // 绑定前添加绑定参数
  8. addBindValues(mpvs, request);
  9. // 执行参数绑定,包括参数格式化、参数校验等
  10. doBind(mpvs);
  11. // 可以添加一些绑定之后的处理
  12. }

至此,已经找到扩展接入点了,为了更好的对扩展开放,引入一个新的接口PropertyValuesProvider

  1. /**
  2. * 属性值提供器接口
  3. */
  4. public interface PropertyValuesProvider {
  5. /**
  6. * 绑定前添加绑定属性,仍然需要经过参数校验
  7. */
  8. default void addBindValues(MutablePropertyValues mpvs, ServletRequest request, Object target, String name) {
  9. }
  10. /**
  11. * 绑定后修改目标对象,修改后的参数不需要经过参数校验
  12. *
  13. */
  14. default void afterBindValues(PropertyAccessor accessor, ServletRequest request, Object target, String name) {
  15. }
  16. }

然后实现新的DataBinder,整个代码如下:

  1. class ArgsBindRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
  2. private final List<PropertyValuesProvider> providers;
  3. public ArgsBindRequestMappingHandlerAdapter(List<PropertyValuesProvider> providers) {
  4. this.providers = providers;
  5. }
  6. @Override
  7. protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods) throws Exception {
  8. return new ArgsBindServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
  9. }
  10. private class ArgsBindServletRequestDataBinderFactory extends ServletRequestDataBinderFactory {
  11. public ArgsBindServletRequestDataBinderFactory(List<InvocableHandlerMethod> binderMethods, WebBindingInitializer initializer) {
  12. super(binderMethods, initializer);
  13. }
  14. @Override
  15. protected ServletRequestDataBinder createBinderInstance(Object target, String objectName, NativeWebRequest request) {
  16. return new ArgsBindServletRequestDataBinder(target, objectName);
  17. }
  18. }
  19. private class ArgsBindServletRequestDataBinder extends ExtendedServletRequestDataBinder {
  20. public ArgsBindServletRequestDataBinder(Object target, String objectName) {
  21. super(target, objectName);
  22. }
  23. /**
  24. * 属性绑定前
  25. */
  26. @Override
  27. protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
  28. super.addBindValues(mpvs, request);
  29. if (null != providers) {
  30. Object target = getTarget();
  31. String name = getObjectName();
  32. providers.forEach(provider -> provider.addBindValues(mpvs, request, target, name));
  33. }
  34. }
  35. /**
  36. * 属性绑定后
  37. */
  38. @Override
  39. public void bind(ServletRequest request) {
  40. super.bind(request);
  41. if (null != providers) {
  42. ConfigurablePropertyAccessor mpvs = getPropertyAccessor();
  43. Object target = getTarget();
  44. String name = getObjectName();
  45. providers.forEach(provider -> provider.afterBindValues(mpvs, request, target, name));
  46. }
  47. }
  48. }
  49. }

最后,加上SpringBoot自动配置类:

  1. @Configuration
  2. public class ArgsBindAutoConfiguration {
  3. @Bean
  4. @ConditionalOnBean(PropertyValuesProvider.class)
  5. @ConditionalOnMissingBean(ArgsBindWebMvcRegistrations.class)
  6. public ArgsBindWebMvcRegistrations argsBindWebMvcRegistrations(List<PropertyValuesProvider> providers) {
  7. return new ArgsBindWebMvcRegistrations(providers);
  8. }
  9. static class ArgsBindWebMvcRegistrations implements WebMvcRegistrations {
  10. private final List<PropertyValuesProvider> providers;
  11. public ArgsBindWebMvcRegistrations(List<PropertyValuesProvider> providers) {
  12. this.providers = providers;
  13. }
  14. @Override
  15. public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
  16. return new ArgsBindRequestMappingHandlerAdapter(providers);
  17. }
  18. }
  19. }

好了,有了新的接口,要实现文章开始的获取用户信息的问题,也就是添加一个新接口PropertyValuesProvider的实现类,并注入到SpringMVC的容器中即可,如果需要获取PAYLOAD中的其它信息,或者有其它的自定义参数绑定逻辑,可以再加几个实现类。

在我的Github上有一个简单的测试示例,有兴趣的朋友不妨一试。

从SpringMVC获取用户信息谈起的更多相关文章

  1. 再谈Token认证,如何快速方便获取用户信息

    前面我写了一篇<Token认证,如何快速方便获取用户信息>的文章,引起了各位读者的积极参与,除了文章中我提出的三种方式,各位读者大佬们也贡献了其他多种实现方式. 今天决定基于大家提供的思路 ...

  2. 微信快速开发框架(八)-- V2.3--增加语音识别及网页获取用户信息,代码已更新至Github

    不知不觉,版本以每周更新一次的脚步进行着,接下来应该是重构我的代码及框架的结构,有朋友反应代码有点乱,确实如此,当时写的时候只是按照订阅号来写的,后来才慢慢增加到支持API接口.目前还在开发第三方微信 ...

  3. .NET微信开发通过Access Token和OpenID获取用户信息

    本文介绍如何获得微信公众平台关注用户的基本信息,包括昵称.头像.性别.国家.省份.城市.语言. 本文的方法将囊括订阅号和服务号以及自定义菜单各种场景,无论是否有高级接口权限,都有办法来获得用户基本信息 ...

  4. 微信第三方登陆,无需注册一键登录,获取用户信息,PHP实现方法

    今天讲讲利用微信oauth2实现第三方登陆的实现方法. 先说说前提吧! 首先你得是服务号,并且是经过认证的.这样微信会给你很多第三方接口的权限,如果是订阅号或者没有认证的服务号那就不用想了! 一开始你 ...

  5. QQ登入(2)获取用户信息

    private void initView() { mUserInfo = (TextView) findViewById(R.id.user_info); mUserLogo = (ImageVie ...

  6. [iOS微博项目 - 3.4] - 获取用户信息

    github: https://github.com/hellovoidworld/HVWWeibo   A.获取用户信息 1.需求 获取用户信息并储存 把用户昵称显示在“首页”界面导航栏的标题上   ...

  7. Laravel OAuth2 (一) ---简单获取用户信息

    前言 本来要求是使用微信进行第三方登陆,所以想着先用 github 测试成功再用微信测试,可是最近拖了好久都还没申请好微信开放平台的 AppID ,所以就只写 github 的第三方登陆吧,估计微信的 ...

  8. C# 脚本代码自动登录淘宝获取用户信息

    C# 脚本代码自动登录淘宝获取用户信息   最近遇到的一个需求是如何让程序自动登录淘宝, 获取用户名称等信息. 其实这个利用SS (SpiderStudio的简称) 实现起来非常简单. 十数行代码就可 ...

  9. SharePoint 2013 APP 开发示例 (二)获取用户信息

    SharePoint 2013 APP 开发示例 (二)获取用户信息 这个示例里,我们将演示如何获取用户信息: 1. 打开 Visual Studio 2012. 2. 创建一个新的  SharePo ...

随机推荐

  1. Spring学习之旅(十五)--SpringBoot

    在使用 Spring 的过程中,有时候会出现一些 ClassNotFoundException 异常,这是因为 JAR 依赖之间的版本不匹配所导致的.而 Spring Boot 就能避免绝大多数依赖版 ...

  2. PHP CURL根据详细地址获取腾讯地图经纬度

    <?php $address = "广东省广州市天河区"; $point = getPoint($address); var_dump($point);//输出经纬度 /** ...

  3. 2019icpc南京网络赛_F_Greedy Sequence

    题意 题意不明,队友告诉我对于每个\(i\),所在下标\(p[i]\),在\([p[i]-k,p[i]+k]\)中找到小于\(i\)的最大数\(x\),然后\(ans[i]=ans[x]+1\)即可. ...

  4. 基于 Javassist 和 Javaagent 实现动态切面

    一.背景介绍 1.需求说明 需求是在程序运行期间,向某个类的某个方法前.后加入某段业务代码,或者直接替换整个方法的业务逻辑,即业务方法客制化.注意是运行期间动态更改,做到无侵入,而不是事先在代码中写死 ...

  5. 在.net core web api项目中安装swagger展示api接口(相当于生成api文档)

    1,  建立或打开项目后,在“程序包管理器控制台”中执行以下命令添加包引用: Install-Package Swashbuckle.AspNetCore 2,在项目中打开Startup.cs文件,找 ...

  6. CodeForces -Codeforces Round #496 (Div. 3) E2. Median on Segments (General Case Edition)

    参考:http://www.cnblogs.com/widsom/p/9290269.html 传送门:http://codeforces.com/contest/1005/problem/E2 题意 ...

  7. CodeForces 758 C Unfair Poll

    Unfair Poll 题意:一共有n排同学每排同学有m个人, 老师问问题有一个顺序, 先从第一排开始问,问完第一排的所有同学之后,再问第2排的,对于所有排的访问顺序为 1,2,3……n-1,n,n- ...

  8. CodeM资格赛 优惠券

    [编程|1000分] 优惠券 时间限制:1秒空间限制:32768K 题目描述 美团点评上有很多餐馆优惠券,用户可以在美团点评App上购买.每种优惠券有一个唯一的正整数编号.每个人可以拥有多张优惠券,但 ...

  9. 联邦学习开源框架FATE助力腾讯神盾沙箱,携手打造数据安全合作生态

    近日,微众银行联邦学习FATE开源社区迎来了两位新贡献者——来自腾讯的刘洋及秦姝琦,作为云计算安全领域的专家,两位为FATE构造了新的功能点,并在Github上提交修复了相关漏洞.(Github项目地 ...

  10. vsnprint参数和意义

    _vsnprintf(char *str, size_t size, const char *format, va_list ap) char *str [out],把生成的格式化的字符串存放在这里. ...