项目场景:
SpringBoot拦截器获取token用户对象优雅地传递到Controller层
问题描述
后端有许多接口都需要请求中携带有正确的Token,这时采用拦截器来验证token,但是每个接口都还是需要解析一遍token,浪费资源,不免显得代码繁琐,臃肿。
解决方案:
我们可以把用户信息存储起来,方便其它地方获取,两种方式:
- 使用ThreadLocal线程存储(推荐)
- 存储到上下文中request.setAttribute
1、编写类TokenUserInfo、UserThreadLocal类
用户实体类
package com.test.entity;
import lombok.Data;
/**
* 用户信息
*
*/
@Data
public class TokenUserInfo {
/**
* 用户id
*/
private String userId;
/**
* 登录帐号
*/
private String loginAccount;
/**
* 用户名
*/
private String loginUserName;
}
封装ThreadLocal便于代码的维护
package com.test.aop;
import com.test.entity.TokenUserInfo;
/**
* ThreadLocal为每个线程单独提供一份存储空间
* 将ThreadLocal进行封装,便于代码的维护和迭代
*/
public class UserThreadLocal {
/**
* 存储用户信息
*/
private static ThreadLocal<TokenUserInfo> userThread = new ThreadLocal<>();
public static void set(TokenUserInfo tokenUserInfo) {
userThread.set(tokenUserInfo);
}
public static TokenUserInfo getUser() {
return userThread.get();
}
public static void remove() {
userThread.remove();
}
}
2、自定义拦截器Interceptor
抛出的异常需要自己捕捉,返回
package com.test.aop;
import com.test.constant.CommonConstant;
import com.test.entity.TokenUserInfo;
import com.test.exception.InvalidTokenException;
import com.test.exception.MyException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Token拦截器
* 拦截器与UserInfoAspect切面的方式选择一种
*/
@Component
@Slf4j
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从header中获取token
String token = request.getHeader(CommonConstant.ACCESS_TOKEN);
// 如果参数中不存在token,则报错
if (StringUtils.isBlank(token)) {
throw new MyException("请求头缺少"+CommonConstant.ACCESS_TOKEN+"参数");
}
try {
// TODO 根据token获取用户信息
// ......
}catch (Exception e){
log.error("获取用户信息失败:", e);
throw new MyException("获取用户信息失败");
}
//模拟已经获取到了用户信息
TokenUserInfo tokenUserInfo = new TokenUserInfo();
tokenUserInfo.setUserId("1227086153ef415896da5819d4fb4c2f");
tokenUserInfo.setLoginAccount("test");
tokenUserInfo.setLoginUserName("测试");
//token失效
if(tokenUserInfo == null){
throw new InvalidTokenException(token);
}
/*
* 存储用户信息方便其它地方获取,两种方式
* 1.使用ThreadLocal线程存储(推荐)
* 2.存储到上下文中request.setAttribute
*/
// 放入线程域
UserThreadLocal.set(tokenUserInfo);
//2.存储到上下文中request.setAttribute
// request.setAttribute(CommonConstant.USER_INFO,tokenUserInfo);
//放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//程序运行结束之后,删除线程,防止内存泄漏
UserThreadLocal.remove();
}
}
3、配置拦截器
排除不需要拦截的路径
package com.test.config;
import com.test.aop.TokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
/**
* WebMvc配置
*/
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 解决跨域问题
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*").allowCredentials(true)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS").maxAge(3600);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
registry.addResourceHandler("/swagger/**").addResourceLocations("classpath:/static/swagger/");
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
}
@Resource
private TokenInterceptor tokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//不拦截的地址
List<String> excludedList = new ArrayList<>();
//swagger地址
excludedList.add("/swagger-ui.html");
excludedList.add("/swagger-ui.html/**");
excludedList.add("/webjars/**");
excludedList.add("/swagger/**");
excludedList.add("/doc.html");
excludedList.add("/doc.html/**");
excludedList.add("/swagger-resources/**");
excludedList.add("/v2/**");
excludedList.add("/favicon.ico");
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/**")//拦截所有请求
.excludePathPatterns(excludedList);//排除的请求
super.addInterceptors(registry);
}
}
4、Controller层获取
package com.test.controller;
import com.test.aop.UserThreadLocal;
import com.test.entity.TokenUserInfo;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@RestController
@Api(tags = "测试接口")
@RequestMapping("/test")
public class TestController {
@RequestMapping(value = "/get", method = RequestMethod.GET)
public String get(HttpServletRequest request) {
/*
* 优雅的获取用户信息,两种方式
* 1.使用ThreadLocal线程存储(推荐)
* 2.存储到上下文中request.setAttribute
*/
TokenUserInfo tokenUserInfo = UserThreadLocal.getUser();
// Object userInfo = request.getAttribute(CommonConstant.USER_INFO);
// TokenUserInfo tokenUserInfo = userInfo != null ? (TokenUserInfo) userInfo : null;
if(tokenUserInfo == null){
throw new InvalidTokenException("request未获取到"+CommonConstant.USER_INFO);
}
return "success";
}
}
拓展
拓展说一下使用Aspect切面的方式获取token,注意Aspect切面的顺序,@Order(较小的数):优先级高
package com.test.aop;
import com.test.constant.CommonConstant;
import com.test.entity.TokenUserInfo;
import com.test.exception.InvalidTokenException;
import com.test.exception.MyException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* 用户信息切面
* 切面与TokenInterceptor拦截器的方式选择一种
*
*/
@Aspect
@Component
@Slf4j
public class UserInfoAspect {
/**
* 所有controller方法,排除getList接口。
*/
@Pointcut("execution(* com.test..controller..*(..)) && !execution(* com.test..controller.TestController.getList(..))")
public void userInfoPointCut() {
// 空注释,避免sonar警告
}
@Around("userInfoPointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//获取request
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
// 从header中获取token
String token = request.getHeader(CommonConstant.ACCESS_TOKEN);
// 如果参数中不存在token,则报错
if (StringUtils.isBlank(token)) {
throw new MyException("请求头缺少"+ CommonConstant.ACCESS_TOKEN+"参数");
}
try {
// TODO 根据token获取用户信息
// ......
}catch (Exception e){
log.error("获取用户信息失败:", e);
throw new MyException("获取用户信息失败");
}
//模拟已经获取到了用户信息
TokenUserInfo tokenUserInfo = new TokenUserInfo();
tokenUserInfo.setUserId("1227086153ef415896da5819d4fb4c2f");
tokenUserInfo.setLoginAccount("test");
tokenUserInfo.setLoginUserName("测试");
//token失效
if(tokenUserInfo == null){
throw new InvalidTokenException(token);
}
try {
/*
* 存储用户信息方便其它地方获取,两种方式
* 1.使用ThreadLocal线程存储(推荐)
* 2.存储到上下文中request.setAttribute
*/
// 放入线程域
UserThreadLocal.set(tokenUserInfo);
//2.存储到上下文中request.setAttribute
// request.setAttribute(CommonConstant.USER_INFO,tokenUserInfo);
//执行方法
return joinPoint.proceed();
// } catch (Exception e){
// //这里可以用catch捕捉异常,因为我配置了全局异常,所以这里就注释掉了
// e.printStackTrace();
} finally {
//程序运行结束之后,删除线程,防止内存泄漏
UserThreadLocal.remove();
}
}
}
使用HandlerInterceptor拦截器与Aspect切面都能达到获取token的效果,但是Aspect切面如果要排除某个接口不是很方便,所以还是推荐HandlerInterceptor拦截器的方式