Spring Security 阻止 WebSocket (SockJS)。

4
在我的一个项目中,我配置了rest服务和websockets,两者都经过spring安全过滤器检查JWT。对于客户端的websockets,应用程序使用sockjs和stomp(在Angular2上),而服务器端使用Spring websockets(Tomcat 8)。当我启用Spring安全性打开连接时,两秒后就会出现以下错误。但是,如果我没有启用spring安全性,则连接不会断开。 Error I get in firefox after two seconds angular2 connect()/subscribe()/send() - 都需要JWT令牌。

public connect() : void {
        let sockjs = new SockJS('/rest/add?jwt=' + this.authService.getToken());
        let headers : any = this.authService.getAuthHeader();
        this.stompClient = Stomp.over(sockjs);
        this.stompClient.connect(this.token, (frame) => {
            this.log.d("frame", "My Frame: " + frame);
            this.log.d("connected()", "connected to /add");
            this.stompClient.subscribe('/topic/addMessage', this.authService.getAuthHeader(), (stompResponse) => {
                // this.stompSubject.next(JSON.parse(stompResponse.body));
                this.log.d("result of WS call: ", JSON.parse(stompResponse.body).message);
            }, (error) => {
                this.log.d(error);
            });
        });
    }

    public send(payload: string) {
        this.stompClient.send("/app/add", this.token, JSON.stringify({'message': payload}));
    }

JwtAuthenticationFilter.java

public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public JwtAuthenticationFilter() {
        super("/rest/**");
    }

    @Override
    protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
        return true;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String token = null;

        String param = request.getParameter("jwt");
        if(param == null) {
            String header = request.getHeader("Authorization");
            if (header == null || !header.startsWith("Bearer ")) {
                throw new JwtAuthenticationException("No JWT token found in request headers");
            }
            token = header.substring(7);
        } else {
            token = param;
        }
        JwtAuthenticationToken authRequest = new JwtAuthenticationToken(token);

        return getAuthenticationManager().authenticate(authRequest);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        super.successfulAuthentication(request, response, chain, authResult);

        // As this authentication is in HTTP header, after success we need to continue the request normally
        // and return the response as if the resource was not secured at all
        chain.doFilter(request, response);
    }
}

JwtAuthenticationProvider.java

@Service
public class JwtAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    @Autowired
    private SecurityService securityService;

    @Override
    public boolean supports(Class<?> authentication) {
        return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
    }

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    }

    @Override
    @Transactional(readOnly=true)
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;
        String token = jwtAuthenticationToken.getToken();

        User user = securityService.parseToken(token);

        if (user == null) {
            throw new JwtAuthenticationException("JWT token is not valid");
        }

        return new AuthenticatedUser(user);
    }
}

JwtAuthenticationSuccessHandler.java

@Service
public class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        // We do not need to do anything extra on REST authentication success, because there is no page to redirect to
    }

}

RestAuthenticationEntryPoint.java

@Service
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        // This is invoked when user tries to access a secured REST resource without supplying any credentials
        // We should just send a 401 Unauthorized response because there is no 'login page' to redirect to
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}

Websocket 配置:

<websocket:message-broker
    application-destination-prefix="/app">
    <websocket:stomp-endpoint path="/add">
        <websocket:sockjs />
    </websocket:stomp-endpoint>
    <websocket:simple-broker prefix="/topic, /queue" />
</websocket:message-broker>

我的Spring Security

<context:component-scan base-package="com.myapp.ws.security"/>

<sec:global-method-security pre-post-annotations="enabled" />

<!-- everyone can try to login -->
<sec:http pattern="/rest/login/" security="none" />
<!--<sec:http pattern="/rest/add/**" security="none" />-->

<!-- only users with valid JWT can access protected resources -->
<sec:http pattern="/rest/**" entry-point-ref="restAuthenticationEntryPoint" create-session="stateless">
    <!-- JWT is used to disabled-->
    <sec:csrf disabled="true" />
    <!-- don't redirect to UI login form -->
    <sec:custom-filter before="FORM_LOGIN_FILTER" ref="jwtAuthenticationFilter" />
</sec:http>

