Osa 5

HTTP-protokollan tilattomuus ja evästeet

HTTP on tilaton protokolla. Tämä tarkoittaa sitä, että jokainen pyyntö on erillinen kokonaisuus, joka ei liity aiempiin pyyntöihin. Suunnittelupäätöksen taustalla oli ajatus siitä, että verkkosivulle ladattava sisältö voi sijaita useammalla eri palvelimella. Jos HTTP ottaisi kantaa käyttäjän tilaan, tulisi myös hajautettujen ratkaisujen tilan ylläpitoon ottaa kantaa — tämä olisi myös ollut melko tehotonta (Basic HTTP as defined in 1992). Päätös tilattomuudesta on ollut järkevä: suurin osa verkkoliikenteestä liittyy muuttumattoman sisällön hakemiseen, jolloin palvelinten ei tarvitse varata resursseja käyttäjän tilan ylläpitämiseen, eikä palvelinten ja selainohjelmistojen toteuttajien ole tarvinnut toteuttaa mekanismeja käyttäjien tilan ylläpitämiseen.

Käyttäjän tunnistamiseen pyyntöjen välillä on kuitenkin tarvetta. Esimerkiksi verkkokaupat ja muut käyttäjän kirjautumista vaativat palvelut tarvitsevat tavan käyttäjän tunnistamiseen. Klassinen — mutta huono — tapa käyttäjän tunnistamiseen on ollut säilyttää GET-muotoisessa osoitteessa parametreja, joiden perusteella asiakas voidaan tunnistaa palvelinsovelluksessa. Tämä ei kuitenkaan ole suositeltavaa, sillä osoitteessa olevia parametreja voi muokata käsin.

HTTP-protokollan tilattomuus ei pakota palvelinohjelmistoja tilattomuuteen. Mikäli haluamme pitää kirjaa käyttäjistä, löytyy siihen erilaisia keinoja yllä mainitusta GET-parametrin käytöstä lähtien. Toistaiseksi yleisin tekniikka käyttäjän tilan ylläpitoon ja tunnistamiseen on HTTP-pyynnön mukana kulkevat evästeet.

HTTP ja evästeet

Merkittävä osa verkkosovelluksista sisältää käyttäjäkohtaista toiminnallisuutta, jonka toteuttamiseen sovelluksella täytyy olla jonkinlainen tieto käyttäjästä sekä mahdollisesti käyttäjän tilasta. HTTP-protokollan versio 1.1 sekä uudemmat tarjoavat mahdollisuuden tilallisten verkkosovellusten toteuttamiseen evästeiden (cookies) avulla.

Evästeet toteutetaan HTTP-protokollan otsakkeiden avulla. Kun käyttäjä tekee pyynnön palvelimelle, ja palvelimella halutaan asettaa käyttäjälle eväste, palautetaan vastauksen osana otsake Set-Cookie, jossa määritellään käyttäjäkohtainen evästetunnus. Otsake voi olla esimerkiksi seuraavan näköinen:

Set-Cookie: fb1a1466ccceee22a0=0hr0aa2ogdfgkelogg; Max-Age=3600; Domain=".helsinki.fi"

Yllä oleva palvelimelta palautettu vastaus pyytää selainta tallettamaan evästeen. Kun selaimella on tiettyyn osoitteeseen liittyvä eväste tallennettuna, tulee sen jatkossa lisätä eväste fb1a1466ccceee22a0=0hr0aa2ogdfgkelogg jokaiseen helsinki.fi-osoitteeseen lähetettävään pyyntöön. Yllä eväste on voimassa tunnin, eli selain ja palvelin voi unohtaa sen tunnin kuluttua sen asettamisesta.

Tarkempi syntaksi evästeen asettamiselle on seuraava:

Set-Cookie: nimi=arvo [; Comment=kommentti] [; Max-Age=elinaika sekunteina]
                    [; Expires=parasta ennen paiva] [; Path=polku tai polunosa jossa eväste voimassa]
                    [; Domain=palvelimen osoite (URL) tai osoitteen osa jossa eväste voimassa]
                    [; Secure (jos määritelty, eväste lähetetään vain salatun yhteyden kanssa)]
                    [; Version=evästeen versio]

