react – Visionmate – En fullservice digital partner

Push-notiser i Grails & React.js baserade webbprojekt

Det här inlägget riktar sig kanske mer mot dig som utvecklare än dig som beställare. Som oftast med system så krävs det en kommunikation mellan servern och klienten. Det är viktigt eftersom olika händelser kan uppstå på servern och en anmälan ska då skickas till klienten. Ett enkelt user cace är en chatt, där användarna kan utbyta meddelanden mellan varandra. Vi kan också använda annan teknik, till exempel Message Queue system. Beroende på behoven så kan man välja den man har mest användning för.

Beskrivning

I det här inlägget kommer vi att diskutera push-meddelanden med hjälp av WebSocket. Jag vill beskriva pedagogiken bakom arbetet, när det kommer till leverans och produktion så har vi självklart enklare rutiner på företaget för att bemöta varje beställning.

Mål

Att kunna genomföra push-notiser i webbapplikationer

Krav

Grails 3.* (Dependencies: Grails Spring WebSocket, Grails Spring Security Rest)

React.js (Dependencies: Sock.js client, Stomp.js)

 

1 – Server konfiguration

Vi måste konfigurera WebSocket message agent för att låta vårt system skicka och ta emot meddelanden via säkra kanaler.

Följande kod (configureClientInboundChannel metod) visar hur man autentiserar via WebSocket. Vi måste vara säkra på att endast en auktoriserad användare kan ansluta, prenumerera och ta emot meddelanden från vår server. Vårt system använder Spring Security Rest och följaktligen är användarna autentiserade via access token i varje förfrågan.

Vi konfigurerar också den endpoint där vår message agent lyssnar på subscription requests och meddelanden (registerStompEndpoints method)

import grails.plugin.springsecurity.rest.token.storage.TokenStorageService
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.messaging.simp.SimpMessageType
import org.springframework.messaging.simp.config.ChannelRegistration
import org.springframework.messaging.simp.stomp.StompHeaderAccessor
import org.springframework.messaging.support.ChannelInterceptorAdapter
import org.springframework.messaging.support.MessageBuilder
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker
import org.springframework.web.socket.config.annotation.StompEndpointRegistry
import org.springframework.messaging.Message
import org.springframework.messaging.MessageChannel
 

@Configuration
@EnableWebSocketMessageBroker
class DefaultWebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

   public static final String AUTHORIZATION = 'Authorization'
   public static final String BEARER = 'Bearer'

   @Autowired
   TokenStorageService tokenStorageService
 

@Override
void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
   stompEndpointRegistry.addEndpoint("/ws")
         .setAllowedOrigins("*")
         .withSockJS()
         .setSessionCookieNeeded(false)
}
 

void configureClientInboundChannel(ChannelRegistration registration) {
   registration.setInterceptors(new ChannelInterceptorAdapter() {
      Message<Object> preSend(Message<Object> message,  MessageChannel channel) {
         String token = null
         Boolean access = false
         StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message)
         List tokens = accessor.getNativeHeader(AUTHORIZATION)
         accessor.removeNativeHeader(AUTHORIZATION)

         if(tokens?.size > 0) {
            token = tokens[0]
         }

         if (token?.startsWith(BEARER)) {
            token = token.substring(BEARER.length()).trim()
         }

         switch (accessor.messageType){
            case SimpMessageType.CONNECT: access = (tokenStorageService.loadUserByToken(token))?true:false
               break
            case SimpMessageType.SUBSCRIBE: access = (tokenStorageService.loadUserByToken(token))?true:false
               break
         }
         accessor.setLeaveMutable(true)
         return (access)?MessageBuilder.createMessage(message.payload, accessor.messageHeaders):null
      }
   })
}

2 – Skicka meddelanden

Vi använder Grails Spring WebSocket vilket ger oss den funktionaliteten som behövs för att kunna använda konfigurationen.

I vårt exempel kommer meddelandena att ha en relaterad åtgärd, för att radera, uppdatera eller skapa meddelanden i klienten. Detta är också möjligt om vi skapar olika ämnen för det ändamålet, men i det här fallet är det inte relevant. BrokerService ansvarar för att skicka meddelanden till en destination med hjälp av brokerMesssagingService.

class BrokerService {

