Ответ 1
Как указывалось выше, документация (АТМ) неясна, пока Spring не предоставит четкую документацию, вот пример, который поможет вам не тратить два дня, пытаясь понять, что делает цепочка безопасности.
Роб-Леггетт предпринял действительно хорошую попытку, но он порвал несколько занятий в Спрингсе, и я чувствую себя некомфортно.
Что нужно знать:
- Цепочка безопасности и конфигурация безопасности для http и WebSocket полностью независимы.
- Spring
AuthenticationProvider
вообще не участвует в аутентификации Websocket. - Аутентификация не будет происходить на конечной точке согласования HTTP, потому что ни один из STOMP (websocket) JavaScripts не отправляет заголовки аутентификации вместе с HTTP-запросом.
- После установки запроса CONNECT пользователь (
simpUser
) будет сохранен в сеансе websocket, и для дальнейших сообщений больше не потребуется аутентификация.
Maven Deps
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-messaging</artifactId>
</dependency>
Конфигурация WebSocket
Приведенная ниже конфигурация регистрирует простого посредника сообщений (обратите внимание, что он не имеет ничего общего с аутентификацией или авторизацией).
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(final MessageBrokerRegistry config) {
// These are endpoints the client can subscribes to.
config.enableSimpleBroker("/queue/topic");
// Message received with one of those below destinationPrefixes will be automatically router to controllers @MessageMapping
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(final StompEndpointRegistry registry) {
// Handshake endpoint
registry.addEndpoint("stomp"); // If you want to you can chain setAllowedOrigins("*")
}
}
Конфигурация безопасности Spring
Поскольку протокол Stomp основан на первом HTTP-запросе, нам нужно авторизовать HTTP-вызов для нашей конечной точки рукопожатия Stomp.
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(final HttpSecurity http) throws Exception {
// This is not for websocket authorization, and this should most likely not be altered.
http
.httpBasic().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests().antMatchers("/stomp").permitAll()
.anyRequest().denyAll();
}
}
Затем мы создадим сервис, отвечающий за аутентификацию пользователей.
@Component
public class WebSocketAuthenticatorService {
// This method MUST return a UsernamePasswordAuthenticationToken instance, the spring security chain is testing it with 'instanceof' later on. So don't use a subclass of it or any other class
public UsernamePasswordAuthenticationToken getAuthenticatedOrFail(final String username, final String password) throws AuthenticationException {
if (username == null || username.trim().isEmpty()) {
throw new AuthenticationCredentialsNotFoundException("Username was null or empty.");
}
if (password == null || password.trim().isEmpty()) {
throw new AuthenticationCredentialsNotFoundException("Password was null or empty.");
}
// Add your own logic for retrieving user in fetchUserFromDb()
if (fetchUserFromDb(username, password) == null) {
throw new BadCredentialsException("Bad credentials for user " + username);
}
// null credentials, we do not pass the password along
return new UsernamePasswordAuthenticationToken(
username,
null,
Collections.singleton((GrantedAuthority) () -> "USER") // MUST provide at least one role
);
}
}
Обратите внимание, что: UsernamePasswordAuthenticationToken
ДОЛЖЕН иметь GrantedAuthorities, если вы используете другой конструктор, Spring автоматически установит isAuthenticated = false
.
Почти там, теперь нам нужно создать Interceptor, который будет устанавливать заголовок simpUser
или генерировать simpUser
AuthenticationException
для сообщений CONNECT.
@Component
public class AuthChannelInterceptorAdapter extends ChannelInterceptor {
private static final String USERNAME_HEADER = "login";
private static final String PASSWORD_HEADER = "passcode";
private final WebSocketAuthenticatorService webSocketAuthenticatorService;
@Inject
public AuthChannelInterceptorAdapter(final WebSocketAuthenticatorService webSocketAuthenticatorService) {
this.webSocketAuthenticatorService = webSocketAuthenticatorService;
}
@Override
public Message<?> preSend(final Message<?> message, final MessageChannel channel) throws AuthenticationException {
final StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT == accessor.getCommand()) {
final String username = accessor.getFirstNativeHeader(USERNAME_HEADER);
final String password = accessor.getFirstNativeHeader(PASSWORD_HEADER);
final UsernamePasswordAuthenticationToken user = webSocketAuthenticatorService.getAuthenticatedOrFail(username, password);
accessor.setUser(user);
}
return message;
}
}
Обратите внимание, что: preSend()
ДОЛЖЕН возвращать UsernamePasswordAuthenticationToken
, другой элемент в цепочке безопасности Spring проверяет это. Обратите внимание: если ваш UsernamePasswordAuthenticationToken
был создан без прохождения GrantedAuthority
, аутентификация завершится неудачно, поскольку конструктор без предоставленных полномочий автоматически устанавливает authenticated = false
ЭТО ВАЖНАЯ ДЕТАЛИ, которая не задокументирована в Spring-Security.
Наконец, создайте еще два класса для обработки соответственно авторизации и аутентификации.
@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebSocketAuthenticationSecurityConfig extends WebSocketMessageBrokerConfigurer {
@Inject
private AuthChannelInterceptorAdapter authChannelInterceptorAdapter;
@Override
public void registerStompEndpoints(final StompEndpointRegistry registry) {
// Endpoints are already registered on WebSocketConfig, no need to add more.
}
@Override
public void configureClientInboundChannel(final ChannelRegistration registration) {
registration.setInterceptors(authChannelInterceptorAdapter);
}
}
Обратите внимание: @Order
является CRUCIAL, не забывайте его, он позволяет нашему перехватчику быть зарегистрированным первым в цепочке безопасности.
@Configuration
public class WebSocketAuthorizationSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(final MessageSecurityMetadataSourceRegistry messages) {
// You can customize your authorization mapping here.
messages.anyMessage().authenticated();
}
// TODO: For test purpose (and simplicity) i disabled CSRF, but you should re-enable this and provide a CRSF endpoint.
@Override
protected boolean sameOriginDisabled() {
return true;
}
}
Удачи !