РЕДАКТИРОВАТЬ
Я нашел очень похожий вопрос Аутентификация и авторизация Websocket весной , который не дал рабочего решения.
ORIGINAL
Я экспериментирую с Spring WebSockets и Security.
Я пытаюсь реализовать пользовательскую процедуру аутентификации в ChannelInterceptor в поисках сообщения CONNECT.Это сообщение имеет несколько пользовательских заголовков для пользователя / пароля.После проверки я звоню SecurityContextHolder.getContext().setAuthentication(..)
.
Примечание: я использую аутентификацию на основе заголовков на основе токенов в точках доступа.К сожалению, невозможно отправить пользовательский заголовок из браузера на рукопожатие websocket .Мне пришлось настроить конечную точку Websocket так, чтобы .antMatchers(WebSocketConfig.ENDPOINT).permitAll()
был доступен каждому.Spring Security
Идея состоит в том, чтобы аутентифицировать пользователя в сообщении о соединении, установить объект авторизации и позволить Spring Security сделать свое волшебство.
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
public static final String ENDPOINT = "/gs-guide-websocket";
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint(ENDPOINT).setAllowedOrigins("*");
}
@Override
public void configureClientInboundChannel(final ChannelRegistration registration) {
registration.interceptors(new XAuthChannelInterceptor(authenticationManager));
}
public static class XAuthChannelInterceptor implements ChannelInterceptor {
private AuthenticationManager authenticationManager;
public XAuthChannelInterceptor(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
var accessor = SimpMessageHeaderAccessor.wrap(message);
// Check Connect
SimpMessageType messageType = accessor.getMessageType();
if (messageType == SimpMessageType.CONNECT) {
try {
// Login Credentials
var authUser = accessor.getFirstNativeHeader(CustomAuthenticationFilter.X_HEADER_USER);
var authPassword = accessor.getFirstNativeHeader(CustomAuthenticationFilter.X_HEADER_PASS);
// Try Login
var requestAuthentication = new UsernamePasswordAuthenticationToken(authUser, authPassword);
var responseAuthentication = authenticationManager.authenticate(requestAuthentication);
// Set User
accessor.setUser(responseAuthentication);
SecurityContextHolder.getContext().setAuthentication(responseAuthentication);
} catch (Exception e) {
throw new RuntimeException("login failed");
}
}
return message;
}
}
}
Кажется, это работает просто отлично.Но: когда сообщение поступает в контроллер, объект аутентификации исчезает.Ну, это потому, что SecurityContextHolder
работает ThreadLocal.
@Controller
public class GreetingController {
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public CreateCustomerCommandResult greeting(CreateCustomerCommand cmd) throws Exception {
var x = SecurityContextHolder.getContext().getAuthentication();
Thread.sleep(1000); // simulated delay
var ret = new CreateCustomerCommandResult();
ret.setId(UUID.randomUUID());
ret.setText("Hello, " + cmd.getName());
return ret;
}
}
В конечном итоге я хотел бы заставить @PreAuthorize
работать так:
@MessageMapping("/hello")
@SendTo("/topic/greetings")
@PreAuthorize("hasAuthority('ADMIN')")
public CreateCustomerCommandResult greeting(CreateCustomerCommand cmd) throws Exception {
//...
}
Мой вопрос: как мне установитьобъект аутентификации внутри ChannelInterceptor для SecurityContext контроллера?