    def brokerMessagingTemplate
    /**
     * Send message to destination
     * @param destination
     * @param message
     */
    void send(String destination, Map message) {
        brokerMessagingTemplate.convertAndSend destination, (message as JSON) as String
    }
}

3 – Web client configuration

I webbklienten använder vi Sock.js och Stomp.js för att prenumerera på vår meddelandekanal. För att prenumerera på en kanal i connection request så skickar vi header configuration med användarens token som behörighet.

Vi ställer som en parameter en callback-funktion som kommer att utföras efter att ha mottagit ett meddelande från servern. Med unsubscribe method kan du ta bort en prenumeration, användaren kommer då inte längre att få push-meddelanden när det är gjort.

import {SERVER_URL} from "../config";
import headers from "../security/headers";
import SockJS from 'sockjs-client';
import Stomp from 'stompjs';

/**
 * WebSockets support
 */
const WebSocket = (endpoint) => {

    const socket = SockJS(`${SERVER_URL}/${endpoint}`);
    const client = Stomp.over(socket);

    function subscribe(destination, callback) {
        client.connect(headers(), function(frame) {
            client.subscribe(destination, function(message){
              callback(JSON.parse(message.body))
              }, headers());
           });
        return client;
    }

    function unsubscribe(client){
        for (const sub in client.subscriptions) {
            if (client.subscriptions.hasOwnProperty(sub)) {
                client.unsubscribe(sub);
            }
        }
    }

    return {
        subscribe,
        unsubscribe
    }

};

export default WebSocket;

4 – Prenumeration och mottagande av anmälningar

Vi lägger subscription call på componentDidMount med en klass baserad på React Component.

Vi måste också implementera funktionaliteten som ska utföras när vi får notiserna.

import React from 'react';
import WebSocket from "../../api/WebSocket";
import {ACTION_STATUS} from '../../constant';
import MessageList from "../../components/MessageList";
import { notifyAddedMessage, notifyDeletedMessage,  notifyUpdatedMessage} from "../../actions/actions";


/**
 * Real-time messages
 */
class MessagePanel extends React.Component {

    componentDidMount() {
        this.subscribe();

    }

    componentWillUnmount() {
        if(this.state.client !== undefined)
            WebSocket('ws').unsubscribe(this.state.client);
    }

    subscribe() {
        const callback = (message) => {
            switch (message.action){
                case ACTION_STATUS.DELETED:
                    this.props.notifyDeletedMessage(message.id);
                    break;
                case ACTION_STATUS.UPDATED:
                    this.props.notifyUpdatedMessage(message);
                    break;
                default:
                    this.props.notifyAddedMessage(message);
            }
        };
        callback.bind(this);

        let client;
        if (user instanceof Object) {
            client = WebSocket('ws').subscribe(‘/topic/messages’, callback);
            this.setState({client});
        }
    }

    render() {
        return (
            <MessageList/>
        )
    }
}

Slutsats

Push-meddelanden gör systemet attraktivt och mer användbart, det minskar också serverns latency och förbättrar användarupplevelsen. I många fall kan vi, på ett enkelt sätt och med ett bra resultat, lägga till det med hjälp av WebSockets .

Referens

https://docs.spring.io/spring/docs/5.0.0.BUILD-SNAPSHOT/spring-framework-reference/html/websocket.html

 

/Carlos Senior Developer på Visionmate

Alla artiklar

Tester i React med Jest och Enzyme

Programvarutestning är en väsentlig del av mjukvaruutveckling och säkerställer att koden är hållbar. Automatiska tester hjälper till att hitta nya buggar, förbättra kvaliteten och kompletterar också den manuella testningen.

I den här inlägget kommer vi att gå igenom några av de vanligaste sätten för att testa dina React-applikationer (med CRA ver, 2.1.3).

Då sätter vi igång så låt mig introducera dagens hjältar:

  1. Jest – ett JavaScript-testramverk framtaget av Facebook. Vi använder CRA där Jest installeras automatiskt. Jest’s jobb är att ta alla testfiler i vårt projekt, köra testerna och visa resultatet i terminalen. Jest kan användas med non-React applikationer.
  2. Enzyme – är ett testverktyg utvecklat av Airbnb. Enzyme använder många av de verktyg som tillhandahålls av React för att bygga sitt API. Enzyme fungerar också bara med React.

SET UP

  • React application with CRA