Evästeet tallennetaan selaimen sisäiseen evästerekisteriin, josta niitä haetaan aina kun käyttäjä tekee selaimella kyselyn. Evästeet lähetetään palvelimelle jokaisen viestin yhteydessä Cookie-otsakkeessa.

Cookie: fb1a1466ccceee22a0=0hr0aa2ogdfgkelogg

Evästeiden nimet ja arvot ovat yleensä monimutkaisia ja satunnaisesti luotuja niiden yksilöllisyyden takaamiseksi. Tällöin jokaisella käyttäjällä on oma uniikki eväste, jonka käyttäjä lähettää palvelimelle.

Yleisesti ottaen evästeet ovat sekä hyödyllisiä että haitallisia: niiden avulla voidaan luoda yksilöityjä käyttökokemuksia tarjoavia sovelluksia ja esimerkiksi toteuttaa verkkokauppoja, mutta niitä voidaan käyttää myös käyttäjien seurantaan. Sovellusten kehittäjien pitää myös tiedostaa tämä — esimerkiksi Googlen ilmaiseksi tarjoama Google Analytics-palvelu, jota käytetään sivustojen kävijöiden seurantaan, kerää tietoa käyttäjistä. Mikäli tällaisia järjestelmiä käytetään arkaluonteisia tietoja sisältävillä sivuilla, voi käyttäjästä vuotaa yksityiseksi toivottuja tietoja ulkopuolisille tahoille. Tämä on näkynyt myös mediassa — lue Helsingin sanomien uutinen aiheesta.

Evästeet ja istunnot eli sessiot

Kun selain lähettää palvelimelle pyynnön yhteydessä evästeen, palvelin etsii evästeen perusteella käynnissä olevaa istuntoa eli sessiota. Jos sessio löytyy, annetaan siihen liittyvät tiedot sovelluksen käyttöön käyttäjän pyynnön käsittelyä varten. Jos sessiota taas ei löydy, aloitetaan uusi sessio, johon liittyvä eväste palautetaan käyttäjälle vastauksen yhteydessä.

Javassa sessioiden käsittelyyn löytyy HttpSession-luokka, joka tarjoaa välineet sessio- ja käyttäjäkohtaisen tiedon tallentamiseen. Oleellisimmat luokan metodit ovat public void setAttribute(String name, Object value), joka tallentaa sessioon arvon, sekä public Object getAttribute(String name), jonka avulla kyseinen arvo löytyy.

HttpSession-olion saa Springissä yksinkertaisimmillaan käyttöön lisäämällä sen kontrollerimetodin parametriksi. Tällöin Spring asettaa kyseiseen pyyntöön liittyvän session metodin parametriin automaattisesti. Alla on kuvattuna sovellus, joka pitää sessiokohtaista kirjaa käyttäjien tekemistä pyynnöistä.

import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class VisitCountController {

    @GetMapping("*")
    @ResponseBody
    public String count(HttpSession session) {
        int visits = 0;
        if (session.getAttribute("count") != null) {
            visits = (int) session.getAttribute("count");
        }

        visits++;
        session.setAttribute("count", visits);

        return "Visits: " + visits;
    }
}

Kun käyttäjä tekee ensimmäistä kertaa pyynnön sovellukseen, palauttaa sovellus merkkijonon "Visits: 1". Vastauksen yhteydessä palautetaan myös eväste. Kun käyttäjä tekee seuraavan kerran pyynnön sovellukseen, lähettää selain pyynnön yhteydessä myös evästeen palvelimelle, jolloin palvelin osaa tunnistaa käyttäjän ja hakee oikean istunnon tiedot — vastaukseksi palautuu lopulta merkkijono "Visits: 2".

Loading

HttpSession-olioon pääsee käsiksi myös muualla sovelluksessa ja sen voi injektoida esimerkiksi palveluun @Autowired-annotaation avulla. Edellinen kontrolleriin toteutettu toiminnallisuus voitaisiin tehdä myös palvelussa.

