使用 Spring Security 在 Spring 中进行 WebSocket 身份验证

Posted

技术标签:

【中文标题】使用 Spring Security 在 Spring 中进行 WebSocket 身份验证【英文标题】:WeBSocket Authentication in Spring using spring security 【发布时间】:2021-04-29 14:19:23 【问题描述】:

根据spring官方文档:

WebSockets 重用与在 建立 WebSocket 连接时的 HTTP 请求。这意味着 HttpServletRequest 上的 Principal 将被移交给 网络套接字。如果您使用的是 Spring Security,则 HttpServletRequest 被自动覆盖。

更具体地说,确保用户已通过您的 WebSocket 的身份验证 应用程序,只需要确保您设置了 Spring 对基于 HTTP 的 Web 应用程序进行身份验证的安全性。

如果我理解正确,这意味着 WebSocket 自握手以来使用相同的通道进行通信,因此应该在第一次连接时进行身份验证。

但是没有说明如何以标准的安全方式实际验证握手。据我所知,HTTP 在升级到 WebSocket 时不会发送 Authentication 标头,那么它是如何完成的?

我真的需要在连接查询中发送身份验证令牌吗,例如

localhost:8080/ws?Auth=... 

并将安全性留给 HTTPS

或者我是否需要在建立连接后验证 WebSocket,例如创建我自己的握手?

有没有适当的正式方式来做到这一点?我正在使用 RAW websockets。

感谢您的想法/帮助。

【问题讨论】:

As far as I am aware HTTP doesn't send an Authentication header while upgrading to the WebSockets 什么意思,升级?当您执行初始 HTTP 请求时,您会提供身份验证标头或会话 cookie,具体取决于您正在运行的安全类型。 但是,当您想要初始化 WebSockets 时,您会发送一个带有 Upgrade 标头的请求,然后开始与 WebSockets 握手。之后,您将无法更好地使用 Web 套接字的客户端实现,也不会让您发送身份验证标头。我可能对这里的细节有误。请随时纠正我! RFC 指定了以下 tools.ietf.org/html/rfc6455#section-10.5 并且 opning 握手是一个普通的 GET 请求,没有什么花哨的,因此您可以在其中包含标头、cookie 或任何您需要的东西。 tools.ietf.org/html/rfc6455#section-1.3 嗯,很有趣,但是您仍然使用 WebSocket 的客户端库,并且它们都不允许您指定身份验证标头。例如,我为此使用本机 JS websocket()。 好吧,javascript 世界和后端世界之间似乎存在不匹配。 RFC说它是允许的,但没有你说的实现。我找到了这个***.com/questions/4361173/… 【参考方案1】:

我个人使用 STOMP,但使用 STOMP(基本上是一个在原始 WebSocks 上进行通信的框架),会话 cookie(来自 spring security)与任何消息一起通过套接字发送。

您可以使用StompAccessorHeader 喜欢:

  @MessageMapping("/agents/start")
    public void start(StompHeaderAccessor stompHeaderAccessor) 
        log.info("Subscriber Start! -", stompHeaderAccessor.getUser() != null ? stompHeaderAccessor.getUser().getName() : "ANON", stompHeaderAccessor.getSessionId());
        mysessionstore.addSessionId(stompHeaderAccessor.getSessionId());
    

如果不使用 STOMP 框架,是否有办法读取原始套接字上每个请求发送的 SessionCookie?

我不是 100% 确定,但我猜你正在使用 TextWebSocketHandler 实现:

@Override
protected void handleTextMessage(WebSocketSession session, TextMessage textMessage)

我可以在 WebSocketSession 的源代码中看到您应该能够在那里获得您的主要身份验证用户:

https://github.com/spring-projects/spring-framework/blob/0de2833894c24c1e70bde991bad171435c6ecac2/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketSession.java#L37

因此,您可以像 POST "/login" 之类的普通 REST 一样进行身份验证,然后该会话也应该对 websocket 有效。

您可以通过套接字进行身份验证吗?您必须创建自己的套接字端点来获取他们的凭据并执行SecurityContextHolder.getContext().setAuthentication(myAuthUserToken) 但也许这会返回一个会话cookie?你必须测试这个 ofc,因为我不确定它是否会起作用耸耸肩

