Spring безопасность и websocket с токеном jwt

У меня есть проект Spring Boot (2.0.0.RELEASE). Я использую аутентификацию и авторизацию на основе токена JWT для защиты REST. Я также хотел использовать Websocket с SockJS и использовать токен для аутентификации сокета. Пытаюсь реализовать в отношении эта почта, но у меня ничего не получилось.

Прежде всего, моя конфигурация безопасности ниже;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final TokenAuthenticationService tokenAuthenticationService;
    private final ObjectMapper mapper;

    @Autowired
    protected SecurityConfig(final TokenAuthenticationService tokenAuthenticationService, ObjectMapper mapper) {
        super();
        this.tokenAuthenticationService = tokenAuthenticationService;
        this.mapper = mapper;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.headers()
                .frameOptions().disable()
                .and()
                .authorizeRequests()
                .antMatchers("/api/v3/auth").permitAll()
                .antMatchers("/api/v3/signup").permitAll()
                .antMatchers("/api/v3/websocket/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .exceptionHandling().authenticationEntryPoint(new RestAuthenticationEntryPoint(mapper))
                .and()
                .addFilterBefore(new AuthenticationTokenFilter(tokenAuthenticationService), UsernamePasswordAuthenticationFilter.class)
                .cors()
                .and()
                .csrf().disable();
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("OPTIONS");
        config.addAllowedMethod("GET");
        config.addAllowedMethod("POST");
        config.addAllowedMethod("PUT");
        config.addAllowedMethod("DELETE");
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

В приложении, чтобы получить токен, нужно сделать POST-запрос к /api/v3/auth. /api/v3/websocket - это конечная точка веб-сокета. Мой класс конфигурации Websocket приведен ниже.

@Configuration
@EnableWebSocketMessageBroker
@Order(Ordered.HIGHEST_PRECEDENCE + 50)
public class SocketBrokerConfig implements WebSocketMessageBrokerConfigurer {
    private static final Logger log = LoggerFactory.getLogger(SocketBrokerConfig.class);

    private final TokenAuthenticationService authenticationService;

    @Autowired
    public SocketBrokerConfig(TokenAuthenticationService authenticationService) {
        this.authenticationService = authenticationService;
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic", "/queue");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        log.info("registering websockets");
        registry
                .addEndpoint("/api/v3/websocket")
                .setAllowedOrigins("*")
                .withSockJS()
                .setClientLibraryUrl("https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.4/sockjs.min.js")
                .setWebSocketEnabled(false)
                .setSessionCookieNeeded(false);
    }

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new ChannelInterceptorAdapter() {

            @Override
            public Message<?> preSend(Message<?> message, MessageChannel channel) {

                StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);

                log.info("in override " + accessor.getCommand());

                if (StompCommand.CONNECT.equals(accessor.getCommand())) {

//                    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
//                    String name = auth.getName(); //get logged in username
//                    System.out.println("Authenticated User : " + name);

                    String authToken = accessor.getFirstNativeHeader("x-auth-token");

                    log.info("Header auth token: " + authToken);

                    Principal principal = authenticationService.getUserFromToken(authToken);

                    if (Objects.isNull(principal))
                        return null;

                    accessor.setUser(principal);
                } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) {
                    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

                    if (Objects.nonNull(authentication))
                        log.info("Disconnected Auth : " + authentication.getName());
                    else
                        log.info("Disconnected Sess : " + accessor.getSessionId());
                }
                return message;
            }

            @Override
            public void postSend(Message<?> message, MessageChannel channel, boolean sent) {
                StompHeaderAccessor sha = StompHeaderAccessor.wrap(message);

                // ignore non-STOMP messages like heartbeat messages
                if (sha.getCommand() == null) {
                    log.warn("postSend null command");
                    return;
                }

                String sessionId = sha.getSessionId();

                switch (sha.getCommand()) {
                    case CONNECT:
                        log.info("STOMP Connect [sessionId: " + sessionId + "]");
                        break;
                    case CONNECTED:
                        log.info("STOMP Connected [sessionId: " + sessionId + "]");
                        break;
                    case DISCONNECT:
                        log.info("STOMP Disconnect [sessionId: " + sessionId + "]");
                        break;
                    default:
                        break;

                }
            }
        });

    }
}

Также мой класс конфигурации безопасности Websocket:

@Configuration
public class SocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    @Override
    protected boolean sameOriginDisabled() {
        // We need to access this directly from apps, so can't do cross-site checks
        return true;
    }
}

Наконец, мой тестовый JS-файл находится ниже.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8"/>
    <title>Test</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"/>
</head>
<body>
<nav class="navbar navbar-default">
    <div class="container-fluid">
        <div class="navbar-header">
            <a class="navbar-brand" href="/test/ws">Test Client</a>
        </div>
    </div>
</nav>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script type="text/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.4/sockjs.min.js"></script>

