学习
实践
活动
专区
工具
TVP
写文章
专栏首页丑胖侠根据Token获取用户信息的N种姿势,这种最完美!

根据Token获取用户信息的N种姿势,这种最完美!

Web项目中经常会用token来进行用户的访问验证,那么在获得token之后,如果有很多地方需要根据token获得对应的用户信息,你会怎么获取?

本文给大家提供N种方式,对照一下,看看你的项目中所使用的方式属于哪个Level,是不是要赶快升级一下?

关于token生成、认证部分的操作本文不会涉及,也就是默认token是经过合法性校验的,本文将重点放在之后进行的业务相关处理,即基于token获取用户信息的方式(部分方式需要基于SpringBoot)。

Level1:手动获取

通常token会放在header当中,最低级的获取方式就是直接从header中获取token,然后通过token转换获得userId,示例代码如下:

@GetMapping("/level1")
public Integer level1(HttpServletRequest request) {
    String token = request.getHeader("token");
    log.info("level1 获得的token为:{}", token);
    Integer userId = TokenUtil.getUserIdByToken(token);
    log.info("userId={}", userId);
    return userId;
}

这种方式最简单直观,还可以进一步封装,比如提供一个BaseController,封装公共的部分,本质是一样的,但又引入了继承关系。因此,通常适用于有少数地方使用的场景。如果有大量的地方使用,这样写比较麻烦,不推荐使用,也没什么技术含量。

Level2:过滤器token转userId

在上一种方案中,既然每一次调用都需要进行token和userId的转换,那就通过过滤器将这一转换过程统一处理。在过滤器中获得token,然后转换成userId,再把userId写回到header当中,使用时直接从header中拿userId即可。

先定义过滤器,示例代码如下:

@Slf4j
@Component
public class ArgumentFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        String token = httpRequest.getHeader("token");
        Integer userId = TokenUtil.getUserIdByToken(token);
        log.info("filter获取用户Id={}", userId);
        HttpServletRequestWrapper requestWrapper = new HttpServletRequestWrapper(httpRequest) {
            @Override
            public String getHeader(String name) {
                if ("userId".equals(name)) {
                    return userId + "";
                }
                return super.getHeader(name);
            }
        };
        filterChain.doFilter(requestWrapper, httpResponse);
    }
}

这里主要通过实现Filter接口的doFilter方法(JDK8可用实现需要的接口方法即可),在request中获得token之后,通过HttpServletRequestWrapper将转换之后的userId放置在header当中。

SpringBoot项目中,需要对ArgumentFilter进行相应的配置,指定过滤的URL:

@Configuration
public class FilterConfig {

    @Resource
    private ArgumentFilter argumentFilter;
    
    @Bean
    public FilterRegistrationBean<Filter> registerAuthFilter() {
        FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();
        registration.setFilter(argumentFilter);
        registration.addUrlPatterns("/level2");
        registration.setName("authFilter");
        // 值越小,Filter越靠前
        registration.setOrder(1);
        return registration;
    }
}

此时在Controller中使用如下:

@GetMapping("/level2")
public Integer level2(HttpServletRequest request) {
    Integer userId = Integer.parseInt(request.getHeader("userId"));
    log.info("userId={}", userId);
    return userId;
}

虽然这种方式已经进步了很多,但每次都要获得HttpServletRequest,然后再从其中获得userId,还是有一些不方便。能不能继续改进一下?那继续往下看。

Level3:参数匹配

上一种方式已经处理获得了userId,那么能不能做的更彻底一些,只需要在Controller方法上出现userId,就直接给它赋值呢?来看一下实现:

@Slf4j
@Component
public class ArgumentParamFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        String token = httpRequest.getHeader("token");
        Integer userId = TokenUtil.getUserIdByToken(token);
        log.info("filter获取用户Id={}", userId);
        HttpServletRequestWrapper requestWrapper = new HttpServletRequestWrapper(httpRequest) {
            @Override
            public String[] getParameterValues(String name) {
                if ("userId".equals(name)) {
                    return new String[]{userId.toString()};
                }
                return super.getParameterValues(name);
            }

            @Override
            public Enumeration<String> getParameterNames() {
                Set<String> paramNames = new LinkedHashSet<>();
                paramNames.add("userId");
                Enumeration<String> names = super.getParameterNames();
                while (names.hasMoreElements()) {
                    paramNames.add(names.nextElement());
                }
                return Collections.enumeration(paramNames);
            }
        };
        filterChain.doFilter(requestWrapper, httpResponse);
    }
}

这里从header中获取到token,转换为userId,然后匹配方法的参数名称,如果是userId,那么就将转换之后的userId赋值给对应的参数。相关filter配置与上一个方法一样,不再贴代码,来看一下Controller中的使用:

@GetMapping("/level3")
public Integer level3(Integer userId) {
    log.info("userId={}", userId);
    return userId;
}