create-react-app testing
cd testing
npm start
  • Enzyme
npm install --save enzyme enzyme-adapter-react-16

Var uppmärksam på vilken versionen av React som du använder. Här använder vi 16, men Enzyme har adapters för alla versioner. Här hittar du fler detaljer: Enzyme installation guide.

För att slutföra konfigurationen måste vi skapa en ny fil som heter setupTests.js i src directory, på så sätt kan vi använda Enzyme i testfilerna. Observera att namnet är viktigt här.

setupTests.js

import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({adapter: new Adapter() });

Sådär, installationen är klar!

 

FUNKTIONER I ENZYME

Vi använder funktionerna nedan för att skapa instances av våra komponenter och returnera objekt som vi kan använda för att skriva tester.

  • Static – renders given component och skickar tillbaka ren HTML
  • Shallow – mest grundläggande versionen, renders only given component utan children. Perfekt för unit tests
  • Full DOM – renders the components with all children, här kan vi interagera med komponenterna

TEST STRUCTURE

Testerna kommer att placeras i en ny katalog __tests__

Ha för vana att organisera testerna på det här sättet. Jest kör alla filer som finns inuti.

För att göra det enkelt att identifiera vilket test som tillhör till vilken fil så beskriver vi dem på samma sätt som ”testad fil”: .test.js i slutet.

 

Våra test kommer att vara schemalagda:
it ('test description', function which contain logic of our test);

Funktionen i vårt test ska innehålla få förväntningar, vi kan beskriva det värde som vi förväntar oss att se.

Äntligen TEST!

Nedan är innehållet av två enkla components:

List.js

import React, { Component } from 'react';


class List extends Component {
    render() {
        return (
            <form>
              <h3>Add new element</h3>
              <textarea />
              <div>
                <button>Save</button>
              </div>
            </form>
        );
    }
}

export default List;

App.js

import React from 'react';
import List from './List'

export default()=>{
    return (
        <div><List /></div>
    );
};

Låt oss helt enkelt testa rendering List.js-komponenten. Vi kommer att använda shallow function eftersom vi inte behöver render children. Om komponenten fungerar korrekt kommer testet att fungera, annars misslyckas det.

List.test.js

import React from 'react';
import { shallow } from 'enzyme'; 
import List from '../List';

it('renders without crashing', () => {
  shallow(<List />);
});

Nu kollar vi om testet fungerade genom typing in terminal

npm test

Härligt! Vårt första test lyckades! För att avsluta tryck ctrl+c.

Nu ska vi kontrollera components instance. Vi vill kontrollera om List.js-komponenten finns i App.js.

App.test.js

import React from 'react';
import { shallow } from 'enzyme'; 
import App from '../App';
import List from '../List';

it('shows the list', () => {
  const wrapped = shallow(<App />);
  expect(wrapped.find(List).length).toEqual(1);
});

const wrapped betyder att objektet som vi kommer att ha från shallow (<App />) är en inbäddad version av App-komponenten.

Och båda testerna gick igenom!

På samma sätt som ovan kan vi kontrollera om din komponent innehåller button och input area:

List.test.js

import React from 'react';
import { mount } from 'enzyme'; 
import List from '../List';

it('contains button and text area', () => {
  const wrapped = mount(<List />);
  expect(wrapped.find('textarea').length).toEqual(1);
  expect(wrapped.find('button').length).toEqual(1);
});

Find är ett kraftfull testverktyg. Tack vare det kan vi upptäcka varje klass, HTML-element, attributsyntax, prop eller objektegenskapsväljare och kontrollera om det uppfyller våra förväntningar.

Nu är frågan, hur testar jag ett test?

Vi kan till exempel göra det genom att ändra ett förväntat värde till ett felaktigt. Låt oss låtsas att vi inte har någon button i List.js, vi sätter värde 0 istället för 1 i testfilen. Nu ska testet misslyckas

List.test.js

it('shows button', () => {
    const wrapped = shallow (<List />);
    expect(wrapped.find('button').length).toEqual(0);
});

Japp det misslyckades och Jest visar exakt vad som gick fel.

 

SLUTSATS

Jest och Enzyme är väl integrerade- och flexibla verktyg för att utföra mjukvarutester. Konfigurationen är lätt vilket såklart är en stor fördel. Jag grottade inte ner mig i specifika metoder utan tanken var att ge en god överblick av Jest’s och Enzyms effektivitet.

