项目场景:
题主线上对接qwen-turbo时在进行用户聊天时出现了token解析异常
线上项目在做用户聊天时,需要扣减用户的聊天权益次数,在spring security中定义了一个JwtAuthenticationTokenFilter,用于对用户的token解析,在非流水式的用户聊天对答请求时,逻辑业务处理正常代码如下
/**
* token过滤器 验证token有效性
*
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
tokenService.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}
问题描述
当使用SSE流式返回gpt回答结果时,出现了tokenService.getLoginUser(request)获取header当中的token时出现了报错,获取用户信息异常
public LoginUser getLoginUser(HttpServletRequest request)
{
// 获取请求携带的令牌,此处出现了解析异常,获取到的token为空
String token = getToken(request);
if (StringUtils.isNotEmpty(token))
{
try
{
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
LoginUser user = redisCache.getCacheObject(userKey);
return user;
}
catch (Exception e)
{
log.error("获取用户信息异常'{}'", e.getMessage());
}
}
return null;
}
原因分析:
spring在处理SSE请求时,OncePerRequestFilter并不是只请求了一次根据题主debug发现RequestMappingHandlerAdapter#asyncManager.hasConcurrentResult()会出现多次调用的情况:
@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);
AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.registerCallableInterceptors(this.callableInterceptors);
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
// 该处的代码会重复进入,等待返回结果。
if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult();
mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
asyncManager.clearConcurrentResult();
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(result, !traceOn);
return "Resume with async result [" + formatted + "]";
});
invocableMethod = invocableMethod.wrapConcurrentResult(result);
}
invocableMethod.invokeAndHandle(webRequest, mavContainer);
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}
return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}
解决方案:
一般我们在处理一个HTTP request请求时默认请求一次,OncePerRequestFilter 就是为了这种理念而设计的,通过判断hasAlreadyFilteredAttribute是否已经处理过该请求来进行是否处理或者放行
@Override
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
throw new ServletException("OncePerRequestFilter just supports HTTP requests");
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
//是否已经做过filter处理的请求的名称
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;
// 默认不改配置时为false
if (skipDispatch(httpRequest) || shouldNotFilter(httpRequest)) {
// Proceed without invoking this filter...
filterChain.doFilter(request, response);
}
//判断是否已经处理过该请求,处理过,直接跳过放行
else if (hasAlreadyFilteredAttribute) {
if (DispatcherType.ERROR.equals(request.getDispatcherType())) {
doFilterNestedErrorDispatch(httpRequest, httpResponse, filterChain);
return;
}
// Proceed without invoking this filter...
filterChain.doFilter(request, response);
}
//只有判断请求没有被处理过才会进入到我们的业务判断代码当中
else {
// Do invoke this filter...
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
doFilterInternal(httpRequest, httpResponse, filterChain);
}
finally {
// Remove the "already filtered" request attribute for this request.
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
我们可以根据源码分析知道,OncePerRequestFilter 只要对一个请求处理过就不会再次处理该请求,直接放行,导致第二次进来的处理逻辑没办法获取到用户的token信息,解决上述问题的逻辑也很简单,不要使用OncePerRequestFilter ,使用GenericFilterBean替代就可以解决问题。实现逻辑
@Component
public class JwtAuthenticationTokenFilter extends GenericFilterBean {
@Autowired
private TokenService tokenService;
@Autowired
private JwtUtils jwtUtils;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 获取token
String token = tokenService.getUserToken(request);
if (StringUtils.isBlank(token) || Arrays.asList(SecurityConfig.SMS_LOGIN_URL,SecurityConfig.JSAPI_CONFIG_URL,SecurityConfig.AUTH_WX_PUB_LOGIN,
SecurityConfig.WX_LOGIN_URL, SecurityConfig.WX_LOGIN_BY_CODE_URL).contains(request.getRequestURI())) {
// token为空直接放行
filterChain.doFilter(request, response);
return;
}
// 解析token
// 拿着userId去Redis中获取用户信息
LoginPerfUser userInfo = tokenService.getLoginUser(request);
//Assert.notNull(userInfo, "未登录");
if (ObjectUtil.isEmpty(userInfo)) {
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.UNAUTHORIZED, "授权登陆已过期")));
return;
}
jwtUtils.verifyToken(userInfo);
// 把用户信息存入SecurityContextHolder
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userInfo, null, null);
SecurityContextHolder.getContext().setAuthentication(authentication);
// 放行
filterChain.doFilter(request, response);
}
}