DEV Community

loading...

SPA with Springboot+JWT+Vue.js+Web socket

lechatthecat profile image lechat ・5 min read

Hello people. I have created a web forum. You can see it here:
https://chatboard.page/

What is this?

This is a Single page application that was created with:

  • Vue.js
  • JWT authentication
  • Web socket (Stomp messages)
  • webpack
  • Spring boot
  • JPA
  • PostgreSQL

This app is a kind of forum that is 2ch + Slack. In this forum, you can add comment as an anonymous user to a board until 1000 comments like in 2ch, but comments are updated in real time like in Slack.

Sometimes I use internet forums like Reddit or 2ch. But sometimes, like when I am playing game, I want them to update by itself. Don't you feel same? So I created this app.

You can create your boards, which are visible from all users, from here:
Alt Text

It has a sign-in feature with JWT authentication, but you can add comments without sigining in. Without sigining in, you cannot delete comments because it is difficult to differentiate IP users. Maybe, later, I will implement a functionality to delete a comment by password, which enables IP users to delete their comments.

Single Page Application

This app is a single page application. Wikipedia explains what it is as follows:

A single-page application (SPA) is a web application or website that interacts with the user by dynamically rewriting the current web page with new data from the web server, instead of the default method of the browser loading entire new pages. The goal is faster transitions that make the website feel more like a native app.

As this says, the app has only one html page as follows:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>ChatBoard</title>
    <link href="/public/css/main.css" rel="stylesheet">
    <link rel="icon" type="/image/png" href="/public/img/favicon/favicon.ico">
  </head>
  <body>
  <noscript>
    <strong>We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
  </noscript>
  <div id="app" v-cloak></div>
  <!-- built files will be auto injected -->
  </body>
  <script src="/public/js/main.js"></script>
  <script src="/public/js/runtime.js"></script>
</html>

Enter fullscreen mode Exit fullscreen mode

You can see that there is a tag with id="app" in the html above. In SPA, Vue.js dynamically operate virtual DOMs in the tag with id="app" to create each page of the web app, which is faster than real DOM operations.

But benefit of single page application isn't only the fast virtual DOM operations.

The data that needs to be fetched from DB is passed by the server side (spring boot / java) in json format. So, once browser loads the whole js/css in the first access, what the server has to do is passing necessary json formatted data for each page. The first load can be longer, but after the first load, the data exchanged between the user and the server is only json. So, usually it is very fast after the long first load.

But, actually I have not created all of the components of Vue. I am using CoreUI-Vue. This is a design template that has MIT License, which means you can use the template for your business as long as you show their license in your app.

Controller for SPA

As I explained, SPA has only html page, which means html file (let's say, index.html) has to accept all of the get/post requests for page view (but except requests for api/webscoket etc). So I created a controller that receives every request but ones for api/websokect as follows:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class SpaController {
    @RequestMapping(value = {"/{path:^(?!api|public|ws)[^\\.]*}", "/**/{path:^(?!api|public|ws).*}/{path:[^\\.]*}"})
    public String get(){
        return "index";
    }
}

Enter fullscreen mode Exit fullscreen mode

This controller accepts all requests that doesn't contain "/api", "/ws", "/public" in the path, so, for example, "/public/css/test.css" is not redirected to the index.html, but "/chat/detail" is redirected to index.html, which has Vue.js and virtual DOMs.

JWT authentication

JWT is an acronym for "JSON Web Token". This is used to manage users' sign-in sessions. Usually, in MPA, sign-in session is checked its validity in every page load. But as SPA has only one page, you cannot check the validity of sign-in session in this way.

So I decided to go with JWT authentication to manage users's sign-in sessions. In Security Config of Spring boot, you can see that it checks the validity of JWT token in every http request by http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);.

public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .cors()
                .and()
            .csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .and()
            .authorizeRequests()
                /** .... omitted .... **/
                .and()
            .httpBasic().disable();
            // Add a filter to validate the tokens with every request
            http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    }
Enter fullscreen mode Exit fullscreen mode

You can see many tutorials on how to implement JWT authentication by googling "jwt spring boot".

Websocket

What is websocket?

WebSocket is a protocol which enables communication between the server and the browser. It has an advantage over RESTful HTTP because communications are both bi-directional and real-time. This allows for the server to notify the client at any time instead of the client polling on a regular interval for updates. -- Vonage - https://learn.vonage.com/blog/2018/10/08/create-websocket-server-spring-boot-dr/

This app is using websocket because chat with websocket can be easily implemented with Spring boot. To use websocket, we must define an endpoint and a message broker for websocket:

public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {
    private static final Logger logger = LoggerFactory.getLogger(WebsocketConfig.class);


    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry
        .addEndpoint("/ws")
        .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/app");
        registry.enableSimpleBroker("/chat", "/queue");   // Enables a simple in-memory broker

        //   Use this for enabling a Full featured broker 
        /*
        registry.enableStompBrokerRelay("/chat")
                .setRelayHost("localhost")
                .setRelayPort(61613)
                .setClientLogin("guest")
                .setClientPasscode("guest");
        */
    }
}
Enter fullscreen mode Exit fullscreen mode

I used these links to learn how they work:

Then in javascript side, we can connect to the endpoint by something like this:

this.stompClient = new Stomp.Client(connectionOption);
// https://stomp-js.github.io/guide/stompjs/using-stompjs-v5.html#create-a-stomp-client
// https://stomp-js.github.io/guide/stompjs/rx-stomp/ng2-stompjs/using-stomp-with-sockjs.html#example-with-stompjs
// Use 'wss://' for https, use 'ws://' for http connection.
// Also it seems '/websocket' after '/ws' is essential.
const wsUri = (window.location.protocol === 'https:' ? 'wss://' : 'ws://') +
  window.location.host + '/ws/websocket';

console.log(wsUri);

const xsrf = this.getCookie("XSRF-TOKEN");
const connectionOption = {
  brokerURL: wsUri,
  connectHeaders: {
    "X-XSRF-TOKEN": xsrf
  },
  debug: function (str) {
    console.log(str);
  },
  reconnectDelay: 10000,
  heartbeatIncoming: 4000,
  heartbeatOutgoing: 4000,
};

this.stompClient = new Stomp.Client(connectionOption);

this.stompClient.onConnect = () => { /** Do something when websocket is connected **/ };
this.stompClient.onStompError =  () => { /** Do something when there is error **/ };
this.stompClient.activate();
Enter fullscreen mode Exit fullscreen mode

These two links were very helpful to learn how to use stomp client:

What I felt after creating the app

Websocket was more difficult than I thought.. But anyway I completed it.

Discussion (0)

pic
Editor guide