Nginx反向代理Websocket认证 - HTTP 403

15

我正在将Nginx用作Spring Boot应用程序的反向代理。我还使用带有sockjs和stomp消息的Websockets。

以下是上下文配置。

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

以下是客户端代码:

var socket = new SockJS(entryPointUrl);
var stompClient = Stomp.over(socket);

var _this = this;

stompClient.connect({}, function () {
    stompClient.subscribe('/app/some-url', function (message) {
         // do some stuff
    });
});

我也使用了Spring Security来保护一些内容。

@Configuration
@Order(4)
public static class FrontendSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/js/**", "/css/**", "/webjars/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login").permitAll()
                .and()
                .logout().permitAll();
    }

}

一切都很顺利,除了当我在 Nginx 反向代理后运行此应用程序时。这是反向代理配置:

    proxy_pass http://testsysten:8080;

    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # WebSocket support (nginx 1.4)
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $http_connection;

    # Max body size
    client_max_body_size 10M;

连接总是以 HTTP 403 状态码失败。

我正在使用版本 1.9.7。

你有任何想法,为什么客户端无法通过身份验证吗?

我知道类似的问题,如这个,但解决方案根本不起作用。

更新

我成功地在 HTTP 上运行了应用程序。我需要在 Nginx 配置中传递CSRF令牌。新配置如下:

    proxy_pass http://testsysten:8080;

    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # Pass the csrf token (see https://de.wikipedia.org/wiki/Cross-Site-Request-Forgery)
    # Default in Spring Boot
    proxy_pass_header X-XSRF-TOKEN;

    # WebSocket support (nginx 1.4)
    proxy_http_version 1.1;

唯一缺少的是重定向到 HTTPS。在 Spring 日志中,我看到以下条目:

o.s.w.s.s.t.h.DefaultSockJsService - Processing transport request: GET http://testsystem:80/localization/226/3mbmu212/websocket

看起来Nginx代理需要重写到正确的端口。

5个回答

17

我自己解决了这个问题。基本上,如果你想使用Websocket和Spring Security,Nginx需要传递一些额外的头部信息。下面这些行需要添加到您的Nginx配置文件的location部分:

    # Pass the csrf token (see https://de.wikipedia.org/wiki/Cross-Site-Request-Forgery)
    # Default in Spring Boot and required. Without it nginx suppresses the value
    proxy_pass_header X-XSRF-TOKEN;

    # Set origin to the real instance, otherwise a of Spring security check will fail
    # Same value as defined in proxy_pass
    proxy_set_header Origin "http://testsysten:8080";  

感谢您的发现!如果有人对不同的主机名+websocket进行proxy_pass,这也通常适用。没有这些,proxy_pass可以工作,但是websocket始终使用server_name主机名,这会破坏websocket。 - Shinebayar G

5

尽管我使用了非常经典的HTTPS配置,但被接受的解决方案对我无效:

server {
    listen 443 ssl;
    location /ws {
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $http_host;
        proxy_pass http://127.0.0.1:8888;
    }
...

问题在于Spring会检查来源,而特别是下面这段代码给我带来了麻烦:
// in org.springframework.web.util.UriComponentsBuilder.adaptFromForwardedHeaders(HttpHeaders):
        if ((this.scheme.equals("http") && "80".equals(this.port)) ||
                (this.scheme.equals("https") && "443".equals(this.port))) {
            this.port = null;
        }

在那段代码中,协议是“http”,端口是8888,并且没有被丢弃,因为它不是标准端口。
然而,浏览器访问https://myserver/时,443端口被省略,因为它是默认的HTTPS端口。
因此,端口不匹配(空 != 8888),并且源检查失败。
要么您可以在Spring WebSockets中禁用源检查:
registry.addHandler( resgisterHandler(), "/ws" ).setAllowedOrigins( "*" );

或者(可能更安全),您可以将方案和端口添加到NGINX代理配置中:
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Port $server_port;

如果您感兴趣,这些标题是被读取的。

org.springframework.web.util.UriComponentsBuilder.adaptFromForwardedHeaders(HttpHeaders)

适用于Spring Boot 2.2.2+

从Spring Boot版本2.2.2开始,您应该添加以下设置,以便考虑这些X-Forwarded-*头:

server.forward-headers-strategy=native

(例如在application.properties中)

3

我在NGINX代理中解决了没有CSRF头的问题。

我的技术栈:spring-boot,spring-security(带有redis会话存储),spring-boot-websocket 使用默认的STOMP实现, NGINX用于提供前端并代理到前端消费的其他服务。

第一次我使用了在NGINX博客中显示的默认配置和这里(复制并粘贴以供历史记录):

http {
    map $http_upgrade $connection_upgrade {
        default upgrade;
        '' close;
    }

    upstream websocket {
        server 192.168.100.10:8010;
    }

    server {
        listen 8020;
        location / {
            proxy_pass http://websocket;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
        }
    }
}

但是无效,仍然显示403 Forbidden错误。 我通过以下配置解决了这个问题(修复WebSocket的真正重要部分为# WebSocket Proxy):
worker_processes  1;

events {
    worker_connections  1024;
}

http {

    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       30010;
        server_name  localhost;
        client_max_body_size 10M;

        location / {
            root   /usr/share/nginx/html;
            index  index.html index.htm;
        }

        # Backend API Proxy
        location /api {
            proxy_pass http://192.168.0.100:30080;
            proxy_set_header Host $http_host;
            proxy_set_header Access-Control-Allow-Origin 192.168.0.100;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-NginX-Proxy true;
            rewrite ^/api/?(.*) /$1 break;
            proxy_redirect off;
        }

        # CDN Proxy
        location ~ ^/cdn/(.*) {
            proxy_pass http://192.168.0.110:9000;
            rewrite ^/cdn/(.*) /$1 break;
        }

        # This is the configuration that fix the problem with WebSocket
        # WebSocket Proxy
        location /ws {
            proxy_pass http://192.168.0.120:30090;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $http_host;
            proxy_set_header Access-Control-Allow-Origin 192.168.0.120;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-NginX-Proxy true;
        }

    }

}

3

我曾经面临过类似的问题。我无法使用基本的Spring Security认证与NGINX一起使用。除了设置 proxy_pass_header X-XSRF-TOKEN;,我还必须设置underscores_in_headers on;,因为默认情况下,NGINX不允许带有下划线的标头,并且CSRF令牌命名为_csrf

所以我的最终配置文件如下:

server {
    underscores_in_headers on;
    listen 80 default_server;
    listen [::]:80 default_server ipv6only=on;

    root /usr/share/nginx/html;
    index index.html index.htm;

    # Make site accessible from http://localhost/
    server_name localhost;

    location / {
            # First attempt to serve request as file, then
            # as directory, then fall back to displaying a 404.
            try_files $uri $uri/ =404;
            # Uncomment to enable naxsi on this location
            # include /etc/nginx/naxsi.rules
    }
    location /example/ {
            proxy_pass_header X-XSRF-TOKEN;
            proxy_pass      http://localhost:8080/;
    }
}

0
在我的情况下(Spring Boot 应用程序),除了按照接受的答案中指定的设置 Origin 标头之外,我还必须将 Host 标头设置为匹配 Origin 标头的 IP:端口,或者干脆将其删除。
这是我的工作虚拟主机配置:
server {
        listen 443 ssl;
        listen [::]:443 ssl;

        ssl_certificate /etc/ssl/certs/<your-cert-file>.pem;
        ssl_certificate_key /etc/ssl/private/<your-key-file>.key;

        server_name <your-server-fqdn>;

        access_log /var/log/nginx/<your-server-fqdn>.access.log;
        error_log /var/log/nginx/<your-server-fqdn>.error.log error;

        root /srv/www/<your-server-fqdn>;
        index index.html index.html;

        location / {
                try_files $uri $uri/ /index.html;
        }

        location /api {
                proxy_pass http://127.0.0.1:8080/v1;
        }

        location /async-api {
                proxy_pass http://127.0.0.1:8080/stomp;

                proxy_http_version 1.1;

                # either set Host header as follows or get rid of the directive altogether  
                #proxy_set_header Host "127.0.0.1:8080";

                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "Upgrade";

                # Set origin to the real instance, otherwise a of Spring security check will fail
                # Same value as defined in proxy_pass
                proxy_set_header Origin "http://127.0.0.1:8080";

                proxy_set_header X-Forwarded-Host $host:$server_port;
                proxy_set_header X-Forwarded-Server $host;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

        location /admin-api {
                proxy_pass http://127.0.0.1:8080/api;
        }
}

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