I have the following problem.
I'm trying to develop a Spring Boot application that serves as RestController and also uses Websockets (w/o STOMP). The RestController has an universal GetMapping method ("/{name}/**) that fetches, depending on name variable, content out of a template database.
My websocket handler should react as broadcast message broker for calls at the endpoint ("/broadcast").
When I test it with Postman, the broadcast websocket call just calls my Restcontroller, what is not intended. It should connect to the Websocket handler.
My code looks like this:
WebSocketConfig:
#Configuration
#EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketBroadcastHandler(), "/broadcast").setAllowedOrigins("*");
}
#Bean
public WebSocketHandler webSocketBroadcastHandler() {
CfWebSocketBroadcastHandler swsh = new CfWebSocketBroadcastHandler();
return swsh;
}
}
Broadcast handler:
public class CfWebSocketBroadcastHandler extends TextWebSocketHandler {
private static Set<WebSocketSession> sessions = null;
public CfWebSocketBroadcastHandler() {
sessions = new CopyOnWriteArraySet<>();
}
#Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
sessions.add(session);
}
#Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
sessions.remove(session);
}
#Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
if (!sessions.contains(session)) {
sessions.add(session);
}
String request = message.getPayload();
WebSocketBroadcastMessage bcm = new Gson().fromJson(request, WebSocketBroadcastMessage.class);
broadcast(bcm);
}
public void broadcast(WebSocketBroadcastMessage bcm) {
for (WebSocketSession session : sessions) {
if (session.isOpen()) {
try {
session.sendMessage(new TextMessage(new Gson().toJson(bcm)));
} catch (IOException ex) {
Logger.getLogger(CfWebSocketBroadcastHandler.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
}
RestController:
#RestController
#Component
#Configuration
public class MyRestcontroller {
#GetMapping(path = "/{name}/**")
public void universalGet(#PathVariable("name") String name, #Context HttpServletRequest request, #Context HttpServletResponse response) {
// get template from databse with name variable
}
}
How can I make sure, that the websocket handler gets called instead of the restcontroller?
Further infos:
I'm using spring-boot 2.6.7 and embedded Tomcat 9.0.0.M6.
The maven dependencies are included.
Thanks for any help.
Related
I've a springboot/openapi application. No dependency on spring security.
When launching a POST request via swagger, the returned status is 403.
The request doesn't arrive in the controller class.
A Get request however does work and returns a status 200.
The following is configured
#Configuration
public class Config {
#Bean
ForwardedHeaderFilter forwardedHeaderFilter() {
return new ForwardedHeaderFilter();
}
}
}
application.yaml
server:
port: 50086
forward-headers-strategy: framework
use-forward-headers: true
What could be the cause of the status 403 ?
Controller
#CrossOrigin
#RestController
#RequestMapping("/ta")
public class TaController {
#Operation(summary = "Calculate")
#RequestMapping(value = "/calculateWithPrices", method = RequestMethod.POST)
public ResponseEntity<CaculationResponseDto> calculateWithPrices(#RequestBody CaculationWithPricesRequestDto caculationWithPricesRequestDto) {
// code ...
}
Try to add a SecurityConfig which inherits from WebSecurityConfigurerAdapter. Example is here.
With the method configure you can set the access to specific url-endpoints and allow the call on them.
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomAuthenticationProvider authProvider;
#Autowired
public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider).eraseCredentials(false);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and().authorizeRequests().antMatchers("**apiEndpoint**").authenticated()
.and().csrf().disable().headers().frameOptions().disable().and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Deactivate authorization for whole application
// http.authorizeHttpRequests().antMatchers("/**").permitAll().and().csrf().disable();
}
}
Class CustomAuthenticationProvider:
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Autowired
private ICustomerRepository customerRepository;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String id = authentication.getName().toString();
String pin = authentication.getCredentials().toString();
try {
// Check if the customer passed in Username exists
CustomerDTO customer = customerRepository.findById(Long.parseLong(id)).orElseThrow();
} catch (Exception e) {
// TODO Auto-generated catch block
throw new BadCredentialsException(id);
}
Collection<? extends GrantedAuthority> authorities = Collections
.singleton(new SimpleGrantedAuthority("ROLE_CUSTOMER"));
return new UsernamePasswordAuthenticationToken(id, pin, authorities);
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
I have implemented WebSocket mechanism in spring boot application.
#Configuration
#EnableWebSocket
public class WebSocketConfiguration implements WebSocketConfigurer {
private String webSockerClientUri;
private WebSocketHandlerRegistry webSocketHandlerRegistry;
private WebSocketHandler webSocketHandler;
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
this.webSocketHandlerRegistry = webSocketHandlerRegistry;
this.webSocketHandler = registerWebSocketHandler();
this.webSocketHandlerRegistry.addHandler(this.webSocketHandler, "/test");
}
public WebSocketHandler registerWebSocketHandler(){
return new WebSocketHandler();
}
public WebSocketHandlerRegistry getWebSocketHandlerRegistry() {
return webSocketHandlerRegistry;
}
public WebSocketHandler getWebSocketHandler() {
return webSocketHandler;
}
}
During startup appp WebSocketHandlerMapping detect registration of Websocket connection "/test". I would like to do the same action in REST endpoint like this.
#PostMapping("/{uri}")
public ResponseEntity register(#PathVariable("uri") String uri){
if (uri != null){
this.webSocketConfiguration.getWebSocketHandlerRegistry().addHandler(webSocketConfiguration.getWebSocketHandler(), "/" + uri);
return new ResponseEntity(HttpStatus.OK);
}
else {
return new ResponseEntity(HttpStatus.NO_CONTENT);
}
}
Uri is added into WebSocketHandlerRegistry however mechanism WebSocketHandlerMapping doesn't detect that fact, because it is not startup state of app. How can I solve this problem?
I am using microservices architecture so I have a separate SSO service which handles all the authentication and authorization requests.
I am using spring websockets in other service and I need to secure it using tokens handled by SSO, so I added this configuration for securing websockets.
#Configuration
#EnableResourceServer
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.nullDestMatcher().authenticated()
.simpTypeMatchers(CONNECT).authenticated()
.simpDestMatchers("/ws/**").hasRole("USER")
.simpSubscribeDestMatchers("/ws/**").hasRole("USER")
.anyMessage().denyAll();
}
#Override
protected boolean sameOriginDisabled() {
return true;
}
}
And for websocket config
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/ws/topic");
config.setApplicationDestinationPrefixes("/ws/view");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/socket/").withSockJS();
}
}
And for remote SSO server
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll()
.antMatchers("/api/**").access("#oauth2.hasScope('service-name')");
http.csrf().disable();
http.httpBasic().disable();
}
#Bean
#Primary
#RefreshScope
public CachedRemoteTokenService tokenServices() {
final CachedRemoteTokenService remoteTokenServices = new CachedRemoteTokenService();
remoteTokenServices.setCheckTokenEndpointUrl(getCheckTokenEndPointUrl());
remoteTokenServices.setClientId(getClientId());
remoteTokenServices.setClientSecret(getClientSecret());
return remoteTokenServices;
}
I added the token in the client but it throws AccessDeniedException
var headers = {
Authorization: 'Bearer ' + myToken
}
stompClient.send("/ws/view/update/", headers, JSON.stringify(view));
I checked the SSO server logs and I found it didn't call it at all! Is there something missing?
Any help will be appreciated
i have used this tutorial and it works for me. you can flow the steps : Intro to Security and WebSockets
I have got Spring Boot Application and I want to test it. I do not use Spring Controllers, but I use Servlet with service method. Also I have got my configuration class that provides ServletRegistrationBean.
But every time when I try to perform mock request I get 404 error. There is no call to servlet at all. I think that Spring does not find this servlet. How could I fix it? While I am launching app at localhost everything works fine.
Test:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringBootTest
#AutoConfigureMockMvc
public class SpringDataProcessorTest {
#Autowired
private MockMvc mockMvc;
#Test
public void retrieveByRequest() throws Exception{
mockMvc.perform(buildRetrieveCustomerByIdRequest("1")).andExpect(status().isOk());
}
private MockHttpServletRequestBuilder buildRetrieveCustomerByIdRequest(String id) throws Exception {
return get(String.format("/path/get('%s')", id)).contentType(APPLICATION_JSON_UTF8);
}
}
Configuration:
#Configuration
public class ODataConfiguration extends WebMvcConfigurerAdapter {
public String urlPath = "/path/*";
#Bean
public ServletRegistrationBean odataServlet(MyServlet servlet) {
return new ServletRegistrationBean(servlet, new String[] {odataUrlPath});
}
}
MyServlet:
#Component
public class MyServlet extends HttpServlet {
#Autowired
private ODataHttpHandler handler;
#Override
#Transactional
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
handler.process(req, resp);
} catch (Exception e) {
log.error("Server Error occurred", e);
throw new ServletException(e);
}
}
}
If you really want to use MockMvc to test your code instead of a Servlet use a HttpRequestHandler and use a SimpleUrlHandlerMapping to map it to a URL.
Something like the following.
#Bean
public HttpRequestHandler odataRequestHandler(ODataHttpHandler handler) {
return new HttpRequestHandler() {
public void handleRequest() {
try {
handler.process(req, resp);
} catch (Exception e) {
log.error("Server Error occurred", e);
throw new ServletException(e);
}
}
}
}
And for the mapping
#Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setUrlMap(Collections.singletonMap(odataUrlPath, "odataRequestHandler");
return mapping;
}
Another solution would be to wrap it in a controller instead of a servlet.
#Controller
public class ODataController {
private final ODataHttpHandler handler;
public ODataController(ODataHttpHandler handler) {
this.handler=handler;
}
#RequestMapping("/path/*")
public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException {
try {
handler.process(req, resp);
} catch (Exception e) {
log.error("Server Error occurred", e);
throw new ServletException(e);
}
}
}
In either way your handler should be served/processed by the DispatcherServlet and thus can be tested using MockMvc.
I have an Java socket API application, that handles socket requests from users and sends responses.
I have a configurer:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
private static final Logger LOGGER = Logger.getLogger(WebSocketConfig.class);
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/queue");
config.setApplicationDestinationPrefixes("/server_in");
config.setUserDestinationPrefix("/user");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket").withSockJS();
}
}
When i send response to user i do the following:
this.simpMessagingTemplate.convertAndSend("/queue/private/user_"+secret_key, socketResponse);
On client i have the following code:
sc.subscribe('/queue/private/user_'+secret_key, function (greeting) {
console.log(greeting.body);
});
And the response is handled successfully.
But the problem is that some other user can also subscribe to "/queue/private/*" destination and handle private messages.
sc.subscribe('/queue/private/*', function (greeting) {
console.log(greeting.body);
});
How can I privent that behaviour?
If you want each user to have a socket and only get him messages, what you can do is :
Subscribe as you do to the endPoint but with "/user" infront for example
sc.subscribe('/user/queue/websocket, function (greeting) {
console.log(greeting.body);
});
and at the server side you should have a rest method:
#RequestMapping(value = "/test", method = RequestMethod.POST)
public void test(Principal principal) throws Exception {
this.template.convertAndSendToUser(principal.getName(), "/queue/click", "");
}
With this every user subscibers to each channel and only the user is notified about, when a rest call is made.
The rest call should be authenticated so the Principal has the username.
The user channel is auto managed from Spring so you have to add it like this.
You can extend ChannelInterceptorAdapter and manage each event individually:
public class AuthorizedChannelInterceptorAdapter extends ChannelInterceptorAdapter {
#Override
public Message<?> preSend(Message<?> message, MessageChannel messageChannel) throws AuthenticationException {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT == accessor.getCommand())
setUserAuthenticationToken(accessor);
else if (StompCommand.SUBSCRIBE == accessor.getCommand())
validateSubscription((Authentication) accessor.getUser(), accessor.getDestination());
return message;
}
private void setUserAuthenticationToken(StompHeaderAccessor accessor) {
String token = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION);
accessor.setUser(loadAuthentication(token));
}
private Authentication loadAuthentication(String token){
return ....;
}
private void validateSubscription(Authentication authentication, String destination) {
if(...)
throw new AccessDeniedException("No permission to subscribe to this topic");
}
}
First of all you will need to store the authentication object provided by the client in connection event. After this, each event sent by the client will have this authentication object set so you can use it to validate if it is authorized to subscribe to a particular channel.