springboot和shiro组合引入SseEmitter的一些坑

文章目录

最近在研究SSE技术,项目上希望任务进度有更新时提醒web页面局部刷新,于是乎找实现方案,然而打通挺容易,就会报各种异常~ 写篇文章记录下~

框架版本

  • springboot 3.2.4
  • shiro 2.0.4

实现方案 -无代码

思路

总体设计思路:基于主题订阅的推送模型

这里就不贴代码了,简单实现步骤:

  • 用户建立SSE连接
  • 根据角色或用户组织订阅主题
  • 服务端推送相应主题消息

token传递

原有项目中token是在header中,但EeventSource是无法设置header的,所以token需要用其他方式从web端传递到服务端,有两种:

  • JSession
  • 在url中做为param传递

心跳处理

添加定期心跳机制防止连接超时。创建定时器,给客户端发送heartbeat心跳包,若检测到失联则直接移除。

shiro filterchain配置

伪代码:

复制代码
filterChain.add("/platform/sse/**", "anon");

nginx配置

SSE需要Nginx特殊配置:proxy_buffering offproxy_http_version 1.1

复制代码
location /platform/sse/ {
    proxy_pass http://webserverpool;
    proxy_buffering off;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_read_timeout 3600s;

    # 标准代理头
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

遇到的问题

SSE客户端失联,报shiro异常

报错信息如下:

复制代码
If your application is running on WebSphere Application Server you may be able to resolve this problem by setting com.ibm.ws.webcontainer.invokeFlushAfterService to false  
org.apache.shiro.UnavailableSecurityManagerException: No SecurityManager accessible to the calling code, 
either bound to the org.apache.shiro.util.ThreadContext or as a vm static singleton.  

Q:用户端发起SSE长连接的整个逻辑中并未使用shiro的任何方法,为什么还会报shiro异常?

A(from AI): SSE 断连 → 容器触发 error dispatch → Shiro 拦截 → 线程无 Subject → 抛 UnavailableSecurityManagerException

解决方案一:关闭ErrorPageFilter - 无用

java 复制代码
@Bean
public ErrorPageFilter errorPageFilter() {
    return new ErrorPageFilter();
}

@Bean
public FilterRegistrationBean<ErrorPageFilter> disableErrorPageFilter(ErrorPageFilter filter) {
    FilterRegistrationBean<ErrorPageFilter> registrationBean = new FilterRegistrationBean<>(filter);
    registrationBean.setEnabled(false);
    return registrationBean;
}

解决方案二:让ShiroFilterFactoryBean在异步线程中使用 - OK

参考文献:github中shiro源码的issue区

java 复制代码
@Bean
public FilterRegistrationBean<DelegatingFilterProxy> filterRegistrationBean() {
    DelegatingFilterProxy _proxy = new DelegatingFilterProxy();
    _proxy.setTargetBeanName("shiroFilterFactoryBean");
    _proxy.setTargetFilterLifecycle(true);

    FilterRegistrationBean<DelegatingFilterProxy> _registration = new FilterRegistrationBean<>();
    _registration.setFilter(_proxy);
    _registration.setAsyncSupported(true);
    _registration.setDispatcherTypes(ASYNC);
    return _registration;
}

SseEmitter IOException on HTTP Connection Close

要崩溃了,解决完一个异常,又来一个。。。检测到客户端失联在SseEmitter.onError()回调中处理了,也将异常静默了,但还是会抛异常。。。

和AI友好交流了好久,也尝试了N种解决方案,都无效!

@ExceptionHandler静默sse的IOException异常

还是在SseEmitter源码区的issue区找到了答案!!!
SseEmitter IOException on HTTP Connection Close #33832

java 复制代码
@SuppressWarnings("rawtypes")
@ExceptionHandler(IOException.class)
@ResponseBody
public void handleIOException(IOException e, HttpServletRequest request, HttpServletResponse response) throws IOException {
		
		log.debug("[handleIOException] uri:{}", request.getRequestURI());
		// SSE 请求:静默、无响应体,不进行 JSON 渲染
	  if (isSseRequest(request)) {
	      log.warn("[SseExceptionHandler] SSE IO异常已拦截并静默: {}", e.getMessage());
	      // 对 SSE:必须确保不再由 Spring 处理返回值
	      try {
	          response.setStatus(HttpServletResponse.SC_OK);
	          response.flushBuffer(); // 立即结束响应
	      } catch (Exception ignore) {
	          // 再失败也不能抛出去
	      }
	      // 不返回任何内容,不写 ResponseVo,不 forward
	      return;
	  }
	  // other uri doing ... 
}

private boolean isSseRequest(HttpServletRequest request) {
	String uri = request.getRequestURI();
    String accept = request.getHeader("Accept");
	    
    return (uri.contains("/platform/sse/") || "text/event-stream".equalsIgnoreCase(accept));
}

思考

Webflux VS. SseEmitter

Webflux 优势

  • WebFlux使用Flux实现SSE流式推送,通过Sinks.Many支持多播,比传统SseEmitter更高
  • 1000个连接:WebFlux只需几个线程,SseEmitter需要1000个线程

shiro兼容性

shiro是基于Servlet,webflux基于netty

若项目使用WebFlux,则原有shiro的鉴权方式就要进行变更,不划算~

AI辅助工具有感

AI工具确实很好用,但复杂场景给出的方案还是不太ok(有可能使用的方式不对?),所以现在习惯:

  • 简单问题,交给AI,保管没错
  • 复杂问题,还是要先思考再去技术论坛上找方案
相关推荐
老华带你飞2 小时前
健身房|基于springboot + vue健身房管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端
JIngJaneIL2 小时前
基于Java酒店预约系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot
曹牧2 小时前
Java:List<Map<String, String>>转换为字符串
java·开发语言·windows
不会写DN2 小时前
存储管理在开发中有哪些应用?
前端·后端
Unstoppable223 小时前
代码随想录算法训练营第 56 天 | 拓扑排序精讲、Dijkstra(朴素版)精讲
java·数据结构·算法·
qinyia3 小时前
WisdomSSH解决docker run命令中log-opt参数不支持导致的容器创建失败问题
java·docker·eureka
电饭叔3 小时前
不含Luhn算法《python语言程序设计》2018版--第8章14题利用字符串输入作为一个信用卡号之二(识别卡号有效)
java·python·算法
小付爱coding3 小时前
Claude Code安装教程【windows版本】
java·git·python
**蓝桉**4 小时前
数组的执行原理,java程序的执行原理
java·开发语言