只需在Controller中的方法参数上定义userId便可直接赋值,看起来是不是方便很多。但很明显上面只支持get请求,如果是Post方法并且参数是通过body体(Json格式)传输,那么参数往往是一个实体对象,比如User。能否直接将userId注入到User实体当中呢?

@Data
public class User {

    private Integer userId;

    private String name;
}

要实现直接注入到User对象中,还需要进一步改造。在上面的filter中再添加上针对body体传输方式的处理,在HttpServletRequestWrapper中再实现getInputStream方法:

@Override
public ServletInputStream getInputStream() {
    byte[] requestBody;
    try {
        requestBody = StreamUtils.copyToByteArray(request.getInputStream());
        Map map = JsonUtils.toObject(Map.class, new String(requestBody));
        map.put("userId", userId);
        requestBody = JsonUtils.toJson(map).getBytes();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }

    final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
    return new ServletInputStream() {
        @Override
        public int read() {
            return bais.read();
        }

        @Override
        public boolean isFinished() {
            return false;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener readListener) {

        }
    };
}

先读取流中的(JSON格式)数据,然后将信息解析成Map,在Map中添加上userId,再转换成JSON格式,最后再创建一个流将其写出。对应的Controller实现如下:

@PostMapping("/level3Post")
public Integer level3Post(@RequestBody User user) {
    log.info("userId={}", user.getUserId());
    return user.getUserId();
}

通过postman等工具测试一下,就会发现User对象中已经被注入了对应值。

至此,是不是就完美了?好像还有一些瑕疵。

第一个:虽然按照约定定义userId参数即可,但容易误伤,比如某些业务有自身的userId,不小心命名重复了,会有被覆盖的风险。

第二个:参数的名称只能是userId,且不能够灵活的定义其他名称。

第三个:如果想返回更多信息,比如用户(User)的信息,处理就变得更加复杂。而且如果body体传递的参数比较复杂,解析成Map再封装转换有一定的风险和性能问题。

那么,我们再进行改造升级一下,下面示例基于SpringBoot。

Level4:方法参数解析器

Spring提供了多种解析器Resolver,比如常用的统一处理异常的HandlerExceptionResolver。同时,还提供了用来处理方法参数的解析器HandlerMethodArgumentResolver。它包含2个方法:supportsParameter和resolveArgument。其中前者用来判断是否满足某个条件,当满足条件(返回true)则可进入resolveArgument方法进行具体处理操作。

基于HandlerExceptionResolver,我们可以分以下部分来进行实现:

  • 自定义注解@CurrentUser,用于Controller方法上的User参数;
  • 自定义LoginUserHandlerMethodArgumentResolver,实现HandlerMethodArgumentResolver接口,通过supportsParameter检查符合条件的参数,通过resolveArgument方法来将token转换成User对象,并赋值给参数。
  • 注册HandlerMethodArgumentResolver到MVC当中。

下面来看具体的实现,先定义注解@CurrentUser:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
}

注解就是用来做标识用的,标识指定的参数需要进行处理。对于注解了@CurrentUser的参数是由自定义的LoginUserHandlerMethodArgumentResolver来进行判断处理的:

@Component
public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
    
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(CurrentUser.class) &&
                parameter.getParameterType().isAssignableFrom(User.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
                                  NativeWebRequest request, WebDataBinderFactory factory) {
        // header中获取用户token
        String token = request.getHeader("token");
        Integer userId = TokenUtil.getUserIdByToken(token);
        // TODO 根据userId获取User信息,这里省略,直接创建一个User对象。
        User user = new User();
        user.setName("Tom");
        user.setUserId(userId);
        return user;
    }
}

supportsParameter方法中通过两个条件来过滤参数,首先参数需要使用CurrentUser注解,同时参数的类型为User。当满足条件时返回true,进入resolveArgument进行处理。

在resolveArgument中,从header中获取token,然后根据token获取对应User信息,这里可以注入UserService来获得更多的用户信息,然后将构造好的User对象返回。这样,后续就可以将返回的User绑定到Controller中的参数上。

但此时自定义的Resolver并没有生效,还需要添加到MVC当中:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Resource
    private LoginUserHandlerMethodArgumentResolver loginUserHandlerMethodArgumentResolver;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(loginUserHandlerMethodArgumentResolver);
    }

}

至此,便可以在Controller中使用该注解来获取用户信息了,具体使用如下:

@GetMapping("/level4")
public Integer level4(@CurrentUser User user) {
    log.info("userId={},username={}", user.getUserId(), user.getName());
    return user.getUserId();
}

上面介绍了直接注入一个User对象,如果你只需要userId,那么将User对象替换成Integer或Long类型即可。

通过这种形式,使用起来更加方便了,当我们需要获取User信息时,只需在请求的参数中使用@CurrentUser User即可。不需要的地方也不会出现误操作的可能,具有了充分的灵活性和可拓展性。

小结