Origin: www.linkedin.com

Alla artiklar

Förbättra React forms med Redux Form

Intro

Redux är ett utmärkt verktyg för att dramatiskt förbättra datahanteringen i din applikation. Då borde det vara ett naturligt steg att använda det också för dina application forms. Absolut, men varför återuppfinna hjulet igen?

Redux Form gör exakt det du kan förvänta dig – det använder application state för att hantera form data utan att du behöver skriva all kod själv.

I det här inlägget kommer jag gå igenom processen med att konfigurera Redux-form och ge några exempel på typiska fall.

Setup

Redux Form-inställningen innehåller följande steg:

Inkludera form reducer i combineReducers function (det måste göras en gång per form i din applikation)

import { combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';
const rootReducer = combineReducers({  
/* Other reducers */  
    form: formReducer
});
export default rootReducer;

Link selected form to form reducer (låt oss säga att du har en komponent som heter LoginPage)

import {reduxForm} from "redux-form";

export default reduxForm({ 
  form: 'login' /* This must be unique */ 
})(LoginPage);

I själva komponenten måste du länka din form till en funktion som kommer att utföras när du submittar. Detta görs via “handleSubmit” som tillhandahålls av Redux Form.

const {handleSubmit} = props
return (
    <form onSubmit={handleSubmit(login)}>
      <Field
        name='username'
        placeholder = "Enter username"
        component="input"type='text'
      />
      <Field
        name='password'
        placeholder = "Enter password"
        component="input"
        type='password'
      />
      <button type="submit">Login</button>
    </form>
)

Out form innehåller två inmatningsfält (för användarnamn och lösenord) och en knapp.

Inloggningsfunktionen kan vara en komponentfunktion eller en redux-åtgärd – vi behöver bara lägga till den med hjälp av anslutningsfunktionen. Det kommer att användas för asynkron validering.

import {login} from "../actions/actions";

/* If you need to connect your component to redux,
it's best to do it before applying reduxForm HOC*/
LoginPage = connect (mapStateToProps, {login})(LoginPage);

export default reduxForm({
    form: 'login'    
})(LoginPage);

Synkron validering

För att minska belastningen på back-end och förbättra användarupplevelsen är det bäst att utföra synkron validering när det är möjligt – till exempel kan vi kontrollera om användarnamnet och/eller lösenordet innehåller ogiltiga tecken.

Det första steget är att lägga till valideringsfunktionen via reduxForm HOC (higher-order component):

export default reduxForm({
    form: 'login',
    validate
})(LoginPage);

Och lägg till själva valideringsfunktionen ovanför LoginPage container.

const validate = (values) => {
    const errors = {}
    if(!values.username) {
        errors.username = "Cannot be empty"
    }
    if(!values.password) {
        errors.password = "Cannot be empty"
    }
    return errors
}

När vi ändrar användarnamn eller lösenord, kommer den synkrona valideringen att köras och kontrollera om båda fälten är populated. Om en valideringsfunktion returnerar errors så kommer formuläret inte att skickas in.

Asynkron validering

Om form values framgångsrikt överför synkroniserad validering så skickas de vanligtvis till en server via ett asynkront anrop. Med hjälp av Redux-formuläret kan vi inaktivera formuläret tills vår begäran har behandlats och ett felmeddelande visas om det inte lyckades.

handleSubmit kommer utföra vår inloggningsåtgärd:

export function login(values) {    
    return (dispatch) => {        
        return ApiConnector.login(values.username, values.password)
            .then(response => {
                if(response.data.success === true){
                  dispatch({
                    type: LOGIN
                  })
                } else {
                  throw new SubmissionError({        
                    _error: "Login failed!"
                    })

                }
            }) 
            .catch((error) => {              
              throw new SubmissionError({        
                _error: "Login failed!"
              })
            })
    }
}

Eftersom denna åtgärd använder ett asynkront anrop, måste vi använda Redux Thunk middleware. Funktionen ApiConnector.login är en post request som skickas med hjälp av axios. För att asynkron validering ska fungera korrekt är det viktigt att returnera Promise-objektet.

Vi kan se att om förfrågan till servern misslyckas (till exempel om en server är nere) eller inloggningen misslyckas, då returneras ett felmeddelande tillbaka till vårt formulär.

Hur kan vi visa felmeddelandet? Vi ändrar vår form container för att extrahera felet från props och visa den ovanför knappen “Logga in”.

const {handleSubmit, error} = props
return (
  <form onSubmit={handleSubmit(login)}>
    <Field
      name='username'
      placeholder = "Enter username"
      component="input"
      type='text'
    />
    <Field
      name='password'
      placeholder = "Enter password"
      component="input"
      type='password'
    />
    {error && <strong>{error}</strong>}
    <button type="submit">Login</button>
  </form>
)

Initial values

Många gånger vill du att dina forms ska ha några initiala värden (möjligen baserat på tidigare användarval). Detta kan göras genom att lägga till initialValues-objektet till container props (till exempel genom mapStateToProps).

function mapStateToProps(state) {
  return {
    initialValues: {
      username: "admin"
    }
  }
}

Vanligtvis kommer initialvärden att fyllas på från application state.

Changing values programmatically

Det finns situationer, när ett värde ändras, eller du automatiskt vill ändra ett annat värde – till exempel om någon vill logga in med “gäst” användarnamn, så ändras lösenordet automatiskt till “gäst” också.

Först måste funktionen “ChangeChange” läggas till med reduxForm HOC.

export default reduxForm({
    form: 'login',
    validate,
    onChange
})(LoginPage);

onChange-funktionen kan läggas till ovanför Login Page komponenten.

import {reduxForm, change} from "redux-form";

const onChange = (values, dispatch, props, previousValues) => {
  if(values.username === "guest") {
    dispatch( change ('login', 'password', 'guest');
  }
}

Detta kan vara väl användbart om du har ett värde som är helt beroende av ett annat.

Sammanfattning

Genom att använda informationen i det här inlägget så bör du kunna implementera Redux Form i dina applikationer. Det är ett mycket kraftfullt verktyg, som i stort sett innehåller all den funktionalitet som behövs för hantering av application forms.

Om du behöver mer information så är det bästa stället att hitta det på här: www.redux-form.com

/ Radek

Origin: www.linkedin.com

Alla artiklar

Animation i React med React Transition Group

Häromdagen jobbade jag på ett nytt projekt utvecklat i React. En del av mitt jobb var att översätta statisk HTML till interaktiva React-components. Jag ville liva upp applikationen lite genom att använda animering.

Under lång tid sköttes det hela perfekt av jQuery. Men med uppkomsten av nya JavaScript-frames, som React och Angular, så försvann möjligheterna med jQuery. DOM-manipulation i React fungerar annorlunda. Komponenterna har livscykler som kan hoppa in och ut ur DOM.

Efter lite research kom jag över några animeringsbibliotek som exempelvis React Transition Group, React-spring, React-motion och andra. Vart och ett av dessa bibliotek har sina för- och nackdelar, några av dem var helt enkelt bara för krångliga att använda. Allt beror dock på projektets behov. Jag ville lägga till snygga bild- och övergångseffekter till vissa element. Det är egentligen en ganska enkel animering så jag bestämde mig för att välja React Transition Group  just på grund av dess enkelhet.

React Transition Group brukade vara en React add-on. Nu är det separerat och underhålls av communityn.

Jag vill nämna att React Transition Group-komponenterna inte ger oss själva animationen. I stället ger det oss en state för varje lifecycle så att vi kan tillämpa CSS-animering på komponenten när den ändrar state.

Låt oss ta en titt på hur det fungerar i praktiken. Jag har skapat en enkel sidebar. Den är gömd som standard och triggas av en knapp. När knappen klickas kommer komponenten att ändra state och producera en side bar.

import React, { Component } from "react";
import { render } from "react-dom";


class App extends Component {
//Initial state
 state = {
  show: false
 };

//Toggle show/hide sidebar
 toggleShow() {
  this.setState({
   show: !this.state.show
  });
 }

//Rendering sidebar
renderSidebar() {
 if(this.state.show) {
  return (
   <div
    style={{
     width: "50%",
      height: "100vh",
      paddingTop: '10px',
      background: "green",
      color: "white",
      textAlign: "center",
      transition: "all 1s ease",
      position: "absolute",
      top: 0,
      left: 0
     }}
    >
     Hi, there!
    </div>
   )
  } else {
    return null
   }
  }


 render() {
  return (
   <React.Fragment>
    {this.renderSidebar()}

    <button
     onClick={() => this.toggleShow()}
     style={{float: "right"}}>
      Button
    </button>

   </React.Fragment>
  );
 }
}


render(<App />, document.getElementById("root"));

Nu har vi fått igång det. Vi lägger till en fin slide effect när side bar dyker upp. Vi använder <Transition /> component. Låt oss först installera paketet och importera <Transition /> component till vår App.

npm install react-transition-group --save

import Transition from "react-transition-group/Transition";

Komponenten <Transition /> component kan finnas i fyra tillstånd: “Enter”, “entered”, “exiting” och “exited”. Det låter oss se dess beteende när det passerar från ett state till ett annat. Det tar också två huvudsakliga props: “timeout” och “in”.

I “timeout” -protokollet ställer vi in övergången i millisekunder. “In” -prop kommer att spåra staten. När “in” -värdet är lika med “true” flyttas en komponent till “in” -state och stannar där i 500ms innan man växlar till “inmatat” tillstånd. Samma process händer när “in” -värdet är lika med “false” – först flyttas en komponent till “exiting” och sedan till “exited” -state.

En annan viktig detalj jag vill nämna är att <Transition /> component returnerar en funktion med state som ett argument. Att placera children elements i det kommer inte att fungera.

Här är slutkoden.

import React, { Component } from "react";
import { render } from "react-dom";

//Import Transition component
import Transition from "react-transition-group/Transition";


class App extends Component {
//Initial state 
state = {
  show: false
 };

//Toggle show/hide sidebar
 toggleShow() {
  this.setState({
   show: !this.state.show
  });
 }


//Rendering sidebar
renderSidebar() {
 return (
  <Transition in={this.state.show} timeout={1000}>
   {state => (
    <div
     style={{
      width: "50%",
      height: "100vh",
      paddingTop: "10px",
      background: "green",
      color: "white",
      textAlign: "center",
      transition: "all 1s ease",
      position: "absolute",
      top: 0,
      //This is where all the magic happens :)
      left: state === "entering" || state === "entered" ? 0 : "-50%"
     }}
    >
     Hi, there!
    </div>
   )}
  </Transition>
 )
}


render() {
 return (
  <React.Fragment>
   {this.renderSidebar()}

   <button
    onClick={() => this.toggleShow()}
    style={{ float: "right" }} >
     Button
   </button>

  </React.Fragment>
  );
 }
}


render(<App />, document.getElementById("root"));

Som du kan se har jag justerat “renderSidebar ()” -funktionen. Jag har wrappat sidofältet med <Transition/> komponenten där “in” -prop lagrar staten och “timeout” är inställd på 1000ms. Allt det magiska sker på CSS ” “left” property. När state är lika med “inmatning” eller “inmatad” så tar den 0 och komponenten blir synlig. Annars flyttas den bort med -50 % (i förhållande till dess bredd) och blir osynlig.

Så här fungerar React Transition Group. Vi gör animationen med hjälp av CSS. Självfallet kan vi göra mycket mer med det. React Transition Group ger oss också andra komponenter så som <CSSTransition /> och <TransitionGroup /> där vi kan lägga till egna namn eller animera element i en lista.

Sammanfattningsvis vill jag säga att React Transition Group är ett utmärkt val för enkla animeringar.

/ Anastasia

Origin: www.linkedin.com

Alla artiklar

Hantera säkerhet i React med JWT

Under de senaste åren har det skett en stor förändring kring sättet vi använder programvara. Fokus har skiftats från stationära applikationer till lättdrivna applikationer i molnet (SaaS). Välkända företag som Facebook och Google har varit ledande i utvecklingen kring denna teknik.

Google har som exempel introducerat Angular, medan Facebook ligger bakom React, båda JavaScript bibliotek som används för att bygga webbgränssnitt.

På Visionmate har vi valt den senare alltså React för att bygga våra applikationer. I det här inlägget tänkte jag prata mer om hur vi hanterar själva säkerheten i applikationsutvecklingen.

JSON Web Token (JWT) är ett populärt sätt för att säkra upp och skydda dina processer (finns detaljerat i RFC 7519-standard).

Den stora fördelen med att använda en JWT-token (säkerhetsnyckel) är att du kan skjuta in extra information som bara kan avkodas av din back-end-kod.

Vi använder vi Grails för back-end och använder Spring Security REST-plugin för att generera och hantera JWT-tokens. Jag kommer inte gå in så mycket på just det idag, men jag eller någon annan i teamet planerar att skriva mer om ämnet framöver.

I det här exemplet använder jag Redux som state container inkluderat autentiseringsdata och nyckelinformation.

Inloggningsfunktionen skickar inte bara uppgifter om själva säkerhetsnyckeln utan även tilldelade användarroller. Anropet talar om alltså vilken typ av rättigheter användaren har – till exempel om de kan redigera vissa dokument eller få tillgång till adminpanelen.

API-inloggningsfunktionen (alla anrop görs med axios):

function login(user) {
        const config = {
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            }
        };
        return axios.post(`${SERVER_URL}/api/login`, user, config)       
    }

Login action:

export function login(username, password) {
    const user = {
        username,
        password
    };
    return (dispatch) => {
        ApiConnector.login(user)
            .then(response => {
                const data = response.data
                const user = {
                    access_token: data.access_token,
                    refresh_token: data.refresh_token,
                    roles: data.roles,
                    username: data.username,
                };
                sessionStorage.setItem('user', JSON.stringify(user));
                dispatch({
                    type: LOGIN,
                    user
                });
            })
            .catch((error) => {
                console.log(error)
            })
    }
}

Eftersom vi har en asynkron kommunikation så används redux-thunk middleware för att hantera den. Som du kan se innehåller svaret inte bara åtkomst- och uppdateringstoken, men även användarroller som senare kan användas för att till exempel göra ytterligare paneler på ett dashboard.

Nyckeln lagras sedan i antingen en session- eller lokal lagring beroende på applikationskraven, det här gör att användaren inte behöver logga in varje gång en webbläsare uppdateras.

Autentiseringsdatan lagras i en reducer:

export default function (state = {loggedIn: false}, action) {
    switch(action.type) {        
        case LOGIN:
            return {...state,
                user: action.user,                 
                loggedIn: true                
            };
        case LOGOUT:
            return {...state,
                user: {},
                loggedIn: false
            };
        default:
            return state
    }
}

En JWT-nyckel inkluderas i varje anrop. Därför kan den lagras i en separat JavaScript-fil som enkelt hämtas senare.

I det här fallet lagras den i headers.js:

export default () => {
    return {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${sessionStorage.getItem('user') ? JSON.parse(sessionStorage.getItem('user')).access_token : null}`
    }
}

Funktionen adderas sedan till varje API-anrop:

import headers from "../security/headers";

 function get(target, params) {
        const config = {
            headers: headers(),
            params
        }
        return axios.get(`${SERVER_URL}/api/${target}`, config)
    }

När säkerhetsnyckeln löper ut och blir ogiltigt så behöver användaren inte logga in igen – det är tillräckligt att använda uppdateringen för att få en ny giltig access.

Uppdatera token function:

function refreshToken() {
        const config = {
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/x-www-form-urlencoded'
            }
        };
        const body = {
            grant_type: "refresh_token",
            refresh_token: JSON.parse(sessionStorage.user).refresh_token
        };
        axios.post(`${SERVER_URL}/oauth/access_token`, qs.stringify(body), config)
            .then(response => {
                const data = response.data
                const user = {
                    access_token: data.access_token,
                    refresh_token: data.refresh_token,
                    roles: data.roles,
                    username: data.username                    
                };
                sessionStorage.setItem('user', JSON.stringify(user));
                store.dispatch({
                    type: REFRESH_TOKEN,
                    user
                })
            })
    }

För att logga ut användaren, radera helt enkelt nyckeln från session/lokal lagring som visas i åtgärden nedan.

Logout action:

export function logout() {
    sessionStorage.removeItem('user');
    return {
        type: LOGOUT
    }
}

Summering

Som jag försökt visa i det här inlägget så måste vissa steg följas för att kunna använda JWT med din applikation. Du kan också använda exemplet som en mall för att lägga till mer funktionalitet och på så sätt utveckla din applikation.

/Radek

 

Bibliotek som används: redux, redux-thunk och axios.

Skicka gärna frågor och kommentarer till vår mail:

info(@)visionmate.se

 

Alla artiklar