Web 项目中经常会用 token 来进行用户的访问验证,那么在获得 token 之后,如果有很多地方需要根据 token 获得对应的用户信息,你会怎么获取?本文给大家提供 N 种方式,对照一下,看看你的项目中所使用的方式属于哪个 Level,是不是要赶快升级一下?关于 token 生成、认证部分的操作本文不会涉及,也就是默认 token 是经过合法性校验的,本文将重点放在之后进行的业务相关处理,即基于 token 获取用户信息的方式(部分方式需要基于 SpringBoot)。
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” 获得。
最后,个人的视频号也开通了,一分钟给大家讲一个干货知识点,一分钟给大家分享一个职场经验。欢迎关注。