本文通过一个场景的业务场景,从最基础的实现一路演变到具有一定设计性的实现,涉及到了拦截器、过滤器、注解等一些列的知识点和实战经验。这正是我们在项目开发时中不断演进的过程。你的项目中使用的哪种方式?是不是需要升级或体验一下了?

本文相关源码地址可关注公众号:程序新视界,回复“1004”获得。

最后,个人的视频号也开通了,一分钟给大家讲一个干货知识点,一分钟给大家分享一个职场经验。欢迎关注。

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!
本文分享自作者个人站点/博客:http://blog.csdn.net/wo541075754复制
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • SpringBoot基础篇之@Value中哪些你不知道的知识点

    看到这个标题,有点夸张了啊,@Value 这个谁不知道啊,不就是绑定配置么,还能有什么特殊的玩法不成?

    一灰灰blog
  • SpringBoot基础篇之@Value中哪些你不知道的知识点

    看到这个标题,有点夸张了啊,@Value 这个谁不知道啊,不就是绑定配置么,还能有什么特殊的玩法不成?

    一灰灰blog
  • 你的账号安全吗?

    账号安全无小事,近些年持续不断爆出的安全事件,有很多低级错误其实都是拥有一个健壮的账号体系可以避免的;多次听闻后曾写一写账号安全相关的东西,但直...

    林喜东
  • 从币改到链改—区块链技术都正在重塑企业架构!

    近期关于什么币改、链改成为了热门话题,因为区块链本身比较复杂,它所衍生出来的事物和概念也同样并不简单,就在最近我的粉丝、学员、朋友、同学越来越多的人咨询我什么是...

    自链财经
  • SpringBoot基础系列@Value 之字面量及 SpEL使用知识点介绍篇

    承接上一篇博文【SpringBoot 基础系列】@Value 中哪些你不知道的知识点 中提及到但没有细说的知识点,这一篇博文将来看一下@Value除了绑定配置文...

    一灰灰blog
  • Windows认证及抓密码总结

    windows的认证方式主要有NTLM认证、Kerberos认证两种。同时,Windows Access Token记录着某用户的SID、组ID、Session...

    HACK学习
  • Spring Security 结合 Jwt 实现无状态登录

    在前后端分离的项目中,登录策略也有不少,不过 JWT 算是目前比较流行的一种解决方案了,本文就和大家来分享一下如何将 Spring Security 和 JWT...

    Bug开发工程师
  • SpringBoot基础系列@Value 之字面量及 SpEL使用知识点介绍篇

    承接上一篇博文[【SpringBoot 基础系列】@Value 中哪些你不知道的知识点](http://mp.weixin.qq.com/s?__biz=MzU...

    一灰灰blog
  • Node 概念及中间件

    大发明家
  • Spring Security 结合 Jwt 实现无状态登录

    在前后端分离的项目中,登录策略也有不少,不过 JWT 算是目前比较流行的一种解决方案了,本文就和大家来分享一下如何将 Spring Security 和 JWT...

    江南一点雨
  • PHP代码审计笔记--CSRF跨站请求伪造

      CSRF(Cross-site request forgery)跨站请求伪造。攻击者盗用了你的身份,以你的名义向第三方网站发送恶意请求,对服务器来说这个请求...

    Bypass
  • 认证授权的设计与实现

    每个网站,小到一个H5页面,必有一个登录认证授权模块,常见的认证授权方式有哪些呢?又该如何实现呢?下面我们将来讲解SSO、OAuth等相关知识,并在实践中的应用...

    ruochen
  • 【补充】任意密码重置姿势

    跟第三个有点类似,只判断了接收端和验证码是否一致,未判断接收端是否和用户匹配,因此修改接收端可达到重置目的

    用户1467662
  • 全网最简单的k8s User JWT token管理器

    kubernetes server account的token很容易获取,但是User的token非常麻烦,本文给出一个极简的User token生成方式,让用...

    sealyun
  • 前端网络高级篇(二)身份认证

    网络身份的验证的场景非常普遍,比如用户登陆后才有权限访问某些页面或接口。而HTTP通信是无状态的,无法记录用户的登陆状态,那么,如何做身份验证呢?

    娜姐
  • 转载:都2021年了,你还不懂幂等性问题的解决方案?

    hello,大家好,很抱歉昨天没有发推文,因为昨天在学习自媒体运营的知识,耽搁了,不过今天给大家补上了

    浩说编程
  • 如果被耗时任务拖累,可能是姿势不对

    如果被耗时任务拖累,可能是姿势不对 在业务中,有时候需要处理一些相对耗时的事情,而且还有一些其他的逻辑还可能会依赖这个耗时任务。诚然,太久的耗时会对用户体验不好...

    IMWeb前端团队
  • Spring Cloud中如何保证各个微服务之间调用的安全性

    不是说你想调用就可以调用,一定要有认证机制,是我们内部服务发出的请求,才可以调用我们的接口。

    猿天地

扫码关注腾讯云开发者

领取腾讯云代金券