<bean id="jwtAuthenticationFilter" class="com.myapp.ws.security.JwtAuthenticationFilter">
    <property name="authenticationManager" ref="authenticationManager" />
    <property name="authenticationSuccessHandler" ref="jwtAuthenticationSuccessHandler" />
</bean>

<sec:authentication-manager alias="authenticationManager">
    <sec:authentication-provider ref="jwtAuthenticationProvider" />
</sec:authentication-manager>

我刚刚看了错误屏幕,似乎您正在尝试使用xdr_streaming进行连接,而不是使用websockets进行连接。 - user1516873
但是当WebSockets的安全性被禁用时,"<sec:http pattern="/rest/add/**" security="none" />"一切都正常工作,消息被传递到服务器,然后传递给所有订阅用户。@user1516873你可以检查并构建,它是Maven,并且可以在5分钟内轻松构建。 - Lukasz
看起来在启用Spring安全性的情况下,Websockets部分运行良好,至少在我的环境中是这样。请检查日志 https://pastebin.com/128L4rkz ,可能是代理问题或客户端浏览器不支持Websockets? - user1516873
@user1516873,如果您在日志的最后一行看到“好极了,我收到了...(被截断)”,那应该是“好极了,我收到了...111111111”,因为您发送的消息是111111111。浏览器兼容,我遇到了和您完全相同的问题。当我检查浏览器日志(请参见附加的图像)时,它会显示“headers is null”,这告诉我当您进行成功的握手时,Spring security没有在响应中附加应该有的一些标头。我使用了几个浏览器进行了检查,当安全性被禁用时,它们都可以正常工作,而当启用时,则全部都无法工作。 - Lukasz
不,只是记录器截断了您的JSON,因为它太长了。完整的消息在这里,并且已经正确传递给simpTemplate。为什么客户端没有收到它 - 这就是问题所在,但Websockets工作正常。 - user1516873
显示剩余2条评论
2个回答

2

你的问题与安全无关。你只是在Stomp连接和订阅函数中传递了错误的参数。

如果您需要传递其他标头,connect()方法还接受两个其他变体:

client.connect(headers, connectCallback);
client.connect(headers, connectCallback, errorCallback);

其中header是一个映射,connectCallback和errorCallback是函数。

this.stompClient.connect(this.token, (frame) => {

应该是

this.stompClient.connect({}, (frame) => {

您可以使用subscribe()方法订阅目的地。该方法需要两个必填参数:destination(对应目的地的字符串)和callback(一个带有一个message参数和一个可选的headers参数的JavaScript函数)。

var subscription = client.subscribe("/queue/test", callback);

this.stompClient.subscribe('/topic/addMessage', this.authService.getAuthHeader(), (stompResponse) => {

应该是

this.stompClient.subscribe('/topic/addMessage', (stompResponse) => {

文档 http://jmesnil.net/stomp-websocket/doc/


不幸的是,那不是解决方案,请检查Github上的最新代码,我已经按照您建议的进行了更改,但没有帮助。如果您可以看一下我的第二篇帖子和请求的JSON文件链接,再次感谢。 - Lukasz

1

@user1516873,最终我解决了它:

  • 传递正确的参数到STOMP解决了一个问题
  • 添加{transports: ["websocket"]}不是必需的(没有它也可以工作)

问题在于我正在使用端口4200上的angular-cli服务器,并使用以下代理文件:

{ "/rest": { "target": "http://localhost:8080", "secure": false } }

但应该像这样:

{ "/rest": { "target": "http://localhost:8080", "secure": false, "ws": true, "logLevel": "debug" } }

所以通过所有配置组合时,我总是通过4200代理服务器进行检查,很少直接通过本机8080进行检查。我只是不知道当应用spring security时,angular-cli代理不支持。谢谢你的帮助!


1
很高兴你让它工作了。{transports: ["websocket"]} 实际上 限制 了 SocketJs 只能使用 websocket 协议,因此如果它无法通过 websocket 连接,它会立即失败,而不是尝试使用不同的协议进行故障转移(如 xhr_streaming)。 - user1516873
这基本上非常有希望,因此建议如果不必要,不要使用{transports:["websocket"]},因为使用它将失去对其他协议的故障转移支持。 - Lukasz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接