// importit

@Service
public class CountService {

    @Autowired
    private HttpSession session;

    public int incrementAndCount() {
        int count = 0;
        if (session.getAttribute("count") != null) {
            count = (int) session.getAttribute("count");
        }

        count++;
        session.setAttribute("count", count);
        return count;
    }
}

Nyt kontrollerin koodi olisi kevyempi:

// importit

@Controller
public class VisitCountController {

    @Autowired
    private CountService countService;

    @RequestMapping("*")
    @ResponseBody
    public String count() {
        return "Visits: " + countService.incrementAndCount();
    }
}
Loading

Spring luo oletuksena yhden ilmentymän luokasta

Spring luo oletuksena yhden olion jokaisesta sen hallinnoimasta luokasta — viite tähän yksittäiseen olioon kopioidaan jokaisen @Autowired-annotaatiolla määritellyn olion arvoksi. Joskus hallinnoiduista luokista halutaan kuitenkin useampi versio, esimerkiksi vaikkapa käyttäjäkohtaisesti.

Hallinnoitujen olioiden luomista voidaan kontrolloida erillisen @Scope-annotaation avulla, mikä mahdollistaa ilmentymien luonnin esimerkiksi sessiokohtaisesti. Seuraavassa on esimerkki ostoskorista, joka on sessiokohtainen: jokaiselle sessiolle luodaan oma ostoskori, eli ostoskori tulee olemaan käyttäjäkohtainen.

Alla käytetty annotaatio @Component on luokan toiminnalle oleellinen — se kertoo Springille, että tätäkin luokkaa tulee hallinnoida. Sen avulla Spring tietää, että luokka tulee kopioida @Autowired-annotaatiolla merkittyihin olioihin.

// importit

@Data @NoArgsConstructor
@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ShoppingCart implements Serializable {

    private Map<Item, Integer> items = new HashMap<>();
}

Ylläolevasta komponentista luotavat ilmentymät ovat elossa vain käyttäjän session ajan, eli sen aikaa kun käyttäjän eväste on voimassa. Ylläolevasta ostoskorista saa lisättyä ilmentymän sovellukseen aivan kuten muistakin komponenteista, eli @Autowired-annotaatiolla.

Loading

Lakiteknisiä asioita evästeisiin liittyen

Euroopan komissio on säätänyt yksityisyydensuojaan liittyvän lain, joka määrää kertomaan käyttäjille evästeiden käytöstä. Käytännössä käyttäjältä tulee pyytää lupaa minkä tahansa sisällön tallentamiseen hänen koneelleen (ePrivacy directive, Article 5, kohta (3)). Myöhemmin säädetty tarkennus tarkentaa määritelmää myös evästeiden käytön kohdalla.

(25) However, such devices, for instance so-called "cookies", can be a legitimate and useful tool, for example, in analysing the effectiveness of website design and advertising, and in verifying the identity of users engaged in on-line transactions. Where such devices, for instance cookies, are intended for a legitimate purpose, such as to facilitate the provision of information society services, their use should be allowed on condition that users are provided with clear and precise information in accordance with Directive 95/46/EC about the purposes of cookies or similar devices so as to ensure that users are made aware of information being placed on the terminal equipment they are using. Users should have the opportunity to refuse to have a cookie or similar device stored on their terminal equipment. This is particularly important where users other than the original user have access to the terminal equipment and thereby to any data containing privacy-sensitive information stored on such equipment. Information and the right to refuse may be offered once for the use of various devices to be installed on the user's terminal equipment during the same connection and also covering any further use that may be made of those devices during subsequent connections. The methods for giving information, offering a right to refuse or requesting consent should be made as user-friendly as possible. Access to specific website content may still be made conditional on the well-informed acceptance of a cookie or similar device, if it is used for a legitimate purpose.

Lisätietoa mm. https://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2009:337:0011:0036:EN:PDF ja https://finlex.fi/fi/laki/ajantasa/2014/20140917#L24P205.

Pääsit aliluvun loppuun! Jatka tästä seuraavaan osaan: