文章目录
- 框架版本
- [实现方案 -无代码](#实现方案 -无代码)
- 遇到的问题
-
- SSE客户端失联,报shiro异常
-
- [解决方案一:关闭ErrorPageFilter - 无用](#解决方案一:关闭ErrorPageFilter - 无用)
- [解决方案二:让ShiroFilterFactoryBean在异步线程中使用 - OK](#解决方案二:让ShiroFilterFactoryBean在异步线程中使用 - OK)
- [SseEmitter IOException on HTTP Connection Close](#SseEmitter IOException on HTTP Connection Close)
- 思考
最近在研究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 off、proxy_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
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,保管没错
- 复杂问题,还是要先思考再去技术论坛上找方案