Я работаю над приложением Spring-MVC, в котором у нас есть Spring-безопасность для аутентификации и авторизации. Мы работаем над переходом на веб-сокеты Spring, но у нас возникла проблема с получением аутентифицированного пользователя внутри соединения с веб-сокетом. Контекст безопасности просто не существует в соединении через веб-сокет, но отлично работает с обычным HTTP. Что мы делаем не так?
Конфигурация веб-сокета:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/app").withSockJS();
}
}
В приведенном ниже контроллере мы пытаемся получить текущего аутентифицированного пользователя, и он всегда нулевой.
@Controller
public class OnlineStatusController extends MasterController{
@MessageMapping("/onlinestatus")
public void onlineStatus(String status) {
Person user = this.personService.getCurrentlyAuthenticatedUser();
if (user!=null){
this.chatService.setOnlineStatus(status, user.getId());
}
}
}
безопасность-приложениеContext.xml :
<security:http pattern = "/resources/**" security = "none"/>
<security:http pattern = "/org/**" security = "none"/>
<security:http pattern = "/jquery/**" security = "none"/>
<security:http create-session = "ifRequired" use-expressions = "true" auto-config = "false" disable-url-rewriting = "true">
<security:form-login login-page = "/login" username-parameter = "j_username" password-parameter = "j_password"
login-processing-url = "/j_spring_security_check" default-target-url = "/canvaslisting"
always-use-default-target = "false" authentication-failure-url = "/login?error=auth"/>
<security:remember-me key = "_spring_security_remember_me" user-service-ref = "userDetailsService"
token-validity-seconds = "1209600" data-source-ref = "dataSource"/>
<security:logout delete-cookies = "JSESSIONID" invalidate-session = "true" logout-url = "/j_spring_security_logout"/>
<security:csrf disabled = "true"/>
<security:intercept-url pattern = "/cometd/**" access = "permitAll" />
<security:intercept-url pattern = "/app/**" access = "hasAnyRole('ROLE_ADMIN','ROLE_USER')" />
<!-- <security:intercept-url pattern = "/**" requires-channel = "https"/>-->
<security:port-mappings>
<security:port-mapping http = "80" https = "443"/>
</security:port-mappings>
<security:logout logout-url = "/logout" logout-success-url = "/" success-handler-ref = "myLogoutHandler"/>
<security:session-management session-fixation-protection = "newSession">
<security:concurrency-control session-registry-ref = "sessionReg" max-sessions = "5" expired-url = "/login"/>
</security:session-management>
</security:http>




Я помню, как наткнулся на ту же самую проблему в проекте, над которым работал. Поскольку я не мог найти решение, используя документацию Spring, а другие ответы на переполнение стека у меня не работали, в итоге я создал обходной путь.
Хитрость, по сути, заключается в том, чтобы заставить приложение аутентифицировать пользователя в запросе на подключение WebSocket. Для этого вам нужен класс, который перехватывает такие события, а затем, получив контроль над этим, вы можете вызвать свою логику аутентификации.
Создайте класс, реализующий Spring ChannelInterceptorAdapter. Внутри этого класса вы можете внедрить любые bean-компоненты, необходимые для фактической аутентификации. В моем примере используется базовая аутентификация:
@Component
public class WebSocketAuthInterceptorAdapter extends ChannelInterceptorAdapter {
@Autowired
private DaoAuthenticationProvider userAuthenticationProvider;
@Override
public Message<?> preSend(final Message<?> message, final MessageChannel channel) throws AuthenticationException {
final StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
StompCommand cmd = accessor.getCommand();
if (StompCommand.CONNECT == cmd || StompCommand.SEND == cmd) {
Authentication authenticatedUser = null;
String authorization = accessor.getFirstNativeHeader("Authorization:");
String credentialsToDecode = authorization.split("\\s")[1];
String credentialsDecoded = StringUtils.newStringUtf8(Base64.decodeBase64(credentialsToDecode));
String[] credentialsDecodedSplit = credentialsDecoded.split(":");
final String username = credentialsDecodedSplit[0];
final String password = credentialsDecodedSplit[1];
authenticatedUser = userAuthenticationProvider.authenticate(new UsernamePasswordAuthenticationToken(username, password));
if (authenticatedUser == null) {
throw new AccessDeniedException();
}
SecurityContextHolder.getContext().setAuthentication(authenticatedUser);
accessor.setUser(authenticatedUser);
}
return message;
}
}
Затем в вашем классе WebSocketConfig вам нужно зарегистрировать свой перехватчик. Добавьте указанный выше класс как bean-компонент и зарегистрируйте его. После этих изменений ваш класс будет выглядеть так:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Autowired
private WebSocketAuthInterceptorAdapter authInterceptorAdapter;
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/app").withSockJS();
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(authInterceptorAdapter);
super.configureClientInboundChannel(registration);
}
}
Очевидно, что детали логики аутентификации зависят от вас. Вы можете вызвать службу JWT или что-то еще, что вы используете.
Класс должен быть WebSocketAuthInterceptorAdapter. Я отредактировал свой ответ.
Я реализовал аналогичный код, и я могу получить пользователя внутри Interceptor, но в сервисе, когда я использую SecurityContextHolder.getContext(), я получаю сообщение об ошибке: An Authentication object was not found in the SecurityContext
Извините, что отвечаю так поздно. Прошло некоторое время с тех пор, как я сделал что-либо, связанное с Spring Websockets. Я предполагаю, что эта ошибка должна означать, что что-то неправильно настроено в Spring Security, и авторизация, которую вы устанавливаете, не интерпретируется фреймворком.
Если вы используете SockJS + Stomp и правильно настроили свою безопасность, вы сможете подключиться через обычный аутентификатор имени пользователя/пароля, такой как @AlgorithmFromHell, и сделать
accessor.setUser(authentication.getPrincipal()) // stomp header accessor
Вы также можете подключиться через http://{END_POINT}/access_token = {ACCESS_TOKEN}. Безопасность Spring должна иметь возможность выбрать его и выполнить loadAuthentication (access_token) через ResourceServerTokenServices. Когда это будет сделано, вы можете получить своего принципала, добавив его в свою реализацию AbstractSessionWebSocketMessageBrokerConfigurer или WebSocketMessageBrokerConfigurer. При этом по какой-то причине загруженный Pricipal вместо этого сохраняется в заголовке «simpUser».
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(final Message<?> message, final MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
if (message.getHeaders().get("simpUser") != null && message.getHeaders().get("simpUser") instanceof OAuth2Authentication) { // or Authentication depending on your impl of security
OAuth2Authentication authentication = (OAuth2Authentication) message.getHeaders().get("simpUser");
accessor.setUser(authentication != null ? (UserDetails) authentication.getPrincipal() : null);
}
}
return message;
}
});
}
Можете ли вы сказать мне, из какого класса WebSocketChannelInterceptorAdapter? Спасибо.