然后我个人创建一个“存储”(单例或 redis)来保存用户主体和 socketSessionId,这样我就可以将用户与套接字匹配。

您可以说将它们存储在带有HashMap<String,String> userPrincipalNameToSocketSessionId 的单例中,作为存储哪个套接字会话属于哪个用户的粗略方式。

例如。

有点像

@Override
protected void handleTextMessage(WebSocketSession session, TextMessage textMessage)
  MySessionStore.addSessionToMap(session.getPrincipal(),session.getId());
  log.info("Added user  websocket session  to the store.",session.getPrincipal(),session.getId());



public */MySingletonClass*/ MySessionStore

@Getter
public static volatile HashMap<String,String> userPrinciapalToSocketMap = new HashMap<>();

//Method to add to map here
public synchronized static addToMap(String principalName,String webSocketSessionId)
...Adds to the map.


需要 Socket 会话的 JWT 无状态身份验证系统?

据我所知,这个...除非有很多重写和实际上分叉/扩展很多 Spring 类...

你可以做一个控制器:

Http GET =&gt; "/websocket-ticket" 将返回一个带有用户主体/用户名/id 的签名令牌,供用户体验使用,然后在 websocket 连接后作为第一条消息传递。

套接字处理程序TextMessageHandler 可以检查令牌的签名并将其添加到您的HashMap&lt;String,String&gt; principalUserToSessionId 存储中。

安全问题(不太可能但确实存在):

具有 XSS 的攻击者可以窥探该令牌并劫持该 websocket 会话。也许你在比赛条件下获胜(即 MITM 需要更长的时间,并且令牌是一次性使用的......更精彩的实现......你现在还需要一个“websocket-ticket-consumed”商店......)。

https://devcenter.heroku.com/articles/websocket-security

我觉得这一切都在走向 X/Y 问题。 为什么要使用 JWT 进行身份验证?

【讨论】:

“所以你可以像普通的 REST 一样进行身份验证,比如 POST "/login",然后该会话也应该对 websockets 有效。”我正在这样做 - 验证和设置 SecurityContextHolder.getContext().setAuthentication(myAuthUserToken) (我正在使用 JWT 身份验证)。但是,这会将 authToken 设置到上下文中。但是接下来,当 websocket 尝试初始化连接时,新线程用于此连接(在 spring 内部),因此 Context 不保存 authToken,因为它使用 ThreadLocal 。这就是我的问题的根源。 那么您需要向我们展示您的代码,除非您正在做一些疯狂的自定义工作,否则您不需要设置 SecurityContextHolder,为什么不使用 spring 提供的内置身份验证过滤器? 您可以将您的 JWT 作为身份验证后的第一条消息发送到套接字,让 TextMessageHandler 使用/验证该 JWT 以了解哪个用户启动了该套接字会话。我也会添加这个......因为它引起了另一个奇怪的安全问题(为什么你的前端 JS 可以访问令牌......:|)。 嗯,你是什么意思为什么它可以访问?它是否需要与每个身份验证请求一起发送? (是的,spring 正在为 httprequest 或 session 设置主体 - 不确定是哪一个,但它们仍然是 threadlocaly 存储的,所以当不同线程接收到请求时,用户需要重新登录,而所有这些都是JWT没用)你能解释一下吗? 没有。因为一旦您知道 websockets 会话 ID,您就可以将其存储在用户的 id/用户名中。将其存储在 Redis/Singlton/SQL 中……任何最合适的。套接字保持打开直到关闭。套接字会话 ID 在其整个生命周期内都是相同的。不是每条发送的消息。

以上是关于使用 Spring Security 在 Spring 中进行 WebSocket 身份验证的主要内容,如果未能解决你的问题,请参考以下文章

Apache Shiro 与 Spring Security

Spring Cloud 值Spring-Security

使用 Spring Security 'hasPermission()' 在未经授权的 REST 服务请求上返回 JSON

Spring Security 不显示错误消息

Understand Spring Security Architecture and implement Spring Boot Security

使用 Spring Security 实现 LDAP 身份验证时出现 Gradle 依赖关系错误