The problem I'm having is with the execution order of ChannelInterceptors.I tried setting up websocket security. Defined a ChannelInterseptor to implement jwt token based authentication and added its ChannelRegistration. Also added @EnableWebsocketSecurity annotation to enable security for websocket. At the same time, interceptor chains are added that define security rules.Here is my class implementing WebSocketMessageBrokerConfigurer. My ChannelInterceptor was being added later than the security interceptor chain, resulting in an authorization error.
@Configuration
@EnableWebSocketMessageBroker
@EnableWebSocketSecurity
@Slf4j
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final JwtDecoder jwtDecoder;
public WebSocketConfig(JwtDecoder jwtDecoder) {
this.jwtDecoder = jwtDecoder;
}
@Bean
public ChannelInterceptor csrfChannelInterceptor(){
return new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
return ChannelInterceptor.super.preSend(message, channel);
}
};
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
List<String> authorization = accessor.getNativeHeader("Authorization");
String accessToken = authorization.get(0).split(" ")[1];
Jwt jwt = jwtDecoder.decode(accessToken);
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
Authentication authentication = converter.convert(jwt);
accessor.setUser(authentication);
}
return message;
}
});
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new AuthenticationPrincipalArgumentResolver());
}
@Bean
public AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages
.anyMessage().authenticated();
return messages.build();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat")
.setAllowedOrigins("http://localhost:8080", "http://localhost:4200");
registry.addEndpoint("/chat")
.setAllowedOrigins("http://localhost:8080", "http://localhost:4200")
.withSockJS();
}
}
The problem is that the order in which ChannelInterseptors are registered depends only on the insertion order, since everything is implemented through a regular ArrayList. There is one way - to determine the order of configurations that implement the WebsocketMessageBrokerConfigurer interface. They are injected via a setter in the DelegatingWebSocketMessageBrokerConfiguration. And the order of implementation of collections can be changed using the @Order annotation, placing it above our configuration class. This solved the problem in my case, but I wondered if there would be a need to explicitly define the order of interceptors, such as wedging into a chain of security interceptors. An analogy can be drawn with SecurityFilterChain, when configuring it you can use the addFilterAt, addFilterBefore, addFilterAfter methods.
I couldn't find a way, but maybe I'm missing something.
After some time I returned to this issue and made one decision. Perhaps not the best, but quite functional. First of all, we need to mark our configuration with the @Order annotation without a parameter, by default it is Integer.MAX_VALUE, which means that our configuration will be processed last, which means that all other configurations will already be processed and all interceptors will be added, which allows us to implement order manually. Here's how I implemented it:
@Configuration
@EnableWebSocketMessageBroker
@EnableWebSocketSecurity
@Order
@Slf4j
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final JwtDecoder jwtDecoder;
public WebSocketConfig(JwtDecoder jwtDecoder) {
this.jwtDecoder = jwtDecoder;
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
ChannelInterceptor securityInterceptor = new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
List<String> authorization = accessor.getNativeHeader("Authorization");
String accessToken = authorization.get(0).split(" ")[1];
Jwt jwt = jwtDecoder.decode(accessToken);
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
Authentication authentication = converter.convert(jwt);
accessor.setUser(authentication);
}
return message;
}
@Override
public String toString(){
return "SecurityInterceptor";
}
};
ChannelInterceptor csrf = new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
return ChannelInterceptor.super.preSend(message, channel);
}
@Override
public String toString(){
return "csrfChannelInterceptor";
}
};
addInterceptorBefore(registration, securityInterceptor, AuthorizationChannelInterceptor.class);
addFilterAt(registration, csrf, XorCsrfChannelInterceptor.class);
}
private void addInterceptorBefore(ChannelRegistration registration, ChannelInterceptor interceptor, Class<? extends ChannelInterceptor> before) {
List<ChannelInterceptor> interceptors = getChannelInterceptors(registration);
int index = findInterceptor(interceptors, before);
interceptors.add(interceptors.get(interceptors.size()-1));
for(int i = interceptors.size()-2; i > index; i--){
interceptors.set(i, interceptors.get(i-1));
}
interceptors.set(index, interceptor);
}
private void addFilterAfter(ChannelRegistration registration, ChannelInterceptor interceptor, Class<? extends ChannelInterceptor> after) {
List<ChannelInterceptor> interceptors = getChannelInterceptors(registration);
int index = findInterceptor(interceptors, after);
interceptors.add(interceptors.get(interceptors.size()-1));
for(int i = interceptors.size()-2; i > index + 1; i--){
interceptors.set(i, interceptors.get(i-1));
}
interceptors.set(index+1, interceptor);
}
private void addFilterAt(ChannelRegistration registration, ChannelInterceptor interceptor, Class<? extends ChannelInterceptor> at) {
List<ChannelInterceptor> interceptors = getChannelInterceptors(registration);
int index = findInterceptor(interceptors, at);
interceptors.set(index, interceptor);
}
private List<ChannelInterceptor> getChannelInterceptors(ChannelRegistration registration) {
Field field = ReflectionUtils.findField(registration.getClass(), "interceptors");
Objects.requireNonNull(field);
field.setAccessible(true);
List<ChannelInterceptor> interceptors =
(List<ChannelInterceptor>) ReflectionUtils.getField(field, registration);
Objects.requireNonNull(interceptors);
return interceptors;
}
private int findInterceptor(List<ChannelInterceptor> interceptors, Class<? extends ChannelInterceptor> interceptor) {
int index = -1;
for(int i = 0; i < interceptors.size();i++){
if(interceptors.get(i).getClass().equals(interceptor)){
index = i;
break;
}
}
return index;
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new AuthenticationPrincipalArgumentResolver());
}
@Bean
public AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages
.anyMessage().authenticated();
return messages.build();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat")
.setAllowedOrigins("http://localhost:8080", "http://localhost:4200");
registry.addEndpoint("/chat")
.setAllowedOrigins("http://localhost:8080", "http://localhost:4200")
.withSockJS();
}
}
This implementation may be far from ideal, but it is simpler than implementing some BeanFactoryPostProcessor and proxy to override behavior. I will be glad if you share your ideas.