<script type="application/javascript">
    var endpoint = "http://192.168.0.58:8080/api/v3/auth";
    var login = {username: "admin", password: "password"};

    $.ajax({
        type: "POST",
        url: endpoint,
        data: JSON.stringify(login),
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        success: function (data) {
            var socket = new SockJS("http://192.168.0.58:8080/api/v3/websocket/");
            var stompClient = Stomp.over(socket);

            var headers = {
                'client-id': 'my-client-id',
                'x-auth-token': data.data.token
            };
            stompClient.connect(headers, function (frame) {
                console.log("Connected ?!");
                console.log(frame);
                stompClient.subscribe(
                    "/user/queue/admin",
                    function (message) {
                        console.log("Message arrived");
                        console.log(message);
                    }
                );
            });
        }
    });
</script>
</body>
</html>

После того, как код JS получает токен от /api/v3/auth, он пытается подключить /api/v3/websocket. Однако переопределенный метод preSend ChannelInterceptorAdapter никогда не вызывается на стороне сервера. Код никогда не достигает этого метода. Фактически, команда CONNECT никогда не достигает сюда, когда я закрываю браузер, команда DISCONNECT достигает как anonymousUser. Когда JS пытается подключиться, метод аутентификации моего JsonWebTokenAuthenticationService вызывается повторно.

Консольный вывод браузера:

XHROPTIONS
http://192.168.0.58:8080/api/v3/auth
[HTTP/1.1 200  4ms]
XHRPOST
http://192.168.0.58:8080/api/v3/auth
[HTTP/1.1 200  7ms]
Object { data: {…} }
index.html:123:12
Opening Web Socket...
stomp.min.js:8:1737
XHRGET
http://192.168.0.58:8080/api/v3/websocket/info?t=1523448356260
[HTTP/1.1 200  4ms]
XHRGET
http://192.168.0.58:8080/api/v3/websocket/848/s1t1xcij/eventsource
XHRGET
http://192.168.0.58:8080/api/v3/websocket/848/suu5lc11/eventsource
XHRPOST
http://192.168.0.58:8080/api/v3/websocket/848/zzjmmaf4/xhr?t=1523448359012
[HTTP/1.1 200  10012ms]
Whoops! Lost connection to http://192.168.0.58:8080/api/v3/websocket

Я перепробовал много разных фрагментов кода в Интернете, но безуспешно.

Мой вопрос в том, почему моя клиентская команда CONNECT попадает в переопределенный перехватчик, а команда DISCONNECT падает?

** РЕДАКТИРОВАТЬ1 **

Я добавляю свою реализацию TokenAuthenticationService ниже. Я заметил, что, если я помещаю точку отладки в метод аутентификации и подожду пару секунд и возобновлю, соединение клиента websocket и команда CONNECT попадает в метод preSend.

Что происходит, когда я прекращаю аутентификацию на пару секунд и почему?

@Service
public class JsonWebTokenAuthenticationService implements TokenAuthenticationService {

    private static final Logger log = LoggerFactory.getLogger(JsonWebTokenAuthenticationService.class);

    @Value("security.token.secret.key")
    private String secretKey;

    private final UserRepository userRepository;

    @Autowired
    public JsonWebTokenAuthenticationService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public Authentication authenticate(HttpServletRequest request) {
        final String token = request.getHeader("x-auth-token");
        final Jws<Claims> tokenData = parseToken(token);

        if (Objects.nonNull(tokenData)) {
            User user = getUserFromToken(tokenData);
            if (Objects.nonNull(user) && user.isEnabled()) {
                return new UserAuthentication(user);
            }
        }
        return null;
    }

    public Authentication getUserFromToken(String token) {
        if (Objects.isNull(token))
            return null;

        final Jws<Claims> tokenData = parseToken(token);

        if (Objects.nonNull(tokenData)) {
            User user = getUserFromToken(tokenData);
            if (Objects.nonNull(user) && user.isEnabled()) {
                return new UserAuthentication(user);
            }
        }
        return null;
    }

    private Jws<Claims> parseToken(final String token) {
        if (Objects.nonNull(token)) {
            try {
                return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
            } catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException
                    | SignatureException | IllegalArgumentException e) {
                log.warn("Token parse failed", e);
                return null;
            }
        }
        return null;
    }

    private User getUserFromToken(final Jws<Claims> tokenData) {
        try {
            return userRepository.findByUsername(tokenData.getBody().get("username").toString());
        } catch (UsernameNotFoundException e) {
            log.warn("No user", e);
        }
        return null;
    }
}

** РЕДАКТИРОВАТЬ2 **

Я добавил Thread.sleep(2000) (чего мне не следовало делать) в свой метод AuthenticationTokenFilter#doFilter, и теперь JS-клиент подключается, подписывается и т. Д. И команда STOMP CONNECT попадает в метод ChannelInterceptorAdapter#preSend. Мне это показалось немного странным.

3
0
6 316
1

Ответы 1

После РЕДАКТИРОВАТЬ2 столкнулся с Эта проблема. И оказалось, что все проблемы вызваны роутером. Я все еще не понимаю, что произошло, но теперь все работает как положено.

Другие вопросы по теме