I am using websockets without Stomp. What is the correct way to decide to whom USer WebSocketSession belongs to?
In my WsConfig i use:
#Configuration
#EnableWebSocket
public class WebSocketServerConfiguration implements WebSocketConfigurer {
protected final CustomWebSocketHandler webSocketHandler;
#Autowired
public WebSocketServerConfiguration(CustomWebSocketHandler webSocketHandler) {
this.webSocketHandler = webSocketHandler;
}
#SuppressWarnings("NullableProblems")
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler, "/ws")
.addInterceptors();
}
}
my WsHandler is currently empty:
#Service
public class SplitBillWebSocketHandler extends TextWebSocketHandler {
#Override
public void handleTransportError(WebSocketSession session, Throwable throwable) throws Exception {
}
#Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
}
#Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
//determine User for session
User user = determineUser(session);
sessionStorage.add(user.getName(),session);
}
#Override
protected void handleTextMessage(WebSocketSession session, TextMessage jsonTextMessage) throws Exception {
}
}
What is the way to determine the user? Or what is the best practice to do this?
Do I need to pass some parameter to websocket URL from client ( which isn't standard as far as I am aware ), or how to identify the session?
Thanks for help!
Related
I have a spring boot application for which I secure methods using spring security annotations as shown below.
public interface UserService {
#PreAuthorize("hasRole('ADMIN')")
List<User> findAllUsers();
#PostAuthorize ("returnObject.type == authentication.name")
User findById(int id);
#PreAuthorize("hasRole('ADMIN')")
void updateUser(User user);
#PreAuthorize("hasRole('ADMIN') AND hasRole('DBA')")
void deleteUser(int id);
}
The problem that I have here is that there are implementations which are called via #RabbitListener methods which does not have any security context because it's not a web request.
I can expect the clients to pass the usernames via message headers sp I could use message.getMessageProperties().getHeaders().get("username") to get the username for that request.
But still I believe there won't be a straightforward approach as the method level annotations won't be evaluated because they are not under security context.
Is there any approach via which I can establish a security context for a spring amqp messages?
Just add the context...
#SpringBootApplication
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class So49957413Application extends GlobalAuthenticationConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(So49957413Application.class, args);
}
#Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("foo").password("bar").roles("baz");
}
#Autowired
private Foo foo;
#RabbitListener(queues = "foo")
public void listen(Message in) {
try {
SecurityContext ctx = SecurityContextHolder.createEmptyContext();
ctx.setAuthentication(
new UsernamePasswordAuthenticationToken(in.getMessageProperties().getHeaders().get("user"), "bar"));
SecurityContextHolder.setContext(ctx);
this.foo.method1();
try {
this.foo.method2();
}
catch (AccessDeniedException e) {
System.out.println("Denied access to method2");
}
}
finally {
SecurityContextHolder.clearContext();
}
}
#Bean
public Foo foo() {
return new Foo();
}
public static class Foo {
#PreAuthorize("hasRole('baz')")
public void method1() {
System.out.println("in method1");
}
#PreAuthorize("hasRole('qux')")
public void method2() {
System.out.println("in method2");
}
}
}
and
in method1
Denied access to method2
It would be better to add/remove the security context in an advice, in the listener container's advice chain, to avoid mixing the security code with your listener logic.
I have implemented a console application using Spring and WebSockets. The application works fine if one or more participants are connected to the base method which is anotated like this.
#MessageMapping("/chat")
#SendTo("/topic/messages")
I will copy the configuration and the implementation which i have made o be more clear.
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat").withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
}
#Controller
public class ChatController {
#MessageMapping("/chat")
#SendTo("/topic/messages")
public OutputMessage send(#Payload Message message) {
return new OutputMessage(message.getFrom(), message.getText());
}
#MessageMapping("/chat/{room}")
#SendTo("/topic/messages/{room}")
public OutputMessage enableChatRooms(#DestinationVariable String room, #Payload Message message) {
return new OutputMessage(message.getFrom(), message.getText());
}
}
#Service
public class SessionHandlerService extends StompSessionHandlerAdapter {
private String nickName;
public SessionHandlerService() {
this.nickName = "user";
}
private void sendJsonMessage(StompSession session) {
ClientMessage msg = new ClientMessage(nickName, " new user has logged in.");
session.send("/app/chat", msg);
}
#Override
public Type getPayloadType(StompHeaders headers) {
return ServerMessage.class;
}
#Override
public void handleFrame(StompHeaders headers, Object payload) {
System.err.println(payload.toString());
}
#Override
public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
session.subscribe("/topic/messages", new SessionHandlerService());
sendJsonMessage(session);
}
}
The problem which i face is that when i subscribe to /topic/messages and session.send("/app/chat", msg);everything works fine. But if i choose something like session.send("/app/chat/room1", msg); and /topic/messages/room1 the participans can not see each other messages like they are in different chat rooms.
I have several folders in /static/img/** and I need to add interceptors to some of them to check user permissions. I've used interceptors earlier and added them this way:
#SpringBootApplication
#EnableTransactionManagement
public class Application extends WebMvcConfigurerAdapter {
...
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry
.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/");
}
#Bean
public AuthHeaderInterceptor authHeaderInterceptor() {
return new AuthHeaderInterceptor();
}
#Bean
public AuthCookieInterceptor authCookieInterceptor() {
return new AuthCookieInterceptor();
}
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry
.addInterceptor(authHeaderInterceptor())
.addPathPatterns(REST_URL)
.excludePathPatterns(
new String[] {
REST_SECURITY_URL,
REST_SETTINGS_URL,
REST_REPORTS_URL
}
);
registry
.addInterceptor(authCookieInterceptor())
.addPathPatterns(REST_REPORTS_URL);
}
}
All works fine for rest controllers and their URLs, but now I need to secure some static resources and I added this:
#SpringBootApplication
#EnableTransactionManagement
public class Application extends WebMvcConfigurerAdapter {
...
#Bean
public RoleAdminInterceptor roleAdminInterceptor() {
return new RoleAdminInterceptor();
}
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry
.addInterceptor(authHeaderInterceptor())
.addPathPatterns(REST_URL)
.excludePathPatterns(
new String[] {
REST_SECURITY_URL,
REST_SETTINGS_URL,
REST_REPORTS_URL
}
);
//THIS NOT WORK
registry
.addInterceptor(roleAdminInterceptor())
.addPathPatterns("/static/img/admin/**");
registry
.addInterceptor(authCookieInterceptor())
.addPathPatterns(REST_REPORTS_URL);
}
}
Commented line doesn't work. When I send request to /static/img/admin/test.png RoleAdminInterceptor is never called.
What I'm doing wrong?
I know this is an old question, but since it's unanswered it might help others searching for it.
This is what worked for me:
1- Declare an interceptor class:
class RoleBasedAccessInterceptor implements HandlerInterceptor {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
AntPathMatcher matcher = new AntPathMatcher();
String pattern = "/static/img/admin/**";
String requestURI = request.getRequestURI();
if (matcher.match(pattern, requestURI)) {
//Do whatever you need
return validateYourLogic();
}
return true;
}
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
#Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
2- Configure WebMvcConfigurer
public class WebMvcConfiguration implements WebMvcConfigurer {
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RoleBasedAccessInterceptor());
}
}
I think in this case you could use Filters with Spring Security instead of Interceptors as you could Validate the access earlier on the process even before hitting the Interceptor, unless there is a specific use case that you need to use the interceptor here.
Some topic about the difference between these two:
filters-vs-interceptor
In other spring platform , I used websocket to send binary message,like this:
ByteBuffer bf = ByteBuffer.wrap(bytes);
old.getBasicRemote().sendBinary(bf);
But with spring-boot, I make my class extends TextWebSocketHandler. But in the method handleTextMessage(WebSocketSession session, TextMessage message) , only have param WebSocketSession and it has no method to send binary.
I try to use BinaryMessage,like this:
session.sendMessage(new BinaryMessage(bytes));
But the client side get the result as Blob(js type), I don't what to do...
you can use BinaryWebSocketHandler to handle binary message communication.
full example
#Configuration
#EnableAutoConfiguration
#EnableWebSocket
public class AppWebSocket {
public static void main(String[] args) {
SpringApplication.run(AppWebSocket.class, args);
}
#Component
public static class MyWebSocketConfigurer implements WebSocketConfigurer {
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyTextHandler(), "/text").withSockJS();
registry.addHandler(new MyBinaryHandler(), "/binary").withSockJS();
}
}
#Component
public static class MyTextHandler extends TextWebSocketHandler {
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
session.sendMessage(new TextMessage("hello world"));
}
}
#Component
public static class MyBinaryHandler extends BinaryWebSocketHandler {
public void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
try {
session.sendMessage(new BinaryMessage("hello world".getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
This is probably obvious, but I am new to this paradigm. I create a Jetty Server and register my websocket class as follows:
Server server = new Server(8080);
WebSocketHandler wsHandler = new WebSocketHandler()
{
#Override
public void configure(WebSocketServletFactory factory)
{
factory.register(MyEchoSocket.class);
}
};
server.setHandler(wsHandler);
The websocket receives messages fine. I would like to also be able to send messages out from the server without having first received a message from the client. How do I access the MyEchoSocket instance that's created when the connection opens? Or, more generally, how do I send messages on the socket outside of the onText method in MyEchoSocket?
Two common techniques, presented here in a super simplified chatroom concept.
Option #1: Have WebSocket report back its state to a central location
#WebSocket
public class ChatSocket {
public Session session;
#OnWebSocketConnect
public void onConnect(Session session) {
this.session = session;
ChatRoom.getInstance().join(this);
}
#OnWebSocketMessage
public void onText(String message) {
ChatRoom.getInstance().writeAllMembers("Hello all");
}
#OnWebSocketClose
public void onClose(int statusCode, String reason) {
ChatRoom.getInstance().part(this);
}
}
public class ChatRoom {
private static final ChatRoom INSTANCE = new ChatRoom();
public static ChatRoom getInstance()
{
return INSTANCE;
}
private List<ChatSocket> members = new ArrayList<>();
public void join(ChatSocket socket)
{
members.add(socket);
}
public void part(ChatSocket socket)
{
members.remove(socket);
}
public void writeAllMembers(String message)
{
for(ChatSocket member: members)
{
member.session.getRemote().sendStringByFuture(message);
}
}
public void writeSpecificMember(String memberName, String message)
{
ChatSocket member = findMemberByName(memberName);
member.session.getRemote().sendStringByFuture(message);
}
public ChatSocket findMemberByName(String memberName)
{
// left as exercise to reader
}
}
Then simply use the central location to talk to the websockets of your choice.
ChatRoom.getInstance().writeSpecificMember("alex", "Hello");
// or
ChatRoom.getInstance().writeAllMembers("Hello all");
Option #2: Have WebSocket be created manually with WebSocketCreator
#WebSocket
public class ChatSocket {
public ChatRoom chatroom;
public ChatSocket(ChatRoom chatroom)
{
this.chatroom = chatroom;
}
#OnWebSocketConnect
public void onConnect(Session session) {
chatroom.join(this);
}
#OnWebSocketMessage
public void onText(String message) {
chatroom.writeAllMembers(message);
}
#OnWebSocketClose
public void onClose(int statusCode, String reason) {
chatroom.part(this);
}
}
public class ChatCreator implements WebSocketCreator
{
private ChatRoom chatroom;
public ChatCreator(ChatRoom chatroom)
{
this.chatroom = chatroom;
}
public Object createWebSocket(UpgradeRequest request,
UpgradeResponse response)
{
// We want to create the Chat Socket and associate
// it with our chatroom implementation
return new ChatSocket(chatroom);
}
}
public class ChatHandler extends WebSocketHandler
{
private ChatRoom chatroom = new ChatRoom();
#Override
public void configure(WebSocketServletFactory factory)
{
factory.setCreator(new ChatCreator(chatroom));
}
}
At this point you can use the same techniques as above to talk to the websockets of your choice.